Klasser og objekter

Klasser og objekter
Mildrid Ljosland og Grethe Sandstrak, Institutt for informatikk og e-læring ved NTNU
Lærestoffet er utviklet for faget IFUD1002 C#.NET
2 Klasser og objekter
Resymé: Denne leksjonen introduserer objekter med deres identitet, tilstand og oppførsel. Deretter ser vi på
hvordan det programmeres med klasser, metoder og referanser. Vi ser også på forskjellen mellom verdityper og
referansetyper, samt gjennomgår ulike måter å overføre argumenter til og fra metoder. Til slutt ser vi litt på
objektorientert design. Leksjonen er knyttet til kapittel 3 i læreboka. Det anbefales å lese leksjonen først, og
deretter læreboka.
Innhold
2.1
2.1.1
2.1.2
2.1.3
2.2
2.2.1
2.2.2
2.2.3
2.2.4
2.2.5
2.2.6
2.3
2.3.1
2.3.2
2.3.3
2.3.4
2.3.5
2.4
2.4.1
2.4.2
2.4.3
2.4.4
2.4.5
2.5
2.5.1
2.5.2
2.5.3
2.5.4
OBJEKTER ............................................................................................................................................. 2
Hva er et objekt? ............................................................................................................................. 2
Objekter og referanser .................................................................................................................... 3
Verdityper og referansetyper........................................................................................................... 4
KLASSER ............................................................................................................................................... 4
Oversikt over mulige medlemmer .................................................................................................... 4
Metoder ........................................................................................................................................... 4
Medlemsvariabler ........................................................................................................................... 5
Konstruktører .................................................................................................................................. 6
Klassemedlemmer ........................................................................................................................... 7
Eksempel ......................................................................................................................................... 8
MER OM KLASSER OG DERES INNHOLD ............................................................................................... 10
Egenskaper .................................................................................................................................... 10
Konstanter ..................................................................................................................................... 11
Strukturer ...................................................................................................................................... 11
Overloading................................................................................................................................... 12
Partielle klasser ............................................................................................................................ 13
ARGUMENTOVERFØRING .................................................................................................................... 14
Innargumenter, utargumenter og kombinerte argumenter ............................................................ 14
Verdioverføring av verdityper ....................................................................................................... 14
Referanseoverføring av verdityper ................................................................................................ 14
Verdioverføring av referansetyper ................................................................................................ 16
Referanseoverføring av referansetyper ......................................................................................... 16
OBJEKTORIENTERT DESIGN ................................................................................................................. 17
Use-case og scenarier ................................................................................................................... 17
UML, klassediagram og sekvensdiagram...................................................................................... 18
Samarbeid mellom objekter og en-del-av-forhold ......................................................................... 19
Litt om bruk av grensesnitt ............................................................................................................ 22
Opphavsrett: Forfatter og Stiftelsen TISIP
Klasser og objekter
side 2 av 22
2.1 Objekter
2.1.1 Hva er et objekt?
Når vi programmerer objektorientert, finner vi de ”tingene” problemet dreier seg om, og
bygger opp programmet rundt dem. Hvis vi skal lage et program som holder orden på data om
ansatte, vil hver ansatt bli et objekt. Objekter har en identitet, en tilstand og en oppførsel. I en
bedrift har vi mange ansatte. Hver av dem er egne individer med sin egen identitet. Identiteten
må være entydig, Pål Hansen på verkstedet må ikke blandes med Pål Hansen i
salgsavdelingen. Hver ansatt har et navn, en fødselsdato, en ansettelsesdato, en stilling, en
lønn osv. – dette er objektets tilstand. Tilstanden kan endres etter hvert som tiden går, for
eksempel kan en ansatt få lønnstillegg. Objektene har også en oppførsel, for eksempel kan
ansatte betjene kunder, drikke kaffe eller skrive timeliste.
Når vi skal programmere, lager vi en klasse der vi lar tilstanden være medlemsvariabler
(kalles også datamedlemmer) og oppførselen være metoder (medlemsfunksjoner). Så kan vi
lage objekter av denne klassen (vi sier at vi instansierer klassen). Hvert objekt får sine
spesielle verdier på medlemsvariablene, mens oppførselen er den samme for alle objektene av
en klasse. (I seinere leksjoner skal vi se på hvordan vi kan lage objekter som oppfører seg litt
forskjellig, men foreløpig må alle oppføre seg likt.) En klasse er altså en beskrivelse av
objekter av en bestemt type. Vi kan se på klassen som en arkitekttegning, mens objektene er
de husene som bygges etter denne tegningen.
Hvert objekt får sin egen identitet, knyttet til plassen i primærlageret der objektet er lagret.
Men akkurat som vi i det daglige foretrekker å omtale de ansatte ved navn, ikke ved
personnummer, foretrekker vi også å omtale objekter med navn, ikke ved lagerplass. Vi
definerer derfor en variabel og lar den inneholde objektets identitet, vi har fått en referanse til
objektet.
La oss se på et enkelt eksempel.
Vi trenger objekter som inneholder datoer. Datoer består som kjent av dag, måned og år, og vi
lager derfor en klasse Dato som inneholder medlemsvariablene dag, måned og år.
Deretter kan vi opprette ulike datoer, for eksempel julaften (24, 12, 2015), Martins
fødselsdato (7, 3, 2009), krigsutbruddet (9, 4, 1945) osv. Dette blir objekter av klassen, og kan
brukes som variabler i programmet vårt.
For å kunne bruke objektene i et program, må vi lage referanser til dem. For eksempel kan vi
bruke referansen julaften om det objektet som har tilstanden (24, 12, 2015). Seinere kan vi
sende en melding til julaften og be objektet endre sin tilstand til (24, 12, 2016).
Hvilken oppførsel er det naturlig å knytte til datoer? Her er noen eksempler: Finne neste dag,
sammenlikne en dato med en annen dato for å finne ut hvilken som kommer først, finne antall
dager mellom to datoer, lage en tekstrepresentasjon av datoen på et bestemt format. Dette
programmeres som metoder, og blir tjenester som klassen tilbyr til omgivelsene. Omgivelsene
kan sende en melding til et objekt og be om at en slik tjeneste utføres, og objektet vet selv
hvordan det skal oppføre seg for å utføre denne tjenesten.
Opphavsrett: Forfatter og Stiftelsen TISIP
Klasser og objekter
side 3 av 22
2.1.2 Objekter og referanser
Objekter kan endre tilstand og vi kan endre hvilken referanse som skal referere til hvilket
objekt. Eksempel: Vi lager de to Dato-objektene (24,12,2015) og (24, 12, 2016). Det første lar
vi julaften referere til, mens nesteJulaften referer til det andre. Se figuren under.
Hvis vi sender melding til julaften om at den skal endre tilstand til (24, 12, 2016), får vi
følgende bilde:
Vi har altså to objekter med samme tilstand men ulik identitet.
Hvis vi i stedet for å endre på tilstanden til julaften, lar julaften referere til det samme
objektet som nesteJulaften refererer til, slik,
vil det ene objektet ikke få noen referanse til seg, mens det andre får to.
Et objekt som ikke har noen referanse til seg, kan ikke brukes mer, og vil bli tatt av
søppelinnsamlingen (garbage collection). Lagerplassen som det opptar, vil da bli frigjort og
kan brukes til andre ting.
Vi kan også ha en referanse som ikke viser til noe objekt. Den vil da inneholde den spesielle
verdien null.
forrige
julaften
(null)
Opphavsrett: Forfatter og Stiftelsen TISIP
Klasser og objekter
side 4 av 22
2.1.3 Verdityper og referansetyper
Datatypene skilles i to store grupper, verditypene og referansetypene. I verditypene lagrer vi
verdiene direkte, for eksempel inneholder en int-variabel et bitmønster som sier hvilket tall
vi skal ha. I en referansevariabel derimot, inneholder variabelen adressen til der verdien er
lagret. Når vi lager et objekt av en klasse må vi alltid ha en referansevariabel til å lagre
adressen. Når vi skriver new, avsettes lagerplass til selve objektet, og i referansen lagres
adressen til denne lagerplassen. Før new er utført, finnes ikke objektet, bare en tom referanse
(null).
Vi har mange datatyper som er innebygd i språket. De enkle typene, int, double, bool, char
og varianter av disse, er verdityper. Av innebygde referansetyper har vi string og object.
En referanse av string-type kan inneholde adressen til en tekststreng, mens en referanse av
object-type kan inneholde adressen til et hvilket som helst objekt.
Vi kommer etter hvert til å lage mange egne datatyper, nemlig klasser. Da får vi altså
referansetyper. Det er også mulig å lage egne verdityper, et eksempel på det er struct (se
punkt 2.3.3).
Når programmet vårt kjører (utføres), tildeles programmet to områder i datamaskinen der
variabler kan lagres. Disse to områdene kalles stakken og heapen. Alle variabler av verditype
lagres på stakken, mens alle variabler av referansetype lagres på heapen. Alle objekter lagres
dermed på heapen, mens referansen til dem (den variabelen som inneholder adressen til
objektet) lagres normalt på stakken. Variabler lagret på heapen må alltid ha (minst) en
referanse til seg for å kunne brukes.
2.2 Klasser
2.2.1 Oversikt over mulige medlemmer
Klasser kan inneholde medlemmer av ulike typer. Generelt deler vi klassens medlemmer i to
grupper, datamedlemmer og funksjonsmedlemmer. Datamedlemmene deles igjen i tre
grupper, variabler, konstanter og hendelser. Funksjonsmedlemmene deles i metoder,
egenskaper, konstruktører, destruktører, operatorer og indekserere.
De mest grunnleggende av disse, variabler, konstanter, metoder, egenskaper og konstruktører,
behandles i denne leksjonen. Noen av de andre, for eksempel hendelser, kommer vi tilbake til
senere i kurset.
2.2.2 Metoder
En metode er en beskrivelse av hvordan en bestemt oppgave skal utføres (i andre
programmeringsspråk kan ordene funksjon eller prosedyre brukes om det samme). Vi kan be
om at denne oppgaven skal gjøres, det kalles et metodekall. Vanligvis trenger vi å sende med
noen opplysninger for at metoden skal kunne gjøre jobben sin, dette kalles argumentene til
metoden. Og vi får ofte tilbake en verdi som resultat av metodekallet, dette er returverdien.
Eksempel: Vi skal lage en metode som beregner volumet av en kloss. For å kunne gjøre
jobben, må metoden vite lengden, bredden og høyden på klossen. Vi kan for eksempel skrive
double volum = beregnVolum(2, 4, 8); Da vil metoden regne ut volumet ut fra
argumentene 2, 4 og 8. Returverdien blir 64, altså 2 * 4* 8, som i vårt eksempel lagres i
variabelen volum.
Opphavsrett: Forfatter og Stiftelsen TISIP
Klasser og objekter
side 5 av 22
En metode består av et metodehode og et metodeinnhold. Metodehodet forteller navnet på
metoden, hvilken datatype returverdien er, hvor mange argumenter den krever, og hvilken
type og rekkefølge disse argumentene har. Metodeinnholdet er de programsetningene som
skal utføres under metodekallet.
Vi kan definere beregnVolum() slik:
double beregnVolum(double lengde, double bredde, double høyde)
{
return lengde * bredde * høyde;
}
Her har vi satt navn på argumentene, nemlig lengde, bredde og høyde. De er blitt til
metodens parametere. Vi bruker betegnelsen argument om det som kommer inn ved
metodekall, og parameter om det navnet vi bruker internt i metoden (læreboka bruker disse to
begrepene litt om hverandre). Argumenter og parametere må listes opp i samme rekkefølge.
Argumentet 8 blir til parameteren høyde siden begge deler står sist i opplistinga. (Unntak: vi
kan spesifisere hvilket argument som skal tilhøre hvilken parameter ved å angi navnet, f.eks.
kan vi skrive høyde: 8, lengde: 2, bredde: 4. Da spiller rekkefølgen ingen rolle.)
Det reserverte ordet return sørger for at metoden avsluttes og en verdi sendes ut som
returverdi. Hvis en metode har en returtype, må vi ha minst en return inni
metodedefinisjonen. Hvis metoden ikke skal returnere noen verdi (den er definert med
returtypen void), behøver vi ikke noen return. Hvis ingen return finnes, avsluttes metoden
når siste setning i den er utført. Hvis vi vil at den skal avsluttes tidligere, kan vi skrive
return; uten noen verdi.
I tillegg til parametre og lokale variabler, kan metoden også bruke objektets
medlemsvariabler. Bruk av medlemsvariabler gjør at vi ofte kan klare oss uten, eller med
svært få, parametre.
2.2.3 Medlemsvariabler
En av de store fordelene med den objektorienterte måten å programmere på, er at vi kan
knytte metodene tett sammen med de dataene de skal jobbe med. Det gjør vi ved å la
objektene ha medlemsvariabler som metodene kan bruke direkte, uten å gå via argumenter.
Hvis vi for eksempel skal be en Dato endre sitt årstall, vet objektet selv hvilket årstall det
inneholder, og derfor også hvilket årstall som skal endres. Får metoden beskjed om hvor mye
årstallet skal endres (via et argument), kan metoden regne ut hva det nye årstallet blir, og
lagre dette i sitt datamedlem.
Det er viktig å unngå feil i programmer. En måte å minske risikoen på, er å sørge for at data
ikke kan endres på ukontrollerte måter. I forbindelse med klasser gjøres dette ved å la
medlemsvariablene være private (nøkkelordet private), noe som innebærer at bare klassen
selv har adgang til dem. I den grad andre deler av programmet har behov for å hente fram
verdien eller endre verdien til en medlemsvariabel, må disse delene gå via såkalte
tilgangsmetoder. Dette er metoder som er definert i klassen, og som har til oppgave å
tilgjengeliggjøre dataene på en kontrollert måte. I dato-eksemplet vårt kan det for eksempel
være behov for å endre verdi på måneden i et objekt. Hvis hvem som helst kunne gjøre det
direkte, kunne vi risikere at det ble puttet inn en ulovlig verdi, for eksempel at måneden ble
Opphavsrett: Forfatter og Stiftelsen TISIP
Klasser og objekter
side 6 av 22
satt til 13. Men ved at klassen selv ordner det, kan den som programmerer klassen sørge for å
legge inn en test som forhindrer slike feil.
Det å la medlemsvariablene være private, kalles å innkapsle dem. Når dataene er innkapslet,
er de beskyttet mot uautorisert bruk. Vi lar alltid dataene være innkapslet, selv om det er
mulig å programmere slik at de ikke er det.
De fleste metoder derimot, er offentlige (public). Det betyr at hvem som helst kan bruke dem.
Det er naturlig siden metoder nettopp har til hensikt å tilby tjenester til utenverdenen. Noen
metoder er imidlertid tenkt som hjelpemetoder (”underleverandører”) til en eller flere andre
metoder, da kan det være aktuelt å ha dem som private metoder.
Tommelfingerregelen er at vi lar alt som ikke utenverdenen trenger å bruke, være privat.
Selv om klassens metoder har tilgang til de private dataene, er det ofte lurt å gå via
tilgangsmetodene likevel. Da slipper vi å passe på at dataene blir brukt riktig alle plasser de
brukes, det gjøres en gang for alle i tilgangsmetodene. Dermed står vi mye friere til å endre på
de private delene. For eksempel kan vi tenke oss at vi finner det mer praktisk å la
månedsnumrene gå fra 0 til 11 i stedet for fra 1 til 12. Brukes månedsnummeret direkte i
mange metoder, må vi gå gjennom alle disse og gjøre de nødvendige endringene. Hvis de
andre metodene derimot kun bruker tilgangsmetodene, trenger vi bare å endre
tilgangsmetodene.
2.2.4 Konstruktører
En konstruktør har til oppgave å lage et nytt objekt. Den har alltid samme navn som klassen.
Når vi skriver Dato dato = new Dato(); bruker vi konstruktøren Dato(). Her sier vi at
dato er en referanse til et objekt av typen Dato. Operatoren new lager et nytt Dato-objekt, og
konstruktøren Dato() forteller hvordan det skal gjøres.
En klasse kan ha mer enn en konstruktør, for eksempel er det naturlig å kunne putte inn dag,
måned og år i en Dato, slik:
Dato julaften = new Dato(24, 12, 2015);
Eller vi kan ønske at en ny dato får samme verdi som en annen:
Dato kopi = new Dato(julaften);
Alle disse konstruktørene må programmers i klassen Dato for å kunne brukes. Vi får altså tre
metoder med samme navn, men med ulik argumentliste, så de blir ”overloaded” (mer om
dette i punkt 2.3.4).
En konstruktør uten argumentliste kalles en standardkonstruktør. Kompilatoren lager
automatisk en enkel standardkonstruktør hvis vi ikke definerer egne konstruktører. Men hvis
vi har laget minst en konstruktør selv, og dette ikke er en standardkonstruktør, vil ikke klassen
få noen standardkonstruktør.
En annen spesiell ting med konstruktører er at de ikke har noen returtype, ikke en gang
returtypen void. I eksemplet i 2.2.6 skal vi se nærmere på hvordan konstruktører kan
programmeres.
Hva er forskjellen på å skrive Dato kopi = new Dato(julaften) og Dato kopi =
julaften? I det første tilfellet lager vi et nytt objekt og gir det samme startverdier på
medlemsvariablene som julaften har. Disse verdiene kan vi seinere endre uten at det har
Opphavsrett: Forfatter og Stiftelsen TISIP
Klasser og objekter
side 7 av 22
noen innvirkning på verdiene som er lagret i julaften. I det andre tilfellet får vi to navn
(referanser) på samme objekt. Hvis vi endrer verdiene i kopi, vil verdiene også endres i
julaften.
Motsatsen til en konstruktør er en destruktør (finalizer). Den har som oppgave å tilintetgjøre
et objekt. Destruktører kan hjelpe oss til å unngå at ressurser blir brukt opp fordi de er knyttet
til objekter som ikke lenger er i bruk.
2.2.5 Klassemedlemmer
Noen ganger er det mer naturlig å knytte en metode til en hel klasse, ikke til bestemte objekter
av klassen. Da har vi det som kalles en klassemetode (statisk metode) (i motsetning til en
vanlig metode som i denne sammenhengen kalles en objektmetode). Det er aktuelt når
metoden ikke bruker data knyttet til objektene, men bare tilbyr en tjeneste som jobber mot
data som medsendes metoden som argumenter. Når vi bruker en slik metode, angir vi
klassenavnet, ikke navnet på et objekt. Klassen Math er et godt eksempel på dette. Vi kan for
eksempel skrive Math.Sqrt(5.0) for å få beregnet kvadratrota av 5.0. Det kan vi gjøre fordi
Sqrt() er definert som en klassemetode. Hadde den ikke vært det, måtte vi først ha laget oss
et objekt, Math m = new Math(), og deretter laget metodekallet m.Sqrt(5.0). (Men siden
Math ikke har noen objektmetoder, er det aldri behov for å instansiere klassen. De som har
laget C# har derfor valgt å la en slik instansiering gi kompileringsfeil.)
Vi kan også ha klassevariabler. Vi vet at for vanlige medlemsvariabler (objektvariabler) får vi
en variabel for hvert objekt vi lager. Disse kan endres uavhengig av hverandre. En
klassevariabel finnes det derimot en og bare en utgave av, uansett hvor mange objekter vi
lager (også hvis vi ikke lager noen objekter av klassen). Og eventuelle endringer gjort av ett
objekt får virkning for alle de andre objektene også. Men oftere vil det være aktuelt å ha
klassekonstanter. Hvis en verdi ikke endrer seg, er det sjelden noen grunn til at hvert objekt
skal ha sin egen utgave av den.
En tredje variant er en klassekonstruktør. Det er en konstruktør som utføres bare en gang for
klassen, ikke en gang for hvert objekt som lages. Dette kan brukes hvis vi trenger å initiere et
eller annet før det lages objekter av klassen.
Nøkkelordet static brukes for å fortelle at vi har et klassemedlem, const at vi har en
klassekonstant. Du har allerede brukt klassemetoden Main(). En klassemetode kan ikke kalle
objektmetoder direkte siden objektmetoder krever et objekt. Derfor vil du få feilmelding hvis
Opphavsrett: Forfatter og Stiftelsen TISIP
Klasser og objekter
side 8 av 22
du i Main() prøver å bruke en objektmetode uten å knytte den til et objekt. Det samme gjelder
bruk av objektvariabler.
2.2.6 Eksempel
Vi skal lage en klasse som skal representere sirkler. En sirkel har en radius. Radiusen må vi
kunne sette og hente fram igjen. Når vi vet radiusen, kan vi beregne areal og omkrets. Dermed
får vi en medlemsvariabel, radius, og fire metoder: settRadius(), finnRadius(),
beregnAreal() og beregnOmkrets().
Klassen kan se slik ut:
public class Sirkel
{
private int radius;
// Konstruktører
public Sirkel()
{
// Tom, radius settes automatisk lik 0
}
public Sirkel(int radius)
{
this.radius = radius;
}
public Sirkel(Sirkel original)
{
radius = original.finnRadius();
}
// Tilgangsmetoder
public int finnRadius()
{
return radius;
}
public void settRadius(int radius)
{
this.radius = radius;
}
// Andre metoder
public double beregnAreal()
{
return Math.PI * finnRadius() * finnRadius();
}
public double beregnOmkrets()
{
return 2 * Math.PI * finnRadius();
}
} // slutt klasse Sirkel
Klassen er definert som offentlig (public). Det gjør at alle kan bruke objekter av klassen.
Først kommer den private medlemsvariabelen radius. For enkelhets skyld har vi latt den
være et heltall, vi kunne også latt den være et desimaltall.
Opphavsrett: Forfatter og Stiftelsen TISIP
Klasser og objekter
side 9 av 22
Deretter har vi tre offentlige konstruktører. Den første lager en sirkel med radius 0 og blir
standardkonstruktør. Den kan være kjekk å ha hvis vi trenger et Sirkel-objekt før vi vet hvor
stor sirkelen skal være. Da kan vi senere endre radiusen ved hjelp av tilgangsmetoden
settRadius(). Selv om den er tom (inneholder ingen setninger), må vi ha den med for at
klassen skal få en standardkonstruktør (siden vi har andre konstruktører også).
Hvis vi vet hvor stor sirkel vi skal lage, bruker vi den andre konstruktøren. Her sender vi med
beskjed om hvor stor radiusen skal være. Legg merke til at parameteren radius har samme
navn som medlemsvariabelen radius, men det er to forskjellige variabler! For å skille dem
fra hverandre, kan vi bruke det reserverte ordet this. this er navnet på det objektet vi holder
på med akkurat nå, så this.radius betyr medlemsvariabelen radius. Vi setter altså
medlemsvariabelen radius lik den radius vi får inn via parameteren.
Det er nemlig slik at hvis en parameter (eller lokal variabel) har samme navn som en
medlemsvariabel, kan ikke medlemsvariabelens navn brukes direkte, det skjules av den andre
variabelens navn. For likevel å kunne bruke det, må vi bruke this til å markere at det er
medlemsvariabelen vi mener. Vi kan også bruke this selv om det ikke er nødvendig, for å
markere at det er dette objektet, og ikke et annet, vi bruker.
Den tredje konstruktøren lager en kopi av et Sirkel-objekt. Det gjør vi ved å sette radius i
det nylagede objektet lik den radius som originalen har. Generelt lager vi en kopi av et
objekt ved å kopiere originalens tilstand, dvs. verdien på alle medlemsvariablene.
Her har vi valgt å bruke metoden finnRadius() for å hente fram originalens radius. Vi kunne
også ha brukt objektets medlemsvariabel, og skrevet radius = original.radius;1. Men
som nevnt tidligere, er det ofte lurt å gå via tilgangsmetodene. Det kunne vi også ha gjort for å
sette radiusen, vi kunne skrevet settRadius(original.finnRadius());. I eksemplet over
gjør vi litt av hvert for å demonstrere flere forskjellige måter å uttrykke ting på.
Siden vi her har to objekter å forholde oss til, kan det være en idé å bruke this for å
tydeliggjøre hvilket objekt vi mener, altså skrive this.radius = original.radius; eller
this.settRadius(original.finnRadius());
Så kommer vi til tilgangsmetodene. Her har vi en metode for å hente fram verdien av radius,
og en for å sette ny verdi på den.
Når vi programmerer en klasse, må vi vurdere hvilke tilgangsfunksjoner vi trenger. Vanligvis
er det naturlig å ha finn-metoder for alle medlemsvariablene, men det er slett ikke sikkert at vi
skal ha sett-metoder for alle. Det kommer an på om vi ønsker at objektene skal være mutable
(foranderlige) eller immutable (uforanderlige). Immutable objekter får sine verdier gjennom
konstruktøren, og kan seinere aldri endres. Mutable objekter har derimot metoder for å endre
(noen av) medlemsvariablene. I vårt eksempel har vi valgt å la Sirkel-objektene være
mutable. Vi har derfor en sett-metode som lar oss endre på radius.
Til slutt har vi metodene beregnAreal() og beregnOmkrets(). Klassekonstanten PI finnes i
klassen Math.
Her er noen eksempler på hvordan vi kan lage objekter av klassen Sirkel, og bruke dem i
beregninger:
Sirkel minSirkel = new Sirkel(10); // en sirkel med radius 10
1
Selv om radius er privat, kan this få tak i original sin radius, for tilgangskontrollen går på klassen, ikke
det enkelte objekt.
Opphavsrett: Forfatter og Stiftelsen TISIP
Klasser og objekter
side 10 av 22
double areal = minSirkel.beregnAreal();
Sirkel enAnnen = new Sirkel();
enAnnen.settRadius(2 * minSirkel.finnRadius()); // radius 20
if (enAnnen.beregnOmkrets() > enAnnen.beregnAreal())
{
Console.WriteLine(”Omkretsen er større enn arealet”);
}
Sirkel endaEn = new Sirkel(minSirkel);
if (endaEn.finnRadius() != minSirkel.finnRadius())
{
Console.WriteLine(”Her er noe alvorlig galt!”);
}
2.3 Mer om klasser og deres innhold
2.3.1 Egenskaper
Vi har sett at vi trenger tilgangsmetoder for medlemsvariablene. I C# har vi en spesiell
mekanisme for å forenkle og standardisere slike tilgangsmetoder. Dette kalles egenskaper
(properties). En egenskap defineres som en metode, men brukes som en medlemsvariabel.
Vi tar fram igjen klassen Sirkel. Her er hvordan vi kan lage egenskapen Radius ut fra
medlemsvariabelen radius:
public int Radius
{
get
{
return radius;
}
set
{
radius = value;
}
}
Vi ser at finnRadius() er blitt til get og settRadius er blitt til set. Det reserverte ordet
value betegner den verdien som skal settes (tilsvarer parameteren radius i settmetoden).
Radius kan nå brukes som om den var en medlemsvariabel, for eksempel int r =
minSirkel.Radius; eller minSirkel.Radius = 20;. Siden den er offentlig, kan den brukes
av hvem som helst, slik at denne setningen er gyldig også utenfor klassen. Det vi har
oppnådd, er å gi utenverdenen en enkel måte å få tilgang på medlemsvariablene på, uten at vi
har gitt avkall på den beskyttelsen som tilgang via tilgangsmetoder gir oss.
Hvis vi vil at det skal være umulig å endre på en egenskap, sløyfer vi set og har bare med
get. Eller vi kan skrive private set for å oppnå at bare klassen selv kan endre egenskapen.
Fra og med c# 2008 er det innført en enkel måte å lage egenskaper på, ved bare å skrive
public int Radius
{
get;
set;
}
eller eventuelt
Opphavsrett: Forfatter og Stiftelsen TISIP
Klasser og objekter
side 11 av 22
public int Radius
{
get;
private set;
}
Da trengs ikke den private variabelen radius, det blir opprettet en tilsvarende variabel
automatisk. Denne måten å gjøre det på, kan bare brukes hvis vi ikke trenger å ha noen annen
kode enn akkurat det å sette og hente verdien, og begge deler må være med. Vi kan for
eksempel ikke bruke denne skrivemåten hvis vi ønsker å kontrollere at radiusen alltid er større
eller lik 0, eller hvis vi bare ønsker get, ikke set.
2.3.2 Konstanter
Vi så tidligere at vi kunne lage en klassekonstant ved å bruke nøkkelordet const, for eksempel
slik:
public const double Skatteprosent = 0.28;
(Konstanter kan godt være offentlige siden det ikke er noen fare for at noen kan endre dem.)
Denne måten å gjøre det på, er grei hvis alle objekter skal ha samme verdi på konstanten. Men
noen ganger trenger vi å lage ulik verdi for de ulike objektene. For eksempel er det ikke
sikkert at alle har samme skatteprosent. Da trenger vi en objektkonstant. Det kan lages ved å
bruke nøkkelordet readonly, slik:
public readonly double Skatteprosent;
Readonly-konstanter får sin verdi i konstruktøren, og kan senere ikke endres. Vi kan for
eksempel lage en konstruktør som tar inn skatteprosenten som et argument, eller vi kan få
konstruktøren til å beregne den på en eller annen måte.
Readonly-konstanter kan også deklareres som static og altså bli klassekonstanter. Fordelen
med dette framfor å bruke const, er at de fortsatt settes i konstruktøren, altså under kjøring,
ikke ved kompilering, slik at vi kan gjøre beregninger før verdien fastsettes.
2.3.3 Strukturer
Objekter kan være enten av klassetype eller av strukturtype. Hovedforskjellen på en klasse og
en struktur, er at klasser er referansetyper, mens strukturer er verdityper. Ellers gjelder det
aller meste av det som er sagt ovenfor om klasser, også for strukturer.
Strukturer er beregnet til bruk for små datamengder. Har vi f.eks. bruk for å returnere to intverdier fra en metode, kan vi definere en struktur for dem og returnere denne.
Det at strukturer er verdityper, medfører at

Vi slipper den ekstra minneadministreringen som kreves når vi oppretter
referansetyper.

Men hvis en slik variabel settes lik en annen, kopieres hele innholdet, ikke bare en
referanse (gjelder for eksempel ved verdioverføring av variabelen).
Til sammen gjør disse argumentene at strukturer er effektivt ved små datamengder, mens
klasser egner seg ved større.
For små, enkle objekter med begrenset levetid, kan vi tillate oss å slakke litt på kravet om at
datamedlemmer skal være private og ha tilgangsmetoder. Da kan vi lage en enkel struktur
slik:
public struct Returverdi {
Opphavsrett: Forfatter og Stiftelsen TISIP
Klasser og objekter
side 12 av 22
public int Lengde;
public int Høyde;
}
Et objekt av strukturen kan lages slik:
Returverdi r = new Returverdi();
eller slik:
Returverdi r;
Begge måtene gjør at vi får en variabel som lagres på stakken (er verditype), ikke på heapen
(slik referansetypene blir).
I begge tilfeller kan vi sette datamedlemmenes verdier slik:
r.Lengde = 10;
r.Høyde = 5;
Hvis vi har laget en konstruktør med to argumenter, kan vi bruke den første metoden til å sette
verdier på datamedlemmene også:
Returverdi r = new Returverdi(10, 5);
Konstruktøren lages på samme måte som før:
public Returverdi(int lengde, int bredde) {
Lengde = lengde;
Bredde = bredde;
}
2.3.4 Overloading
Vi kan lage flere metoder med samme navn. Dette er gunstig når vi har situasjoner der vi
egentlig skal gjøre det samme, men har argumenter med ulik type. WriteLine() er et typisk
eksempel på dette. Vi må kunne skrive ut både tall, tegn og tekststrenger, ja til og med
objekter kan det være kjekt å kunne skrive ut. Derfor tilbys metoden i mange ulike varianter.
Kompilatoren må ha en måte å skille mellom kall til ulike metoder med samme navn. Det kan
den gjøre ved å se på antallet, typen og rekkefølgen til parameterne. En metode med en
parameter er forskjellig fra en med to parametre, en metode der parameteren er en tekststreng
er forskjellig fra en der parameteren er et heltall og en metode der første parameter er en
tekststreng og andre parameter et heltall er forskjellig fra en metode der første parameter er et
heltall og andre parameter er en tekststreng. Derimot spiller det ingen rolle hvilke navn vi
bruker på argumenter eller parametere, og det spiller heller ingen rolle hvilken returtype
metoden har.
En metodes signatur er metodenavnet og parameterlista, altså metodehodet minus returtypen.
Hvis to metoder har samme signatur, blir de regnet som samme metode, og vi får
kompileringsfeil hvis vi prøver å definere den to ganger. Hvis signaturen er forskjellig, men
metodenavnet er likt, er det to forskjellige metoder med overloading.
Her er alle de ulike variantene av WriteLine() (hentet fra dokumentasjonen). Du ser at det
finnes en variant uten noen argumenter (hvorfor kan det ikke finnes fler enn en?), 12 varianter
med ett argument, to med to argumenter, to med tre argumenter og en med fire argumenter.
Alle har returtype void. Det er praktisk for oss som skal bruke dem så vi slipper å huske så
mye forskjellig, men det er ikke noe krav fra kompilatorens side. Legg også merke til at vi
ikke har noe navn på parameterene. Det er ikke nødvendig siden disse navnene ikke har noen
betydning for signaturen. Det viktige er antallet, typen og rekkefølgen av parameterene.
(Noen av parameterene er oppgitt med datatyper du kanskje ikke kjenner. Det er ikke
Opphavsrett: Forfatter og Stiftelsen TISIP
Klasser og objekter
side 13 av 22
meningen at du skal forstå detaljene i alt dette, poenget er å se at de finnes mange metoder
med samme navn men forskjellige signaturer.)
public
public
public
public
public
public
public
public
public
public
public
public
public
public
public
public
public
public
static
static
static
static
static
static
static
static
static
static
static
static
static
static
static
static
static
static
void
void
void
void
void
void
void
void
void
void
void
void
void
void
void
void
void
void
WriteLine();
WriteLine(bool);
WriteLine(char);
WriteLine(char[]);
WriteLine(decimal);
WriteLine(double);
WriteLine(int);
WriteLine(long);
WriteLine(object);
WriteLine(float);
WriteLine(string);
WriteLine(uint);
WriteLine(ulong);
WriteLine(string, object);
WriteLine(string, params object[]);
WriteLine(char[], int, int);
WriteLine(string, object, object);
WriteLine(string, object, object, object);
Når kompilatoren skal finne ut hvilken metode som skal kalles, går den gjennom lista av
metoder med det aktuelle navnet. Hvis den finner en metode med en signatur som passer
nøyaktig, brukes den. Hvis ikke, prøver den å omforme argumentene slik at det passer.
Eksempel: Gitt metodene finnDato(int dag) og finnDato(string dag). Hvis vi sender
inn en char som argument, vil den første versjonen brukes siden char kan omformes til int,
men ikke til string.
En annen variant (som ikke er overloading) er metoder som har et eller flere
standardargumenter. Et standardargument er et argument som får verdi når metoden defineres.
Så kan den som skal bruke metoden bestemme om standardverdien skal brukes eller ikke.
Eksempel:
public void skrivLinjer (string hvaSkalSkrives, int antallLinjer = 1) {
for (int i=0; i < antallLinjer; i++) {
Console.WriteLine(hvaSkalSkrives);
}
}
Denne metoden kan enten kalles ved å skrive
skrivLinjer(tekst);
Da får vi en linje. Eller vi kan be om 4 linjer ved å skrive
skrivLinjer(tekst, 4);
Parametre med standardargumenter må alltid stå bakerst i parameterlista.
2.3.5 Partielle klasser
Normalt lar vi koden for en hel klasse finnes på en og bare en fil. Men i noen tilfeller kan det
være hensiktsmessig å dele den på mer enn en fil. Dette blir aktuelt for oss når vi begynner å
lage Web-applikasjoner. Da lager Visual Studio to filer. På den ene lagres den koden vi lager
Opphavsrett: Forfatter og Stiftelsen TISIP
Klasser og objekter
side 14 av 22
selv, på den andre lagres det som Visual Studio generer for oss. Da lages altså to partielle
klasser, og disse settes sammen til en fullstendig klasse ved kompilering.
2.4 Argumentoverføring
2.4.1 Innargumenter, utargumenter og kombinerte argumenter
Når vi skal lage en metode, er det lurt å starte med å finne ut hvilke opplysninger metoden
trenger for å kunne gjøre jobben sin. Dette blir argumenter som kommer inn til metoden, og vi
kaller dem inn-argumenter.
Deretter må vi finne ut hva som skal komme ut av metoden. Noen ganger skal metoden bare
gjøre noe, uten at vi forventer noe spesielt svar tilbake – for eksempel skrive til skjermen. Da
får vi en metode med returtype void.
Andre ganger vil resultatet være en eller flere verdier som skal formidles tilbake til den
kallende metoden. Er det bare én verdi som skal ut, kan vi bruke returverdien fra metoden. I
C# er det (i motsetning til for eksempel i Java) imidlertid også mulig å bruke argumenter til
dette, da får vi det som kalles ut-argumenter (de sender verdier ut av metoden).
Det siste tilfellet er når vi først må få en opplysning inn til metoden, deretter endre den og
sende den ut igjen. Da får vi et kombinert inn-/ut-argument. Eksempel: Vi skal ha en metode
som skal være slik at første og andre argument skal bytte verdi. Da trenger vi verdien til første
argument for å kunne endre verdi på andre argument, og vi trenger verdien til andre argument
for å kunne endre verdien på første argument.
2.4.2 Verdioverføring av verdityper
I de eksemplene vi har sett på hittil, har vi for det meste brukt verdityper som argumenter til
metodene. Da får vi (hvis vi ikke spesifikt angir noe annet) det som kalles verdioverføring av
argumentene: metodens parametere får samme verdi som argumentene, de blir altså kopier av
argumentene. Parametrene fungerer som de lokale variablene, de opprettes når metoden kalles
og forsvinner igjen straks metoden er ferdig. Inni metoden kan de godt endres, men siden de
bare er kopier av argumentene, har dette ingen effekt på argumentenes verdier.
Verdioverføring egner seg derfor bare til innargumenter.
2.4.3 Referanseoverføring av verdityper
Hvis vi vil endre på et argument, må vi bruke referanseoverføring. Referanseoverføring
innebærer at det ikke er verdien, men adressen til variabelen som overføres, og metoden får
dermed adgang til den lagerplassen som argumentet er lagret i. Argument og parameter blir
derfor samme variabel, og endring i parameteren gir tilsvarende endring i argumentet.
For å fortelle at vi ønsker referanseoverføring, bruker vi det reserverte ordet ref. Her er
metoden byttVerdi() som bytter om verdiene på de to argumentene:
static void byttVerdi(ref double a, ref double b)
{
double temp = a;
a = b;
b = temp;
}
Opphavsrett: Forfatter og Stiftelsen TISIP
Klasser og objekter
side 15 av 22
Når vi bruker metoden, må vi også passe på å få med oss ordet ref:
byttVerdi(ref lengde, ref høyde);
fører til at lengde får den verdien som høyde hadde, og høyde får den verdien som lengde
hadde.
Men nøkkelordet ref forteller også at argumentet må ha fått en verdi før metoden kalles. Det
egner seg for kombinerte inn/ut-argumenter siden disse nettopp har bruk for argumentets
startverdi.
Derimot er det ikke nødvendig med en startverdi på utargumenter, siden slike argumenter skal
få sin verdi inni metoden. Da skriver vi out i stedet for ref og forteller dermed kompilatoren
at startverdi ikke er nødvendig. Vi får fortsatt referanseoverføring av argumentene.
Eksempel: Beregning av kroner og øre
static void omformTilKronerOgØre(double beløp, out int kr, out int øre)
{
kr = (int)beløp;
double temp = (beløp – kr) * 100;
øre = (int)(temp + 0.5); // Avrunding i stedet for avkutting
}
Bruk:
...
double beløp = 100.0 / 3.0;
int kr;
int øre;
omformTilKronerOgØre(beløp, out kr, out øre);
Console.WriteLine("beløpet blir " + kr + " krone(r) og " + øre + " øre.");
...
Referanseoverføring er grei å bruke hvis det er mer enn en ting vi ønsker å få ut av metoden
fordi vi kan referanseoverføre så mange argumenter som vi måtte ønske. Er det bare en ting
som skal ut, bruker vi returverdi, er det flere, bruker vi referanseoverføring.
Referanseoverføring har imidlertid et par ulemper i forhold til verdioverføring:

Ved verdioverføring kan et argument være et hvilket som helst uttrykk – en variabel,
en konstant eller et sammensatt uttrykk. Ved referanseoverføring må argumentet ha en
adresse, og derfor kan bare en variabel brukes.

Verdioverføring er en sikrere overføringsmetode. Ved referanseoverføring må den
kallende metoden overlate sin variabel til en annen metode og må stole på at denne
metoden faktisk gjør det som forventes og ikke ødelegger variabelen.
Oppsummering:
Bruk verdioverføring mest mulig. Referanseoverføring brukes bare når det er helt nødvendig.
Innargumenter trenger ingen spesielle merker på seg. De blir verdioverført.
Kombinerte argumenter trenger nøkkelordet ref for å fortelle at de er referanseoverført med
startverdi.
Utargumenter trenger nøkkelordet out for å fortelle at de er referanseoverført uten krav om
startverdi.
Opphavsrett: Forfatter og Stiftelsen TISIP
Klasser og objekter
side 16 av 22
2.4.4 Verdioverføring av referansetyper
For referansetyper fungerer argumentoverføringen annerledes. Der er det selve referansen
som verdioverføres og som dermed kopieres. Men siden dette er adressen til objektet, vil altså
argumentet og parameteren bli to referanser til samme objekt. Vi får derfor direkte tilgang til
selve objektet, og kan endre dets tilstand (hvis det er et mutabelt objekt). Derfor sier vi ofte at
objektet referanseoverføres, når det helt presist er referansen til objektet som verdioverføres.
Eksempel: I konstruktøren
public Sirkel(Sirkel original)
{
radius = original.finnRadius();
}
vil parameteren original være en referansetype. Hvis vi inni denne konstruktøren legger til
setningen
original.radius = 0;
og senere skriver
Sirkel denne = new Sirkel(10);
Sirkel denAndre = new Sirkel(denne);
vil denAndre få radius 10, mens denne får endret sin radius til 0, siden original viser til
samme objekt som denne. Se figuren under.
Utføring av konstruktøren Sirkel denAndre = new Sirkel(denne)
denne
denne
0
denAndre
10
10
Før starten
original
Like før avslutning
denne
0
denAndre
10
Etter avslutning
2.4.5 Referanseoverføring av referansetyper
Det er også mulig å referanseoverføre referansen. Da kan vi ikke bare endre på objektet, men
også på referansen, dvs. vi kan få referansen til å referere til et annet objekt. Det kan være
nyttig i noen spesielle tilfeller. Eksempel: Vi ønsker en metode som sorterer to tekststrenger
slik at første argument inneholder den minste og andre argument den største. Da kan vi skrive
Opphavsrett: Forfatter og Stiftelsen TISIP
Klasser og objekter
side 17 av 22
static void sorter(ref string a, ref string b)
{
if (a.CompareTo(b) > 0) {
string temp = a;
a = b;
b = temp;
}
}
Men også her gjelder regelen om at vi bruker verdioverføring mest mulig, og
referanseoverføring bare hvis det er nødvendig.
2.5 Objektorientert design
2.5.1 Use-case og scenarier
Til slutt skal vi se litt nærmere på prinsippene for objektorientert design. Dette er ikke dekket
i læreboka, men kan være nyttig å vite litt om likevel.
Før vi begynner å programmere, må vi vite hva vi skal lage og hvordan det skal fungere. En
måte å starte på, er å tenke seg systemet i bruk, og følge prosessen fra start til mål. Da lister vi
opp typiske måter å bruke systemet på (use-case), og lager scenarioer som viser interaksjonen
mellom ulike deler av systemet etter hvert som prosessen går framover.
Eksempel:
Tenk deg at vi skal lage et system for administrering av lønnsutbetalinger i en bedrift. Alle
ansatte får betaling i henhold til innleverte timelister der de angir hvilken type arbeid de har
gjort og hvor mange timer.
Use-caser:
-
Hver av de ansatte skriver inn en arbeidstype og et antall timer. Kanskje må prosessen
gjentas fordi flere typer arbeid skal registreres.
-
Lønningssjefen skal produsere en liste med antall timer arbeidet og lønna for alle ansatte.
-
Produksjonssjefen skal holde oversikt over hva som faktisk gjøres. Hun må derfor
produsere lister over antall timer jobbet med ulike typer arbeid.
Et scenario for første use-case kan være:
En ansatt logger seg inn på systemet og blir bedt om å oppgi arbeidstype og antall timer.
Deretter finner systemet riktig person i sin database, og registrerer den angitte arbeidstypen og
timetallet. Deretter spør systemet om brukeren vil registrere flere arbeidstyper og timer. Hvis
ja, foreta samme prosess en gang til. Hvis nei, logg ut bruker.
Sideløp: Brukeren ønsker å rette opp en feil – Hva skal skje da?
Objekter: Bruker, system, database. Flere vil nok dukke opp hvis vi begynner å gå mer
detaljert til verks (detaljdesign og/eller programkode).
Opphavsrett: Forfatter og Stiftelsen TISIP
Klasser og objekter
side 18 av 22
2.5.2 UML, klassediagram og sekvensdiagram
UML (Unified Modelling Language) er en standard for hvordan ulike sammenhenger innen
objektorientert design/programmering kan framstilles. Det er definert mange typer
diagrammer, hvorav klassediagram kanskje er det enkleste og mest benyttede.
I et klassediagram kan vi blant annet framstille
1. Objektenes innhold (variabler, metoder)
2. En-del-av-forhold mellom objekter
3. Arvesammenheng mellom klasser
Et klassediagram for en enkel klasse kan bestå av klassenavnet og de medlemsvariabler og
metoder som finnes der, for eksempel slik:
Bruker
navn
SjekkPassord
FinnArbeidstype
FinnTimeantall
Figur 1: Klassediagram for klassen Bruker
Her er Bruker navnet på klassen. Den har medlemsvariabelen navn og metodene
SjekkPassord, FinnArbeidstype og FinnTimeantall.
Vi skal se på mer kompliserte klassediagram når vi kommer til en-del-av-forhold (i punkt
2.5.3 og arv (i leksjon 4).
Et sekvensdiagram prøver å framstille samarbeidet mellom objekter, og i hvilken rekkefølge
ting skjer. Se figuren under. Der gir først brukeren systemet beskjed om at han ønsker å gjøre
en ny registrering. Da må systemet først verifisere brukerens identitet, noe det gjør ved å
sende en forespørsel (melding) til databasen. Etter at databasen har gitt beskjed om at
brukeren er ok, sender systemet en forespørsel til brukeren om å angi data. Brukeren sender
dataene tilbake til systemet, hvorpå systemet sjekker dataene ved å sende en forespørsel til (en
annen del av) seg selv. Når dataene er godkjent, sendes de til databasen for registrering.
Databasen gir beskjed tilbake om at registreringen er utført, og systemet gir til slutt brukeren
beskjed om det samme.
Opphavsrett: Forfatter og Stiftelsen TISIP
Klasser og objekter
side 19 av 22
En bruker
Systemet
Databasen
Ny registering
Sjekk brukerid
ok
Angi data
Register data
Sjekk data
Register data
Ok
Ok
Figur 2: Sekvensdiagram for registering av data
2.5.3 Samarbeid mellom objekter og en-del-av-forhold
Objekter samarbeider ved å sende meldinger til hverandre. Meldingene består i å be et objekt
utføre en metode og gjerne få et svar tilbake. I vårt eksempel sender objektet enBruker
meldingen NyRegistering() til objektet Systemet som igjen sender meldingen
SjekkBrukerId til objektet Databasen. Databasen utfører metoden og sender svaret tilbake
til Systemet (se figur 2).
Ofte vil det være et mer komplisert samarbeid som foregår. La oss tenke oss at vi holder på å
programmere en banktjeneste. Da trenger vi kanskje et bank-objekt og en rekke kontoobjekter. Hvis vi skal lage en metode for å overføre et beløp fra en konto til en annen, kan
bank-objektet sende melding til et konto-objekt om at det skal redusere sin saldo med et
bestemt beløp, og til et annet konto-objekt om at det skal øke sin saldo tilsvarende. Bankobjektet behøver ikke å vite hvordan de to konto-objektene faktisk utfører forespørselen, det
er nok å vite at de gjør det.
En konto har (blant annet) et kontonummer, en kontoeier og en saldo. Dette blir
medlemsvariabler i konto-klassen. Vi sier at kontonummeret er en del av konto-objektet. I
figur 3 har vi tegnet klassediagram for Konto-klassen, men har for enkelhets skyld droppet
metodene.
Opphavsrett: Forfatter og Stiftelsen TISIP
Klasser og objekter
side 20 av 22
Konto
kontonummer
saldo
kontoeier
Figur 3: Klassediagram for klassen Konto
Bank-objektet på sin side, har mange konto-objekter, ett for hver konto som finnes i banken.
Kontoene er altså en del av bank-objektet.
Vi kan bygge opp komplekse objekter ved å sette det sammen av mange deler, som hver for
seg kan være objekter bestående av nye deler. Slike en-del-av-forhold kan deles i ulike typer:

Komposisjon: Delene er fast knyttet til det sammensatte objektet, og har bare mening
så lenge det overordnede objektet eksisterer. For eksempel har det ingen mening å
snakke om kontonummeret hvis kontoen ikke eksisterer.
I figur 4 er dette vist i et UML-diagram. Den svarte firkanten forteller at det er en
komposisjon. Datamedlemmet kontonummer er trukket ut og viser hvilken rolle
klassen String spiller i denne sammenhengen – vi skal ha en tekststreng som
inneholder kontonummeret. Ett-tallet nærmest Konto-klassen forteller at
kontonummeret tilhører en og bare en konto, mens ett-tallet nærmest String-klassen
forteller at hver konto bare inneholder ett kontonummer.
Konto
1
kontonummer
1
String
saldo
kontoeier
Figur 4: Komposisjon

Aggregering: Delene er mindre fast knyttet til det overordnede objektet. En del kan
være knyttet til flere objekter. For eksempel kan en kontoeier være tilknyttet flere
kontoer.
I figur 5 har vi tegnet en aggregering, vist ved den hvite firkanten. Vi sier at rollen
som kontoeier skal fylles av et objekt av typen Navn. Figuren viser også at hver konto
bare kan ha en eier (ett-tallet nærmest klassen Navn), mens en eier kan eie mange
kontoer (stjerna nærmest klassen Konto).
Opphavsrett: Forfatter og Stiftelsen TISIP
Klasser og objekter
side 21 av 22
Konto
1
kontonummer
1
String
kontoeier
1
Navn
saldo
*
fornavn
etternavn
finnFornavn
finnEtternavn
Figur 5: Aggregering

Assossiasjon: Det er en viss tilknytning mellom objektene, men det er ikke lenger gitt
hvem som ”eier” hvem (eller at noen av dem eier den andre). For eksempel kan vi
tenke oss at bank-objektet har en rekke konto-objekter samt en rekke kontoeierobjekter. Konto og kontoeier er knyttet sammen med en assosiasjon, men det er
bankobjektet som "eier" begge. Se figur 6, der vi har sagt at en konto kan være
assosiert til mange navn, og et navn kan være assossisert til mange kontoer.
Bank
kontoer
1
*
Konto
*
kontonummer
*
kontoeier
1
kontoeiere
*
Navn
Figur 6: Assossiasjon
Hvilken type en-del-av-forhold vi har, vil ha betydning for hvordan vi programmerer
klassene. Har vi en komposisjon, skal delene eksistere bare når det overordnede objektet
eksisterer. Hvis utenverdenen skal ha tak i en av delene, får de en kopi, slik at vi ikke risikerer
at noen refererer til en del som ikke lenger eksisterer. For eksempel vil ikke bank-objektet la
en applikasjon få direkte tilgang til sine konto-objekter, bare til kopier av dem.
Har vi derimot en aggregering, vil det være naturlig at andre får tilgang til selve objektene.
Har flere kontoer samme kontoeier, ønsker vi at hvis den ene kontoen endrer adressen til
kontoeieren, vil den endrede adressen også gjelde for de andre kontoene som har samme eier.
Og det kan vi ikke få til hvis de ulike kontoene har hver sin kopi av kontoeier-objektet, de må
ha tilgang til det samme objektet.
Opphavsrett: Forfatter og Stiftelsen TISIP
Klasser og objekter
side 22 av 22
For begge disse typene en-del-av-forhold vil ”kommandolinjen” vanligvis gå fra det
overordnede objektet til de ulike delene. Bank-objektet gir for eksempel beskjed til et kontoobjekt om å oppdatere saldoen. Derimot er det ikke naturlig at konto-objektet ber bankobjektet utføre en oppgave. I praksis programmerer vi det slik at det overordnede objektet
inneholder en referanse til del-objektet.
For assosiasjoner kan det være aktuelt med kommandoer begge veier. Kontoeier-objektet kan
ønske å få sjekket saldoen på alle sine kontoer mens kontoen kan gi kontoeier-objektet
beskjed om å endre adresse. I slike tilfeller må vi ha forbindelse begge veier.
Som en fellesbetegnelse for disse typene bruker vi begrepet en-del-av-forhold. Bare hvis vi
eksplisitt trenger å presisere typen nærmere, vil vi bruke de presise betegnelsene.
2.5.4 Litt om bruk av grensesnitt
Vi har sagt tidligere at utenverdenen ikke behøver å vite hvordan en metode er implementert,
det er nok å vite hvordan den skal brukes. I design-fasen er det derfor greit å bare navngi
metoder og spesifisere hensikten med dem, så kan implementasjonen vente til senere.
Noen ganger finner man ut at en metode kan være kjekk å ha i flere forskjellige klasser. For
eksempel kan vi sammenlikne to tekststrenger, men det kan også være nyttig å kunne
sammenlikne to konto-objekter eller to dato-objekter. Generelt vil mange klasser kunne ha
nytte av en sammenlikningsmetode. I slike tilfeller definerer vi et grensesnitt (interface) for
den (eller de) felles metoden(e) og lar klassene implementere dette grensesnittet.
Grensesnitt navngis gjerne med en I (for interface) først, for eksempel har vi grensesnittet
IComparable som inneholder metoden CompareTo(). CompareTo() er definert slik at den
returnerer en negativ verdi hvis objektet som meldingen blir sendt til er mindre enn det
objektet som sendes med som argument, 0 hvis de er like og en positiv verdi returneres hvis
det objektet som meldingen sendes til er større enn argumentet.
Klassen String (og mange andre) implementerer dette grensesnittet. Dermed vet vi at
strengobjekter kan sammenliknes, men for å finne ut hvordan det faktisk gjøres, må vi se på
dokumentasjonen for String-klassen. For objekter av andre klasser vil sammenlikningen skje
på andre måter, hver klasse må lage sin egen implementasjon av metoden. Poenget med
grensesnitt er at det skal være et enhetlig ansikt utad, kjenner du bruken i en klasse, vet du at
den brukes på samme måte i andre klasser.
Kontoer kan sammenliknes hvis vi skriver følgene:
class Konto : IComparable
{
…
public virtual int CompareTo(object obj)
{
Konto detAndre = (Konto)obj;
int sml = kontonummer.CompareTo(detAndre.kontonummer);
return sml;
}
}
Opphavsrett: Forfatter og Stiftelsen TISIP