Keri sisuni

Klassid

Objekt-orienteeritud programmeerimises on kesksel kohal klassid. Klass on struktuur, mis sisaldab nii andmeid kui ka nendega seotud toiminguid.

Vaatame näitena klassi Person, mille abil saame süsteemis esitada isikuid. Sellel klassil on kaks omadust - FirstName ja LastName, lisaks sellele toiming nimega Rename, mille abil saab muuta isiku nime.

public class Person
{
    // Omadused
    public string FirstName { get; set; }
    public string LastName { get; set; }

    // Meetod
    public void Rename(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }
}

var person = new Person();      // loo uus Person-tüüpi objekt
person.FirstName = "Jaan";      // omista eesnimeks Jaan
person.LastName = "Jalgaratas"; // omista perenimeks Jalgratas

var firstName = person.FirstName; // salvesta eesnimi muutujasse firstName
var lastName = person.LastName;   // salvesta perenimi muutujasse lastName

person.Rename("Juhan", "Juurikas"); // Kutsu Rename() meetodi
using System;

namespace OOP
{
    public class Person
    {
        // Omadused
        public string FirstName { get; set; }
        public string LastName { get; set; }

        // Meetod
        public void Rename(string firstName, string lastName)
        {
            FirstName = firstName;
            LastName = lastName;
        }
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            var person = new Person();
            person.FirstName = "Jaan";
            person.LastName = "Jalgaratas";

            Console.WriteLine(person.FirstName + " " + person.LastName);

            person.Rename("Juhan", "Juurikas");

            Console.WriteLine(person.FirstName + " " + person.LastName);
        }
    }
}

Omadused ja klassi sisemised muutujad moodustavad klassi andmete osa. Klassi funktsioone nimetatakse meetoditeks (method) ning need moodustavad klassi toimingute osa. Meetodites saame kasutada kõiki klassi osi, mida soovime.

Klassid koosnevad järgmistest komponentidest:

  • Klassi muutujad (class variable, class field) - klassi sees defineeritud muutujad, mida kasutatakse peamiselt klassi sisemiseks tööks ja mida teistele klassidele ei näidata.
  • Omadused (property) - sarnased klassi muutujatele, kuid võivad sisaldada ka koodi (kui klass peab oma muutujaid teistele klassidele näitama, siis selleks kasutame omadusi).
  • Meetodid (method) - klassiga seotud toimingud (sarnased funktsioonidele, kuid toimetavad klassi piires ainult).
  • Sündmused (event) - klassid võivad pakkuda teistele klassidele sündmuste "kuulamist" (sündmustega tutvume aine raames hiljem).
  • Konstandid (constant) - konstandid on sarnased klassi muutujatele, kuid neil on alati kindel väärtus, mis ei muutu.

Jäta meelde!

Klassi muutujad, omadused, meetodid ja sündmused kannavad ühist nimetust class members. Hea eesti keelne vaste sellele terminile puudub.

Klassi muutujad

Klassi muutujad on sellised muutujad, mida saavad kasutada kõik antud klassi omadused ja meetodid. Enamasti klassi muutujaid klassist väljapoole ei näidata, sest klassi ehitamise juures ei eeldata, et neid muutujaid klassi koosseisu mittekuuluv kood otse muuta saaks.

Vaatame näitena lihtsat loenduri klassi nimega SimpleCounter. Selle klassi abil on võimalik loendada kasutades Increase() ja Decrease() meetode. Decrease() meetod kannab lisaks hoolt selle eest, et loenduri väärtus ei läheks nullist väiksemaks. Klassis on kasutusel muutuja nimega _counter. Selles muutujas hoitakse loenduri käesolevat väärtust. Teised klassid ega loenduri klassi mittekuuluv kood sellele muutujale otse juurde ei pääse. Kui pääseks, siis saaks nö. "väline kood" anda _counter muutujale suvalisi väärtusi, mida me kontrollida ei saa.

public class SimpleCounter
{
    // Klassi muutuja
    private int _counter;

    // Tagasta loenduri käesolev väärtus
    public int Value
    {
        get { return _counter; }
    }

    // Suurenda loendurit ühe võrra
    public void Increase()
    {
        _counter++;
    }

    // Vähenda loendurit ühe võrra
    public void Decrease()
    {
        if(_counter == 0)
        {
            return;
        }

        _counter--;
    }

    // Lähtesta/nulli loendur
    public void Reset()
    {
        _counter = 0;
    }
}
using System;

namespace OOP
{
    public class SimpleCounter
    {
        // Klassi muutuja
        private int _counter;

        // Tagasta loenduri käesolev väärtus
        public int Value
        {
            get { return _counter; }
        }

        // Suurenda loendurit ühe võrra
        public void Increase()
        {
            _counter++;
        }

        // Vähenda loendurit ühe võrra
        public void Decrease()
        {
            if(_counter == 0)
            {
                return;
            }

            _counter--;
        }

        // Lähtesta/nulli loendur
        public void Reset()
        {
            _counter = 0;
        }
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            var counter = new SimpleCounter();

            // Suurenda loenduri väärtust kahe võrra
            counter.Increase();
            counter.Increase();
            Console.WriteLine(counter.Value);

            // Vähenda loenduri väärtust kahe võrra
            counter.Decrease();
            counter.Decrease();
            Console.WriteLine(counter.Value);

            // Vähenda loenduri väärtust ühe võrra
            counter.Decrease();
            Console.WriteLine(counter.Value);

            // Peale programmi jooksutamist kommenteeri allolev rida välja
            // Visual Studio joonistab sellele punase joone alla
            // counte._counter = -1;
        }
    }
}

Omadused

Omadus (property) on krants, mis saadakse klassi muutuja ja meetodi ristamisel. Omadused näevad koodis kasutamisel välja nagu klassi muutujad, kuid need võivad sisaldada ka koodi. Omadustel on Microsofti programmeerimise maailmas eriline koht. Neid kasutatakse nii töölaua- kui veebirakenduste juures ohtralt. Klassi muutujaid peetakse klassi privaatseks teemaks ja tavaliselt neid teistele klassidele ei näidata.

Omadused koosnevad kahest komponendist: lugemine (get) ja omistamine (set); Neid komponente nimetatakse ka getter ja setter. Omadused, millel puudub omistamise osa, on mõeldud ainult lugemiseks (read-only property). Sarnaselt võime defineerida ka selliseid omadusi, mida ei saa lugeda, kuid millele saab omistada. Vajadus ainult omistatavate omaduste järgi on haruldane ja enamasti on tegemist erandlike tehniliste olukordadega.

Omadustega parema tutvuse tegemiseks kirjutame Person-klassi selliselt ümber, et ees- ja perekonnanime omistamisel eemaldatakse nime ümbert tühikud.

public class Person
{
    private string _firstName; // muutuja eesnime jaoks
    private string _lastName;  // muutuja perekonnanime jaoks

    public string FirstName 
    {        
        get // getter
        {
            return _firstName;
        } 
        set // setter
        {
            // eemalda tühikud
            _firstName = value.Trim();
        }
    }

    public string LastName 
    {
        get // getter
        {
            return _lastName;
        } 
        set // setter
        {
            // eemalda tühikud
            _lastName = value.Trim();
        }
    }
}

var person = new Person();       // loo uus Person-tüüpi objekt
person.FirstName = " Jaan";      // omista eesnimeks " Jaan"
person.LastName = " Jalgratas "; // omista perenimkes " Jalgratas "
using System;

namespace OOP
{
    public class Person
    {
        private string _firstName; // muutuja eesnime jaoks
        private string _lastName;  // muutuja perekonnanime jaoks

        public string FirstName 
        {        
            get // getter
            {
                return _firstName;
            } 
            set // setter
            {
                // eemalda tühikud
                _firstName = value.Trim();
            }
        }

        public string LastName 
        {
            get // getter
            {
                return _lastName;
            } 
            set // setter
            {
                // eemalda tühikud
                _lastName = value.Trim();
            }
        }
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            var person = new Person();
            person.FirstName = " Jaan ";
            person.LastName = " Jalgratas";

            // Kirjutame nime püstkriipsude vahele, et näha kas tühikud on alles
            Console.WriteLine("|" + person.FirstName + "|");
            Console.WriteLine("|" + person.LastName + "|");
        }
    }
}

Kui omistame omadusele väärtuse, siis saame setteri sees kasutada spetsiaalset märksõna value. See on muutuja, milles hoitakse omadusele omistatud väärtust.

Vaatame nüüd omadust, millel puudub omistamise osa ehk setter. Person-klassile saame ühe sellise omaduse lisada näiteks isiku täisnime jaoks. Olgu selle omaduse nimeks FullName.

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public string FullName
    {
        get
        {
            return FirstName + " " + LastName;
        }
    }
}
using System;

namespace OOP
{
    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }

        public string FullName
        {
            get
            {
                return FirstName + " " + LastName;
            }
        }
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            var person = new Person();
            person.FirstName = "Jaan";
            person.LastName = "Jalgratas";

            // Kirjuta välja isiku täisnimi
            Console.WriteLine(person.FullName);
        }
    }
}

Omadus FullName liidab omavahel kokku isiku eesnime ja perekonnanime. Omadused, mis tagastavad arvutatud väärtusi, ei vaja tavaliselt omistamise osa. Selliste omaduste kohta kasutatakse terminit read-only property.

Automaatsed omadused

Tähelepanelikul õpilasel tekkisk kindlasti juba küsimus: miks me mõnikord kirjutame omadused välja lühidalt ja miks me mõnikord kasutame eraldi muutujat omaduse väärtuse hoidmiseks?

Varasemates C# versioonides olid ainult abimuutujaga omadused. Vaatame Person-klassi, kus on kolm omadust ja kasutusel on abimuutujad.

public class Person
{
    private string _firstName; // Abimuutuja eesnime jaoks
    private string _lastName;  // Abimuutuja perekonnanime jaoks
    private string _address;   // Abimuutuja aadressi jaoks

    public string FirstName 
    { 
        get { return _firstName; }; 
        set { _firstName = value; }; 
    }

    public string LastName 
    { 
        get { return _lastName; } 
        set { _lastName = value; }
    }

    public string Address 
    { 
        get { return _address_; } 
        set { _address = value; }
    }
}

Selliseid klasse, kus omadused otseselt midagi ei tee, kuid kus neid on vaja täna arendusvahenditele, on pea igas koodibaasis päris palju. Tegemist on tavaliselt klassidega, mida nimetatakse kas mudeliteks või DTO-deks (Data Transfer Object). Need klassid kannavad endaga vähesel määral toiminguid ja loogikat, kui üldse. Kuid need klassid võivad sisaldada ohtralt omadusi. Siit tekkiski inseneridel idee - kui omadus midagi ei tee peale väärtuse tagastamise ja vastuvõtmise, siis miks mitte lasta kompilaatoril genereerida selline kood. Tulemuseks olid automaatsed omadused (automatic property).

Automaatne omadus tähendab seda, et eeltoodud koodi loob kompilaator automaatselt - meie ise peame ainult ütlema, millised omadused meil on. Selle jaoks kasutame seda lühemat süntaksit.

public class Person
{
    public string FirstName { get; set; }    
    public string LastName  { get; set; }
    public string Address  { get; set; }
}
using System;

namespace OOP
{
    // Person klass tavaliste omadustega
    public class Person
    {
        private string _firstName; // Abimuutuja eesnime jaoks
        private string _lastName;  // Abimuutuja perekonnanime jaoks
        private string _address;   // Abimuutuja aadressi jaoks

        public string FirstName 
        { 
            get { return _firstName; }; 
            set { _firstName = value; }; 
        }

        public string LastName 
        { 
            get { return _lastName; } 
            set { _lastName = value; }
        }

        public string Address 
        { 
            get { return _address_; } 
            set { _address = value; }
        }
    }

    // Person klass automaatsete omadustega
    public class PersonAutomatic
    {
        public string FirstName { get; set; }    
        public string LastName  { get; set; }
        public string Address  { get; set; }
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            var person1 = new Person();
            person1.FirstName = "Jaan";
            person2.LastName = "Jalgratas";

            var person2 = new PersonAutomatic();
            person2.FirstName = "Jaan";
            person2.LastName = "Jalgratas";

            Console.WriteLine(person1.FirstName + " " + person1.LastName);
            Console.WriteLine(person2.FirstName + " " + person2.LastName);
        }
    }
}

Mõlemad selles jaotuses toodud Person-klassid on tehniliselt üks ja sama asi.

Meetodid

Meetodid on klassi koosseisu kuuluvad funktsioonid. Järgnevas näites on defineeritud klassile SimpleCalculator kolm meetodi: Add(), Divide() ja LogError().

Add() meetodil on parameetrid x ja y. Meetod tagastab täisarvu (x ja y summa). Sarnaselt töötab ka Divide(), kuid see meetod kirjutab välja vea kui jagaja on 0. LogError() meetodil on parameeter message, mis sisaldab ekraanile kirjutatavat veateadet. Tagastatavaks tüübiks on void, mis tähendab, et meetod väärtust ei tagasta.

public class SimpleCalculator
{
    public int Add(int x, int y)
    {
        return x + y;
    }

    public int Divide(int x, int y)
    {
        if(y == 0)
        {
            LogError("Cannot divide by zero");
            return int.MinValue;
        }

        return x / y;
    }

    private void LogError(string message)
    {
        Console.WriteLine(message);
    }
}
using System;

namespace OOP
{
    public class SimpleCalculator
    {
        public int Add(int x, int y)
        {
            return x + y;
        }

        public int Divide(int x, int y)
        {
            if(y == 0)
            {
                LogError("Cannot divide by zero");
                return int.MinValue;
            }

            return x / y;
        }

        private void LogError(string message)
        {
            Console.WriteLine(message);
        }
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            var calculator = new SimpleCalculator();

            Console.Write("2 + 3 = ");
            Console.WriteLine(calculator.Add(2, 3));

            Console.Write("8 / 4 = ");
            Console.WriteLine(calculator.Divide(8, 4));

            // Proovime nulliga jagamist
            Console.Write(calculator.Divide(6, 0));
        }
    }
}

Klassil võib olla mitu samanimelist meetodi. Erinema peavad need signatuuri (parameetrite poolest). Järgnevas näites on SimpleCalculator klassil kaks Add() meetodi, mille erinevuseks on parameetrite arv.

public class SimpleCalculator
{
    public int Add(int x, int y)
    {
        return x + y;
    }

    public int Add(int x, int y, int z)
    {
        return x + y + z;
    }
}

Sündmused

Klassidele saab defineerida sündmusi, mida teised klassid jälgivad. Kujutame ette, et meil on klass, mis loeb uudisvooge ja me tahame teada kui on saabunud uus uudis. Üks võimalus oleks käia ja küsida selle klassi käest iga natukese aja tagant, et on midagi uut või ei. Kuid see tähendaks, et programm teeb rohkem tööd kui oleks hädavajalik - me peame ju hoidma üleval tsüklit, mis käib uudiste klassi käest küsimas. Palju mugavam oleks niipidi, et uudiste klass ütleb meile ise kui uus uudis saabus. See on umbes nagu kana ja munaga. Kui on muna vaja, siis me ei käi iga 10 minuti tagant kana uurimas. Kana tulistab ise munemissündmuse meile välja kaagutamise teel ja meie ülesanne on muna üles leida.

Koodis võiks seda väljendada alltoodud kujul. Teeme klassi nimega Hen. Sellel on sündmus nimega EggReady ja meetod Start(), mis paneb kana iga kahe sekundi järelt munema. Hen-klassi proovimiseks teeme Hen-tüüpi objekti ning omistame EggReady-sündmusele aktsiooni (action), mis kirjutab ekraanile koos kellaajaga, et muna saabus. Seejärel kutsume Start() meetodi, mis paneb kana munema. Kõlab keeruliselt algul, kuid vaatame koodi.

public class Hen
{    
    public delegate void NewEggDelegate();  // delegaat, mis defineerib sündmuse
    public event NewEggDelegate EggReady; // sündmus ise

    public void Start()
    {
        for(var i = 0; i < 5; i++)
        {
            Thread.Sleep(2000); // Oota kaks sekundit
            EggReady.Invoke();  // Kutsu EggReady sündmus esile
        }
    }
}

var hen = new Hen(); // Loo uus Hen-tüüpi objekt

// Kui EggReady sündmust kutsuti, siis kirjuta välja kellaaeg ja Egg!
hen.EggReady += () => 
{
    Console.Write(DateTime.Now.ToLongTimeString());
    Console.WriteLine(": Egg!");
};

hen.Start(); // Käivita kana
using System;
using System.Threading;

namespace OOP
{
    public class Hen
    {    
        public delegate void NewEggDelegate();
        public event NewEggDelegate EggReady;

        public void Start()
        {
            for(var i = 0; i < 5; i++)
            {
                Thread.Sleep(2000);
                EggReady.Invoke();
            }
        }
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            var hen = new Hen();

            hen.EggReady += () => 
            {
                Console.Write(DateTime.Now.ToLongTimeString());
                Console.WriteLine(": Egg!");
            };

            hen.Start();
        }
    }
}

Vaatleme Hen-klassi lähemalt. Klassi alguses on defineeritud delegaat (delegate) nimega NewEggDelegate. Tagastatavaks väärtuseks on void ja parameetrid puuduvad. Delegaat on sündmuse definitsioon. Delegaat määra ära, millised parameetrid on sündmusel ja millise väärtuse sündmus tagastab. Parameetritega sündmusi vaatame järgmises peatükis.

Järgmisel real on EggReady sündmuse enda definitsioon. Pane tähele, et sündmuse tüübiks on NewEggDelegate ja sündmuse nimeks, mida mujal koodis näeme ja kasutame, on EggReady. Kui oleme loonud uue Hen-tüüpi objekti, siis omistame EggReady sündmusele tükikese koodi, mis annab ekraanil märku kui muna tuli.

Kui eeltoodud näide käivitada, siis saame tulemuseks sarnase väljundi:

12:41:02: Egg!
12:41:04: Egg!
12:41:06: Egg!
12:41:08: Egg!
12:41:10: Egg!

Parameetritega sündmused

Eelmises näites oli meil munemise sündmus, kuid see poleks piisavalt informatiivne mitme kana korral kui me sooviks kasutada täpselt sama koodi kõikide munevate kanadega.

Järgnev kood kasutab Task-klassi, et panna kaks kana korraga munema. EggReady sündmuse peale käivitub sama kood, mis enne. Console.ReadLine() ootab nupuvajutust, et programmi töö lõpetada. Seda on vaja seepärast, et Task.Start() ei oota kuni selle käivitatud kood lõpetab.

Task.Start(() => 
{
    var hen = new Hen();
    hen.EggReady = () => Console.WriteLine(DateTime.Now + ": Egg!");
    hen.Start();
}
Task.Start(() => 
{
    var hen = new Hen();
    hen.EggReady = () => Console.WriteLine(DateTime.Now + ": Egg!");
    hen.Start();
}

Console.ReadLine();

Eeltoodud koodi jooksutamisel saame järgneva väljundi:

12:41:02: Egg!
12:41:02: Egg!
12:41:04: Egg!
12:41:04: Egg!
12:41:06: Egg!
12:41:06: Egg!
12:41:08: Egg!
12:41:08: Egg!
12:41:10: Egg!
12:41:10: Egg!

Me näeme küll, et kanad on munenud, kuid meil pole mingit infot selle kohta, kumma kanaga tegemist oli. Me saame selle info tuua munemise sündmusesse kui lisame klassile ja delegaadile parameetri kana nime jaoks.

public class Hen
{
    private string _henName;

    public delegate void NewEggDelegate(string henName); 
    public event NewEggDelegate EggReady;

    public Hen(string henName)
    {
        _henName = henName;
    }

    public void Start()
    {
        for (var i = 0; i < 5; i++)
        {
            Thread.Sleep(2000);
            EggReady.Invoke(_henName);
        }
    }
}

var eggReadyHandler = new Hen.NewEggDelegate((henName) =>
{
    var message = DateTime.Now.ToLongTimeString();
    message += ": " + henName + ": Egg!";

    Console.WriteLine(message);
});

Task.Start(() => 
{
    var hen = new Hen("White");
    hen.EggReady += eggReadyHandler;
    hen.Start();
}
Task.Start(() => 
{
    var hen = new Hen("Brown");
    hen.EggReady += eggReadyHandler;
    hen.Start();
}

Console.ReadLine();
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp17
{
    public class Hen
    {
        private string _henName;

        public delegate void NewEggDelegate(string henName);
        public event NewEggDelegate EggReady;

        public Hen(string henName)
        {
            _henName = henName;
        }

        public void Start()
        {
            for (var i = 0; i < 5; i++)
            {
                Thread.Sleep(2000);
                EggReady.Invoke(_henName);
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var eggReadyHandler = new Hen.NewEggDelegate((henName) =>
            {
                var message = DateTime.Now.ToLongTimeString();
                message += ": " + henName + ": Egg!";

                Console.WriteLine(message);
            });

            Task.Run(() =>
            {
                var hen = new Hen("White");
                hen.EggReady += eggReadyHandler;
                hen.Start();
            });

            Task.Run(() =>
            {
                var hen = new Hen("Brown");
                hen.EggReady += eggReadyHandler;
                hen.Start();
            });

            Console.ReadLine();
        }
    }
}

Tulemusena saame efektiivsema väljundi.

08:57:25: Brown: Egg!
08:57:25: White: Egg!
08:57:27: White: Egg!
08:57:27: Brown: Egg!
08:57:29: Brown: Egg!
08:57:29: White: Egg!
08:57:31: White: Egg!
08:57:31: Brown: Egg!
08:57:33: Brown: Egg!
08:57:33: White: Egg!

Klassi konstandid

Konstandid on sarnased klassi muutujatele, kuid erinevad selle poolest, et neil on fikseeritud väärtus, mida muuta ei saa. Konstandid on äärmiselt kasulikud koodi loetavuse huvides. On palju programme, mille lähtekoodis kasutatakse konstante, kuid nende sisu pole alati kerge aimata.

Järgnevas näites sisaldab kood kahte selgusetut konstanti (-20 ja 40). Selliseid konstante nimetatakse ka maagilisteks konstantideks (magic constant).

public class TemperatureSensorReader
{
    public int GetTemperature()
    {
        var temp = ReadSensor();

        if(temp < -20 || temp > 40)
        {
            Alert(temp);
        }

        return temp;
    }

    private void Alert(int temp) 
    {
        // Väljasta hoiatus
    }

    private int ReadSensor()
    {
        // Loe sensori näit
    }
}

Kui nende asemel kasutada klassi konstante, millel on konstandi väärtust kirjeldav nimi, on meil koodi kergem mõista. Järgnevas näites on klassile defineeritud konstandid MinimumTemperature ja MaximumTemperature, mis annavad endistele maagilistele konstantidele konkreetse tähenduse.

public class TemperatureSensorReader
{
    public const int MinimumTemperature = -20;
    public const int MaximumTemperature = 40;

    public int GetTemperature()
    {
        var temp = ReadSensor();

        if(temp < MinimumTemperature || temp > MaximumTemperature)
        {
            Alert(temp);
        }

        return temp;
    }

    private void Alert(int temp) 
    {
        // Väljasta hoiatus
    }

    private int ReadSensor()
    {
        // Loe sensori näit
    }
}

Viited