- Kursmaterial
- Planering
- Arbete
- Kunskapsdokument
- Tutorials
- Andra kurser
- Om Kursolle
Tutorial - 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.
Note
Bilderna i denna tutorial kommer från Xamarin vilket är en tidigare IDE för Mac. Om du kör Visual Studio på Windows eller macOS så kommer det inte se likadant ut, men det bör inte vara några problem att genomföra denna tutorial ändå.
Vissa bilder är utbytta och fler kanske byts ut allt eftersom om det visar sig att det behövs.
2. Skapa klassen
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 diagrams.net. 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 börjar med att skapa ett nytt projekt.
2.3 Skapa klassen Car
Nu är det dags att skapa klassen Car genom att skapa filen Car.cs
.
Nu är det dags att fylla filen Car.cs
med sin första kod. Skriv av koden på bilden.
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.
Lägg märke till att själva programmet skriver vi i filen Program.cs
medan kod som ingår i klassen Car
skrivs i Car.cs
. Du kommer skriva kod i bägge dessa filer under arbetets gång så se till att rätt kod hamnar i rätt fil.
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.
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.
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.
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.
4.1 UML
Nu 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.
När vi kör vårt program ser utskriften ut så här.
5.1 UML
Nu 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.
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.
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:
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;
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
Nu 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.
Ä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;
- 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.
- 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.
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.
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()
.
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.
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.
Perfekt!
7.1 UML
Dags 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.
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.
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.