Na následující .NET Tip mě přivedl nedávný dotaz v diskusích o dělení v SQL. My se budeme bavit o VB.NET a C# a ukážeme si, jak tam fungují aritmetické operace, zejména dělení, protože tam je to krásně vidět.
Číselné literály
Pokud někam do kódu napíšu jako číslo 15, bere se to automaticky jako hodnota typu System.Int32 (tedy Integer resp. int). Přestože se patnáctka vejde do typu Byte, je to prostě Integer, protože ten má velikost 32 bitů a procesory s ním pracují nejrychleji. Pokud by číslo bylo větší a do intu se nevešlo, kompilátor to bude brát jako hodnotu typu System.Int64, tedy Long. Jakmile za číslo dopíšeme desetinnou část, konstanta se změní na datový typ System.Double, takže 15.0 je prostě double.
Co je to operátor?
Operátor je vlastně úplně normální funkce, která na vstupu dostane operandy a vrací výsledek. Podívejme se třeba na operátor dělení, ve VB.NET i v C# máme operátor /. V každém jazyce se chová ale trochu jinak.
Ve VB.NET má operátor / funkci obyčejného dělení, jehož výsledkem je vždy desetinné číslo. Pro celočíselné dělení máme speciální operátor \. Naproti tomu v C# je tomu jinak, tam operátor / slouží jak pro normální dělení s desetinným výsledkem, tak i pro dělení celočíselné.
Ve VB.NET je operátor dělení definován takto:
Datové typy operandů (VB.NET) | Výsledek |
Oba výrazy celočíselné | Double |
Oba výrazy typu Decimal | Decimal |
Oba výrazy typu Single | Single |
Alespoň jeden výraz typu Single nebo Double | Double |
V C# je operátor dělení definován takto:
Datové typy operandů (C#) | Výsledek |
Oba výrazy Byte, SByte, Int16, UInt16, Int32 nebo UInt32 | Int32/UInt32 |
Oba výrazy celočíselné, alespoň jeden typu Int64 nebo UInt64 | Int64/UInt64 |
Oba výrazy typu Decimal | Decimal |
Oba výrazy typu Single | Single |
Alespoň jeden výraz typu Single nebo Double | Double |
Tady vidíme, že při dělení celých čísel je ve VB.NET výsledkem číslo desetinné, zatímco v C# je to opět celé číslo.
Jak fungují konverze?
Číselné datové typy v .NET Frameworku mají za jistých okolností povoleny automatické konverze. To znamená, že je možné je přiřadit do proměnné jiného typu bez příslušného přetypování. Těchto konverzí jsou dva druhy, které se od sebe liší.
Bez obav můžeme přetypovat Int32 na Int64 a nemusíme se bát, že o nějakou informaci přijdeme. Stejně tak můžeme přetypovat i Single na Double, Int32 na Decimal nebo třeba Byte na Single. Takové konverze se nazývají widening conversions protože všechny hodnoty prvního datového typu je možné reprezentovat v datovém typu druhém (všechny možné hodnoty typu Byte můžeme uložit do Single). Navíc tyto konverze probíhají automaticky a přetypovávat nemusíme. Mimochodem přiřazení FileStreamu do proměnné typu Stream je samozřejmě také widening conversion, opět nemusíme přetypovávat a každý FileStream můžeme bezpečně uložit do proměnné typu Stream (opačně ne!).
Opačné konverze se nazyvají narrowing conversions a u nich už může dojít ke ztrátě nějaké informace nebo k chybě. Je to třeba přiřazení proměnné typu Double do proměnné typu Int32 (provádíme zaokrouhlení, ztrácíme informaci), nebo Int64 do Int32. Pokud se velké číslo do Int32 nevejde, v C# se ve standardním nastavení horní bity se oříznou, takže v cílové proměnné dostaneme jiné číslo. VB.NET standardně kontroluje chyby přetečení, takže skončíme s výjimkou OverflowException. Kontrola přetečení se jak u C#, tak u VB.NET dá na úrovni aplikace zapnout nebo vypnout nastavením kompilátoru, v C# ji můžete navíc vynutit nebo potlačit pro výraz nebo blok kódu klíčovými slovy checked a unchecked.
Stejně tak je narrowing conversion i přetypování Streamu na FileStream, které klidně může skončit chybou, protože každý Stream nemusí být FileStream.
Jak je to ve VB.NET a v C#?
V C# se widening convertions provádí automaticky a přetypovávat nemusíme. Klidně můžeme napsat double a = 5; a už víme, že 5 je sice hodnota typu Int32, ale převod na double je právě widening conversion a provede se automaticky.
Naopak narrowing conversions se automaticky neprovádí a přetypovávat musíme. Takže pokud napíšeme float a = 15.3; a budeme se divit, proč to nefunguje, je to proto, že 15.3 je hodnota typu double a my se ji snažíme narvat do floatu, tedy datového typu s menší přesností. Možnosti jsou dvě - buď přetypovat (float a = (float)15.3;) anebo použít u literálu suffix f, který řekne, že tahle konstanta bude typu float. Zápis by vypadal takto: float a = 15.3f;.
Obdobně musíme přetypovávat při FileSteam fs = (FileStream)s, pokud s je typu Stream. To může i skončit chybou, pokud v s bude třeba NetworkStream.
Ve VB.NET se ve výchozím nastavení oba typy konverzí provádějí automaticky a přetypovávat nemusíme. Pokud chceme VB donutit, aby narrowing konverze kontroloval a nutil nás do přetypování jako C#, stačí v kompilátoru zapnout Strict Mode.
Jak teda funguje to dělení?
Pokud jste všechno pochopili správně, měli byste být schopni odpovědět na tuto otázku:
1. Jaký bude datový typ proměnné retVal, jakou bude mít hodnotu a proč? Kód je ve VB 9 se zapnutou volbou Option Infer (default), takže retVal nebude typu Object, ale datový typ se určí při kompilaci automaticky podle výsledku přiřazovaného výrazu.
Dim retVal = 15 / 4
2. Jaký bude datový typ proměnné retVal, jakou bude mít hodnotu a proč? Kód je v C# 3, datový typ proměnné opět určí kompilátor podle výsledku výrazu.
var retVal = 15 / 4;
Závěrem
Doufám, že vám tento .NET Tip pomohl zase trochu nahlédnout do toho, jak v .NETu věci fungují. No a já si jdu zahrát golf.