Programozzunk C#-ban – 3. rész

A mai leckében megismerkedünk a C# típuskészletével, adatok be – és kírásának módjával, valamint a kivételkezelés alapjaival.

Referencia vs érték

A C# egy erősen típusos nyelv, amely rendelkezik dinamikus típusossággal. Ez a mondat elsőre ellentmondásnak tűnhet. Azonban a C# mindkettővel rendelkezik. A dinamikus típusosság a .NET 4.0 újdonsága volt, így ez a funkció csak .NET 4.0-át vagy újabb keretrendszert célzó alkalmazások esetén használható.

Azonban mielőtt még nagyon belemennék a típusokba, érdemes tisztázni, hogy C# esetén kezelési szempontból kétféle típus létezik. Vannak az értéktípusok és a referenciatípusok. Az értéktípusok minden függvény hívás esetén értékként lesznek átadva. Ez azt jelenti, hogy konkrétan nem a változó van átpasszolva a függvénynek, hanem annak az értéke, vagyis hívás előtt másolódik az érték, tehát az adat nagyon rövid ideig 2x van benne a memóriában.

Ennek a viselkedésmódnak egyszerű típusok, mint számok, szövegek esetén van értelme. De nem csak ezen típusok viselkednek így, hanem minden struktúrából származtatott objektum. Az egyszerű típusok egyébként struktúraként vannak megvalósítva a keretrendszerben.

A másik fő típus a referenciatípusok. Ezek lényegében az osztályok. Itt már ténylegesen a változó van átadva, nem történik másolás, csak 1x van jelen az objektum a memóriában. Ez nagy mennyiségű adatok tárolásánál jó, de néha problémát okoz, ha referencia helyett értékre lenne szükségünk. Erre is kitaláltak megoldásokat, de erről majd a felületek kapcsán írok részletesen.

Eddig zavarosnak tűnhet, a dolog, szóval jöjjön egy példakód.

using System;
 
namespace _02_ertekatadas_pelda
{
    class osztaly
    {
        public double ertek;
    }
 
    class Program
    {
        static void ErtekatadoPelda(double ertek)
        {
            ertek = 2.1;
        }
 
        static void ReferenciaPelda(osztaly referencia)
        {
            referencia.ertek = 2.1;
        }
 
        static void Main(string[] args)
        {
            Console.WriteLine("Érték típus példa");
            Console.WriteLine();
 
            double ertek = 3.14;
            Console.WriteLine("Függvény hívás előtt az ertek: {0}", ertek);
            ErtekatadoPelda(ertek);
            Console.WriteLine("Függvény hívás utan az ertek: {0}", ertek);
 
            Console.WriteLine();
            Console.WriteLine("Referencia típus példa");
            Console.WriteLine();
 
            osztaly o = new osztaly();
            o.ertek = 3.14;
            Console.WriteLine("Függvény hívás előtt az ertek: {0}", o.ertek);
            ReferenciaPelda(o);
            Console.WriteLine("Függvény hívás utan az ertek: {0}", o.ertek);
        }
    }
}

A program kimenete:

Érték típus példa
 
Függvény hívás előtt az ertek: 3,14
Függvény hívás utan az ertek: 3,14
 
Referencia típus példa
 
Függvény hívás előtt az ertek: 3,14
Függvény hívás utan az ertek: 2,1

A kód lényege az, hogy az érték típusok esetén, ha egy függvény megpróbálja a függvényen belül módosítani a paraméterként kapott értéket, akkor csak a függvényen belül lesz maradandó a változás, a függvényhívás végeztével az eredetileg átadott érték nem módosul, míg a referencia típusoknál igen.

Az értéktípusok is átadhatóak referenciaként, de ehhez módosítani kell a függvény definíciót és a hívást is. Az alábbi példa ezt mutatja be:

using System;
 
namespace _03_ertekatadas_referencia
{
    class Program
    {
        static void ErtekatadoPelda(ref double ertek)
        {
            ertek = 2.1;
        }
 
        static void Main(string[] args)
        {
            Console.WriteLine("Érték típus referenciaként példa");
            Console.WriteLine();
 
            double ertek = 3.14;
            Console.WriteLine("Függvény hívás előtt az ertek: {0}", ertek);
            ErtekatadoPelda(ref ertek);
            Console.WriteLine("Függvény hívás utan az ertek: {0}", ertek);
        }
    }
}

A módosításban bevezetett kulcsszó a ref. Ennek segítségével az értéktípusok esetén kikényszeríthető a referenciaátadás az érték helyett. A kulcsszót a hívásban és a függvény definíciójában is el kell helyezni.

Típusok

Mivel erősen típusos a nyelv, rengeteg beépített típus van. Ezek a típusok a .NET keretrendszerben vannak megvalósítva struktúraként és minden .NET keretrendszeren futó nyelvből használhatóak. A C# esetén speciális kulcsszavak is be vannak vezetve a típusok jelölésére. Az alábbi táblázat a típusrendszert foglalja össze:

C# adattípus .NET típus Méret byte-ban Alsó határ Felső határ
sbyte System.Sbyte 1 -128 127
byte System.Byte 1 0 255
short System.Int16 2 -32768 32767
ushort System.UInt16 2 0 65535
int System.Int32 4 -2147483648 2147483647
uint System.Uint32 4 0 4294967295
long System.Int64 8 -2^63 2^63 – 1
ulong System.UInt64 8 0 2^64 – 1
char System.Char 2 0 65535
float System.Single 4 1,5 x 10^-45 3.4 x 10^38
double System.Double 8 5.0 x 10^-324 1.7 x 1010 ^ 308
bool System.Boolean 1 false (0) true (1)
decimal System.Decimal 16 1.0 x 10^-26 7.9 x 10^28
string System.String  n karakter esetén (n+1)*2 byte a mérete

A .NET 4.0 óta van még két speciális szám típus. Ezek a System.Numerics névtérben találhatóak meg. A BigInt típus nagy méretű egész számok tárolására képes, 128 bites egész számok segítségével kezelhetőek. A Complex típus pedig komplex számok leírására szolgál.

A var kulcsszó segítségével egy automatikus típusú változót hozhatunk létre. Ez nem dinamikus típus, mivel a fordítás pillanatában eldől a változó típusa. Hasznos, ha olyan osztályokat akarunk példányosítani, amelyeknek kilométer hosszú neve van.

A dynamic típus segítségével egy olyan változót hozhatunk létre, amelynek a típusa futás közben határozódik meg. Ennek segítségével megkapjuk az olyan nyelvek előnyeit, mint a Python, vagy a PHP, de ezért árat kell fizetni. Az ár pedig sebességben mérhető. A dynamic típust használó kódok jóval lassabban fognak futni. Továbbá, ha dynamic típust használunk, akkor le kell mondanunk programozás közben az IntelliSense összes előnyéről. Ezt a típust azért vezették be egyébként, hogy a C# programjainkba beágyazhatóak legyenek dinamikus típusos szkript nyelvek, mint az IronPython és az IronRuby.

A string-eken különféle műveleteket végezhetünk. Egy dolog azonban fontos. Ha n darab szövegen végzünk összefűzést, az így fog működni: összefűzi az első kettőt, beteszi egy új string változóba. Az új kapott változón és a következőn elvégzi a műveletet, közben csinál egy új változót. Ezt ismétli egészen addig, amíg a végére nem jut. Ez, ha n nagy szám, akkor iszonyat nagy kézifék a programban, valamint iszonyat nagy memória zabáló is.

Jobb megoldás, ha sok szövegen kell műveletet végezni, a StringBuilder osztály alkalmazása, amely hasonló funkcionalitással rendelkezik, mint a string osztály, de láncolt listás adattárolást használ, így ha bővítjük, akkor nem keletkeznek ideiglenes string változók a memóriában.

Adatok bekérése, kiírása

C# esetén a konzolról a Console osztály megfelelő függvényei segítségével tudunk adatokat bekérni. A bekért adat lehet egy adott gomb, vagy egy sor szöveg, esetleg egy karakter. A beolvasásra használható függvények:

int Console.Read();

Egy karaktert olvas be, de a karaktert int típusban adja vissza, ezért konvertálni kell, ha a tényleges karakterre és nem a kódjára vagyunk kíváncsiak, akkor konvertálni kell.

string Console.ReadLine();

Egy sor adatot olvas be. A sor elválasztó karakter az Enter, ami a szövegekben “\n” karakterrel hivatkozható.

ConsoleKeyInfo Console.ReadKey();

Egy gombot olvas be. A gomb adatai a ConsoleKeyInfo osztályban tárolódnak.

A szövegek önmagukban nem sok mindenre jók, viszont lehet őket konvertálni mindenféle adatra. A Convert osztály egy csomó adatkonvertáló függvényt valósít meg, aminek a segítségével könnyen és egyszerűen konvertálhatunk a típusok között.

A C/C++ stílusú adattípus konvertálás is megmaradt, de ezen konvertálási móddal például nem tudunk szöveget közvetlenül más típussá alakítani, illetve ez a fajta konvertálás nem minden esetbe fogja ugyan azt adni, mint a Convert osztály megfelelő függvénye. A Convert osztály függvényei To névvel kezdődnek, ami után egy .NET típusazonosító áll. Ez fejezi ki azt, hogy a függvény adott típusra konvertál.

Az alábbi példa a Convert osztály alkalmazását mutatja be bekért adatok esetén:

using System;
 
namespace _04_adatbekeres_convert
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Bekérés és konvertálás példa");
 
            Console.Write("Írj be egy számot: ");
            string sor =  Console.ReadLine();
 
            Console.Write("Írj be egy lebegőpontos számot: ");
            string sor2 = Console.ReadLine();
 
            Console.WriteLine("A bekért szám +1: {0}", Convert.ToInt32(sor) + 1);
            Console.WriteLine("A bekért lebegőpontos szám osztva 2-vel: {0}", Convert.ToDouble(sor2) / 2);
 
        }
    }
}

Ha változókat szeretnénk kiíratni a szövegünkben, akkor nem kell azzal bajlódnunk, hogy külön Write vagy WriteLine hívással írassuk ki őket. A szövegben, ahol változó tartalmát szeretnénk beilleszteni, ott egyszerűen kapcsos zárójelek között írjunk be egy számot. Ez a szám arra hivatkozik, hogy a szöveg után, a WriteLine függvény melyik paraméterét kell beilleszteni a programnak a kiírásba. A számozás 0-tól kezdődik, mint ahogy a fenti példában látható.

Kivételkezelés

A kivételkezelés egy igen hasznos nyelvi tulajdonság, segítségével felkészíthetjük a programot arra, ha a felhasználó hülyeséget ír be. C esetén is lehet persze hibákat kezelni, de ott a függvény visszatérési értéke egy int típus, ami jelzi a sikerességet, vagy a sikertelenséget, cserébe, ha tényleges értéket akar visszaadni a függvény, akkor szenvedhetünk referenciákkal meg mutatókkal. Valamint, ha több függvényt használunk, amelyek egymástól függenek, akkor minden egyes függvényhívás eredményét if-else párosokkal kell körbevenni, majd ha az egyik már hülyeséget érzékel, akkor megszakítani a folyamatot, átugorni egy másik pontra.

Gyanítom kellően zavaros és érthetetlen ez a megközelítés. Ha így van, ne csodálkozzunk, a C/C++ szoftverek többségében ebből a hülye megközelítésből ered a legtöbb szoftver hiba.

C# esetén típust ad vissza a függvény, ha pedig hiba volt a futás közben, akkor dob egy hibát, amit egy kivételkezelő blokkal elkapunk. Ez a megközelítés C++ esetén már létezik, de ott nem használja egy függvény se a beépített könyvtárból, így nem is igen ismert. Ennek a fő oka az, hogy a kivételkezelés akkor működik jól, ha csak egy adott osztályból származtatott típusokat lehet kivételként dobni és elkapni.

C# esetén ez megvalósított dolog. Az összes kivételeset, amit kezelhetünk, az Exception osztályból származik, amely rendelkezik egy szöveges leírással és egy csomó nyomkövetési információval, amivel megkönnyíthető a kivételkezelés. Ha saját típusú kivételeket szeretnénk dobálni, akkor az Exception osztályból kell származtatnunk a saját osztályunkat, de számos specializált Exception osztály létezik.

A kivételt úgy tudjuk leginkább realizálni, ha az előző példában a bekérésnél szám helyett, mondjuk azt adjuk meg, hogy asdf. Ekkor normál módban futtatva a programunkat kapunk egy semmitmondó Windows hibaüzenetet, miszerint a program elszállt.

hiba

Viszont, ha debug üzemmódban indítjuk el a programunkat (Debug menü -> Start Debugging), és ugyanúgy hibás bemenetet adunk meg, akkor kapunk egy üzenetet, miszerint nem kezeltünk egy kivételt. Jelen esetben a kivétel típusa FormatException, ami akkor dobódik, ha a beviteli adat konvertálás esetén nem megfelelő formátumú.

hiba_debug

Az alábbi kódrészlet azt mutatja be, hogy hogyan lehet kezelni a futás közben fellépő hibákat.

using System;
 
namespace _05_adatbekeres_convert_hibakezelt
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Bekérés és konvertálás példa");
 
            Console.Write("Írj be egy számot: ");
            string sor = Console.ReadLine();
 
            Console.Write("Írj be egy lebegőpontos számot: ");
            string sor2 = Console.ReadLine();
 
            try
            {
                Console.WriteLine("A bekért szám +1: {0}", Convert.ToInt32(sor) + 1);
                Console.WriteLine("A bekért lebegőpontos szám osztva 2-vel: {0}", Convert.ToDouble(sor2) / 2);
            }
            catch (Exception)
            {
                Console.WriteLine("Hiba történt!! Nem megfelelő a bevitt adat.");
            }
 
        }
    }
}

A try blokkban lévő kód az, ami konkrétan kivél védett. A try blokk utáni catch rész határozza meg, hogy milyen hibákat akarunk elkapni. Egy catch blokk csak egy típust kaphat el, de egy try blokk után több catch blokk is jöhet a különböző hibák elkapására és kezelésére. Jelen esetben a blokk mindenféle hibát elkap, mivel Exception típus van beírva és ebből a típusból származik az összes hiba.

Ha a zárójelben (Exception valami) állna, akkor a valami nevű változón keresztül információkat tudnánk kiírni a hiba típusáról a felhasználónak.

A try blokk után állhat egy finally blokk is. Ez olyan kódot határoz meg, amely a kivételkezelés sikeressége és sikertelensége esetén is lefusson. Ide tehetők például a blokkban használt osztályok erőforrás felszabadító utasításai. Az alábbi példa részlet a kiegészített kivételkezelést mutatja be:

try
{
    Console.WriteLine("A bekért szám +1: {0}", Convert.ToInt32(sor) + 1);
    Console.WriteLine("A bekért lebegőpontos szám osztva 2-vel: {0}", Convert.ToDouble(sor2) / 2);
}
catch (Exception ex)
{
    Console.WriteLine("Hiba történt!! Nem megfelelő a bevitt adat.");
    Console.WriteLine("A hiba leirasa: {0}", ex.Message);
}
finally
{
    Console.WriteLine("töltöttkáposzta");
}

Példák helye

A sorozat példakódjai a https://github.com/webmaster442/csharppeldak címről tölthetőek le