Tutorial: OOP C# Grund

OOP C# Grund

1. Introduktion

I denna tutorial lär vi oss att använda klasser och objekt samt hur vi bygger en enkel applikation kring dessa. I tutorialen kommer det finnas en mängd kod som du kan följa med i och även skriva av för att bygga din egen applikation. Arbetar du igenom denna tutorial så får du med dig alla de grunder som du behöver för att kunna bygga egna objektorienterade applikationer. Detta är en praktisk övning, det teoretiska delarna behöver du gå igenom först för att till fullo ta till dig denna övning.

2. Skapa klassen

UML Vi börjar med att kolla på hur vår klass skall se ut, för att visa hur klasserna skall se ut använder vi oss av UML (Unified Modeling Language). All UML i denna tutorial har ritats upp i webbapplikationen draw.io. Jag rekommenderar att du lär dig skapa dina egna klasser i den applikationen, du kommer ändå behöva göra detta senare.

2.1 UML

Klassmodellen i UML består av tre rader

  • den första raden innehåller klassens namn
  • den andra raden innehåller klassens medlemsvariabler. Den består av tre delar;
    • medlemsvariabelns synlighet
      • (+) innebär public
      • (-) innebär private
      • I senare moment kommer vi kika på andra synligheter, framförallt (#) protected
    • medlemsvariabelns namn
    • medlemsvariabelns typ
  • den tredje raden innehåller klassens metoder, här beskrivs metodernas namn, argument och returvärde. Den är just nu tom, men vi kommer lägga till metoder senare.

2.2 Skapa applikationen CarDemo

Vi skapar ett nytt projekt i den utvecklingsmijö vi föredrar. Jag kommer i detta projekt använda mig av IDE'n Xamarin.

Skapa projektet
Bild 2 - Skapa ett konsollprojekt, i Xamarin ligger det som ett av .NET-applikationerna.
Skapa projektet
Bild 3 - Döp projektet till något lämpligt och lägg det någonstans där det säkerhetskopieras.
Skapa projektet
Bild 4 - Filen Program.cs skapas automatisk. Det är här programmet startar vid exekvering.

2.3 Skapa klassen Car

Nu är det dags att skapa klassen Car, börja med att skapa filen Car.cs och skriv in koden som du ser i bild 5.

Skapa ny fil
Bild 5 - Skapa en ny fil till projektet genom att högerklicka på den CarDemo som har en blå icon framför sig och välj Add och sedan New File....
Klassen Car
Bild 6 - Filen Car.cs som innehåller den första versionen av vår klass Car.

Du kan testa att köra din applikation, du kommer inte kunna skapa någon bil men då kan iaf se att koden är godkänd, att applikationen kompileras och körs. Men vad skrivs ut?

Tycker du någon gång att mina bilder är för små och svåra att läsa så högerklicka på dem och välj att öppna dem i en ny flik.

3. Skapa objekt

Vi skall nu skapa ett objekt av typen Car och ge den några värden innan vi testar att skriva ut objektet.

Att skapa ett objekt av en klass kallas att man skapar en instans av klassen. Tänk dig klassen som en mall där alla objekt utgår ifrån, klassen talar om vilka medlemsvariabler och medlemsmetoder som objektet skall innehålla men klassen styr inte vilka värden som skall lagras. Alla bilar lagrar samma typ av information men varje unik bil lagrar unik information om t.ex. regnr och årsmodell.

Skapa ett objekt
Bild 7 - Objektet c1 skapat, lägg märke till de två olika sätten som jag skriver ut samma sak, det andra sättet är det jag kommer använda senare.

För att skapa ett objekt så måste ett nytt objekt skapas (new Car()) och vi måste också skapa en variabel som pekar på detta objekt, denna variabel har vi här döpt till c1.

Utskrift
Bild 8 - Utskriften av vårt objekts värden.

4. Skapa funktion för utskrift

Eftersom tanken är att kunna skapa många bilar i vår applikation så skapar vi metoden toString() i klassen Car som sedan kan anropas från varje objekt, detta är en medlemsmetod. toString() är en metod som finns i ”alla” objekt genom arv, men vi kan skapa en egen toString()-metod som skriver ut informationen om ett objekt på det sätt som vi vill.

Om vi skriver ut den ärvda toString() så kommer det se ut så här. Koden skall läggas i Program.cs.

Kodexempel
Bild 9 - Kodexempel för den ärvda metoden ToString().
Utskrift
Bild 10 - Utskriften blir då på detta sätt vilket är helt oanvändbart för oss.

Vi bygger vår egna metod ToString() som inte har några inparametrar men returnerar en String. Här skulle man kunna välja att skriva ut texten inne i metoden men jag väljer att returnera resultatet och sedan skriva ut det inne i programmet när jag tycker det är lämpligt.

Utskrift
Bild 11 - Metoden ToString(), lägg märke till hur jag dokumenterar metoden. I flera andra språk finns det ett standardiserat sätt att dokumentera men detta saknas i C#. Du får göra på ditt egna sätt men glöm inte bort att få med det viktiga och att vara konsekvent.
Utskrift
Bild 12 - Utskriften blir direkt mycket mer användbar.

Om du kikar väldigt noga i metoddeklarationen ovan så ser du nyckelordet override som används för att tala om att min metod ToString skall köra över den funktionen ToString som klassen Car har fått genom arv.

Utskrift
Bild 13 - Informationsrutan om override.

4.1 UML

UMLNu har vi lagt till en metod i vår klass och det är dags att uppdatera vårt UML-diagram. Vi lägger till metoden i den nedersta raden och anger tre saker;

  • medlemsmetoden synlighet
    • (+) innebär public
    • (-) innebär private
  • medlemsmetodens namn, inom parentes skrivs också eventuella inparameterar till metoden.
  • medlemsmetodens returtyp

5. Skapa konstruktorer

En konstruktor är en speciell typ av metod som har samma namn som klassen och som anropas när vi skapar en instans av klassen. En konstruktor saknar returvärde. Konstruktorn används för att initiera medlemsvariabler i klassen.

Det går att skapa flera konstruktorer, där varje konstruktor skiljer sig med avseende på antal parametrar eller parametertyper.

Om inga konstruktorer definieras kommer kompilatorn generera en konstruktor som inte tar några argument. Denna konstruktor kallas för en förvald konstruktor (default constructor, kallas därför också för defaultkonstruktor ibland). När du skapar en konstruktor så faller den förvalda konstruktorn som skapar ett tomt objekt. Därför måste vi, om vi fortfarande vill ha en sådan, skapa den själva.
I vårt exempel så skapades en tom konstruktor automatiskt när vi skapade vår klass.

Lägg extra märke till ”this” som hela tiden pekar på medlemsvariabeln i ett objekt. Genom att använda ”this” så kan vi i detta läge använda samma variabelnamn men samtidigt hålla isär dem från vilken som tillhör objektet och vilken som är en inparameter i konstruktorn eller metoden. Att hålla nere antalet variabler, och ge dem beskrivande och vettiga namn, underlättar vårt arbete i större projekt.

Kodexempel
Bild 15 - Koden för konstruktorn.
Kodexempel
Bild 16 - Hur vi använder den nya konstruktorn.

När vi kör vårt program ser utskriften ut så här.

Utskrift
Bild 17 - Utskriften.

5.1 UML

UMLNu har vi lagt till två konstruktorer i vår klass och det är dags att uppdatera vårt UML-diagram. Vi lägger till konstruktorerna först i den delen där vi listar metoderna och anger tre saker;

  • konstruktorns synlighet är alltid public (+)
  • konstruktorns namn är alltid samma som klassen
  • eventuella inparametrar anges inom parentesen, inget returvärde kommer från en konstruktor

6. Get- och set- metoder

Synlighet för medlemsvariabler, och i viss mån medlemsmetoder, styr vi med nyckelorden public eller private. För att skydda innehållet i våra objekt så skall vi nu ändra medlemsvariablernas synlighet från public till private.

Kodexempel
Bild 19 - Ändra medlemsvariablernas synlighet från public till private.

Metoderna skall fortfarande vara publika. Eventuella metoder som inte kommer användas utanför objektet skulle man kunna låta vara privata, just nu har vi ingen sådan metod som bara använd inom objektet men det kanske kommer senare.

Nu kan du inte längre köra programmet utan kommer få felmeddelande på de rader som kräver att medlemsvariablernas synlighet är public.

Error
Bild 20 - Felmeddelande eftersom medlemmarna inte längre är synliga.

Felmeddelandet beror på att objekten som är skapade i applikationen inte längre får prata med medlemsvariablerna i objektet c1. För att komma runt detta måste vi nu hitta ett annat sätt att kommunicera med objektets medlemsvariabler. Här brukar man skapa get- och set-metoder, getters och setters. För vår medlemsvariabel regNr så hade vi vanligtvis skapat metoderna getRegNr() som hade hämtat registreringsnummer för objektet och returnerat detta som en sträng samt metoden setRegNr(String: regNr) där vi hade matat in ett registreringsnummer som då hade lagrats i objektet.

Detta är något som är väldigt vanligt och som görs till de flesta medlemsvariablerna i våra program. C# har löst detta på ett eget sätt som de kallar Properties vilket innebär att vi på ett kortare sätt skriver dessa metoder (getters och setters). I vår applikation skriver vi så här:

Kodexempel
Bild 21 - Properties.

För att kunna prata med våra Properties så är det nu RegNr som går att nå och vi kan i vår kod använda den som om det vore en variabel. Vi skriver om koden så här;

Kodexempel
Bild 22 - Kommunicera med properties.

Nu fungerar det som det skall när jag kör applikationen.

Properties har funnits i C# relativt länge men i både version 6 och 7 av C# så har det tillkommit två nya sätt att skapa properties. Läs mer om det här om du är intresserad.

6.2 Namngivning med properties

Hur vi skall skriva namn på klasser, variabler och metoder är alltid en diskussion bland utvecklare. Vissa tycker att det inte är så viktigt men själv kan jag tycka att det är extremt viktigt i början av er karriär som utvecklare samt den dagen då ni sitter i stora projekt och inte utvecklar all kod själv. En metod känner du enklast igen genom att den har parenteser efter namnet. Namngivning av metoder i C# är värt ett kapitel i sig. I vissa språk skall en metod ha en gemen begynnelsebokstav medan i andra språk skall den vara versal. I Java jobbar vi uteslutande med gemen, i C# så är namngivningsstandarden att den skall vara versal. Detta gör det lurigt för utvecklare som utvecklar i olika språk, vilket de flesta i någon form i eller mellan projekt gör. Detta kommer vi runt genom att vissa saker inte är huggna i sten. Jag använder i stort sett samma namngivning oavsett om det är C#, Java, PHP eller JavaScript. Det är inte fel, men det är viktigt att utvecklarna inom ett gemensamt projekt är överens om hur strukturen skall vara.

En variabel, en klass och nu en property måste namnges på ett sätt så att det är enkelt att veta vad som avses när vi skriver kod. Klasser är vi överens om skall ha en versal begynnelsebokstav. När det gäller medlemsvariabler och property kommer vi tvingas ta ett beslut.

6.2.1 Alternativen

Medlemsvariabel Property/metod Not
regNr getRegNr() / setRegNr() (tex. java)
GetRegNr() / SetRegNr() (tex. C#)
Om property ej finns i språket är det vanligt att skriva på detta sättet.
regNr RegNr Versal begynnelsebokstav för property för att skilja dem åt.
_regNr regNr Detta sätt gör att propertyn har det namn som vi normalt sett sätter på en medlemsvariabel. Medlemsvariabeln används ändå bara inom klassen och genom dess property.

Författaren till boken som används i kursen använder variant nr 2 och så kommer jag också göra i denna tutorial.

Innan du skall göra ditt slutprojekt i kursen skall du skapa din namngivningsstandard i projektet. Det är lika bra att träna och hitta den standard som du gillar bäst redan nu.

6.3 UML

UMLNu stöter vi på problem när vi skall skapa ett UML-diagram för vår klass då det inte finns någon standard för hur Properties skall markeras. Om vi läser på nätet så hittar vi flera olika exempel. Det viktigaste tycker jag är att vi är tydliga och konsekventa. Jag väljer att följa den struktur som senare kommer användas i boken som vi jobbar med i denna kurs.

För att följa bokens struktur så skriver vi våra properties bland metoderna och anger bara om de är set och/eller get.

Glöm inte bort att också föra in den ändrade synligheten för medlemsvariablerna.

7. Validera data

Om vi anger ett ologiskt värde till någon av våra medlemsvariabler så skapar vi oss problem. Vi tänker oss att man anger årsmodellen som 12 för en bil, hur skall du programmet reagera på detta? Ännu värre är om man anger ett ologiskt värde som 1450 eller -800. Detta borde vi ta hand om på något sätt. Vi har ju skapat en property som heter Year, den kan vi bygga vidare på och lägga in lite logik i den.

Kodexempel
Bild 24 - Skriver om property för year.
Kodexempel
Bild 25 - Testar vad som händer om vi matar in ett värde vi inte vill ha.
Utskrift
Bild 26 - Utskriften.

Är det bättre med -1 än -800 eller 1450? -1 är ett tal som vi kan tala om är felaktigt och ta om hand på annat sätt. Du som programmerare, eller beställaren, bestämmer vilka regler som skall gälla i applikationen. Det viktigaste är att veta hur man skall ta hand om felaktigt data.

Vi vill meddela användaren att alla årtal som lagras som -1 är felaktig och därför måste vi skriva om vår kod. Vi har två möjligheter att göra detta;

  1. Vi skriver om metoden ToString() så att applikationen kollar om year = -1, om det är sant så skriver vi ut något om felaktigt årtal.
  2. Vi skriver om propertyn Year så att den returnerar ett årtal som en String eller ett meddelande som en String, istället för att den som idag returnerar en int.

Hade vi byggt detta i Java så hade jag säkerligen gjort den andra lösningen, samma med PHP, men nu så blir vår property begränsningen. Jag kan inte låta datatypen vara int när jag prata med propertyn och få en String tillbaka när jag kallar på den. En lösning kan då vara att göra om year till en String och lagra årtalet eller -1 som en String, vilket fungerar men det innebär att vi hela tiden behöver omvandla mellan String och int för att göra beräkningar och jämförelser. Då lutar det mer åt att jag använder mig av den första lösningen, gör om i utskriftsmetoden. Men det innebär också att jag inte alltid har rätt formatering av min property av Year. Ett tredje sätt innebär att propertyn Year bara får hantera set och så skapar jag en get-metod (getter) som tar hand om formateringen som jag vill ha den vilket r fördelen, nackdelen är att jag inte får den konsekventa kod jag vill eftersträva. En kompromiss är att jag låter propertyn för get och set finnas kvar och skapar en metod som jag döper till YearToString() som gör just vad den heter.

Inte något enkelt val att göra, här ser vi tydligt olikheterna mellan olika programmeringsspråk, men jag tror vi kör på den sista varianten tills vidare.

Kodexempel
Bild 27 - Skapar metoden YearToString().
Kodexempel
Bild 28 - Anropar den nya metoden ifrån metoden ToString().
Utskrift
Bild 29 - Testar den nya utskriften så att den fungerar som det är tänkt.

När vi nu har skapat våra properties så är det lika bra att anropa dessa från konstruktorn som tar argument. Defaultkonstruktorn låter vi vara, den kan få skapa objekt helt utan kontroll (här skulle vi kunna sätta kod i defaultkonstruktorn så att alla nya objekt skapas med förvalda värden).

Huvudanledningen till att vi anropar våra properties från konstruktorn är att vi där har skapat viss logik och kanske skapar mer logik i framtiden. Som vårt program fungerar nu så kan vi inte uppdatera en bil med ett felaktigt årtal, men vi kan skapa det med ett felaktigt årtal. Eftersom vi redan har byggt logiken vore det ju dumt att inte använda den. Bygg om konstruktorn enligt följande.

Kodexempel
Bild 30 - Properties anropas från konstruktorn.

När vi har ändrat konstruktorns anrop till set-properties så bör vi göra samma sak med medlemsvariablernas get-properties. Eftersom vi inte har så många metoder för tillfället så skall detta endast göras i metoden ToString().

Kodexempel
Bild 31 - Properties anropas från metoden ToString().

Eftersom vi redan har byggt logik i metoden YearToString() så känns behöver vi göra samma sak med propertien ForSale så att den ger oss det värde som vi vill ha. Vi bygger en metod för det i samma stil.

Kodexempel
Bild 32 - Metoden ForSaleToString()...
Kodexempel
Bild 33 - ... och omskrivning av ToString().

Som du ser så innehåller metoden ToString() mycket mindre kod nu. Eftersom jag inte bygger upp någon sträng så behöver jag ingen variabel för detta utan kan direkt returnera den sträng som kommer från metoden String.Format(). Skapa inte fler variabler än nödvändigt, men samtidigt skall du bygga den kod som du förstår och känner dig trygg med.

Testa nu så att vi fortfarande har en fungerande applikation, att göra den snyggare är inte mycket värt om den inte fungerar.

Utskrift
Bild 34 - Testar applikationen så att den fungerar som det är tänkt.

Perfekt!

7.1 UML

UMLDags att uppdatera UML-diagrammet igen. Vi har lagt till två metoder, YearToString() och ForSaleToString(), men inte förändrat någon synlighet i övrigt. Att innehållet i metoderna förändrats kommer inte synas i UML-diagrammet, vad metoderna utför får vi skriva i metodens dokumentation. Det som anges i våra UML-diagram är ju vad metoden heter, vilka parametrar som skall skickas in och vad som returneras och vilken datatyp returvärdet har. Det är oftast det som någon annan utvecklare är intresserad av.

8. Användarens inmatning

Än då länge är vår applikation inte så bra, all information om bilar som är inmatade är ju redan hårdkodade. Nu skall vi testa att låta användaren skapa en egen bil.

Kodexempel
Bild 36 - Testar applikationen så att den fungerar som det är tänkt.

Vad behöver förklaras från dessa rader med kod?
Fem stycken inmatningar görs, de fyra första läser jag in med Console.ReadLine() eftersom jag vill ta emot en text. De tre första inmatningar är texter som lagras som sådana, den fjärde görs om till en int mha Convert.ToInt16(). 16 i detta fallet står för lagringsstorleken, i bitar, på den int som används. Det innebär att värdet på en variabel med Int16 kan vara mellan -32768 till +32767 vilket räcker för att lagra vår årtal. Int32 är ett alternativ och då kan vi lagra tal i intervallet ±2.1 miljarder. Int32 är samma som den enkla datatypen int som vi deklarerar year med. Vi kan dock alltid lagra ett mindre tal Int16 i en större variabel Int32/int utan att förändra värdet.

Efter det lilla sidospåret så har vi inmatningen av J/N för att avgöra om en bil är till salu eller inte. Jag läser in endast ett tecken med Console.Read() och konverterar detta till en char. Denna char använder jag sedan metoden ToUpper() för att göra om till versaler. Jämförelsen sker sedan med 'J' för att ge bilen värdet att den är till salu. Alla andra svar, bokstäver som siffror och tecken, låter variabeln forSale ha kvar värdet false vilket innebär att bilen inte är till salu. Här skulle jag kunna kontrollera att bara J eller N kan läsas in innan jag går vidare och skapar bilen. Men det är inte här vi skall lägga fokus i detta arbete. Gör denna test på egen hand om du vill.

Innan vi är klara så testar vi applikationen så att den fungerar som vi vill.

Utskrift
Bild 37 - Inmatning och utskrift via konsollen.

Det ser bra ut! Där ser du också att jag testar att mata in ett gement j för att se att det fungerar.

9. Hela applikationens kod

9.1 UML

UML

9.2 Program.cs

Kodexempel

9.3 Car.cs

Kodexempel Kodexempel Kodexempel