14 - referenciák, case objectek

Scalában minden összetett típus (az Int, Double, Boolean, Long stb. egyszerű típusok, ezekből a Java primitív típusai készülnek; de minden más típus) létrehozáskor a heap memóriában, dinamikusan jön létre. Vegyük például a Pont típust még egyszer:

1
2
3
4
case class Pont(x: Int, y: Int)

val p = Pont(2, 3)
val q = p

Ennek a kódnak a tényleges viselkedéséhez legközelebb álló C kód a következő:

1
2
3
4
5
6
7
8
9
typedef struct {
  int x;
  int y;
} Pont;

Pont *p = (Pont*)malloc(sizeof(Pont));
p->x = 2;
p->y = 3;
Pont *q = p;

Vagyis, amikor egy Pont(2,3) hívást intézünk, akkor dinamikusan, a heap memóriában lefoglalódik egy új memóriaterület, a két mezőnek megfelelő érték beáll a 2-re ill. 3-ra, majd ennek az újonnan létrehozott és tartalommal feltöltött memóriaterületnek végső soron a címe kerül a p értékbe.

Ezek után a q=p értékadás hatására ez a cím másolódik át q-ba, így a két érték egy-egy pointert tárol, ami ugyanarra a memóriaterületre mutat.

(attól ne tartsunk, hogy nincs memória felszabadítás: a JVM mentalitása az, hogy nincs külön memória-felszabadító függvény, mint C-ben a free, hanem a JVM mellett fut egy garbage collector, aki ha észreveszi, hogy egy memóriaterületre már nem mutat senki, felszabadítja automatikusan.) Teljesen hasonlóan, a Lista case classunk is leginkább valami efféle lehetne C-ben:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
typedef struct {
  int type; //0 üres, 1 nemüres
  union {
    struct UresLista *uresLista;
    struct NemuresLista *nemuresLista;
  };
} Lista;

typedef struct UresLista{
} UresLista;

typedef struct NemuresLista{
  int head;
  Lista *tail;
} NemuresLista;

// ures: egy UresLista
Lista *ures = (Lista*)malloc(sizeof(Lista));
ures->type = 0;
ures->uresLista = (UresLista*)malloc(sizeof(UresLista));

// egyelemu: NemuresLista( 3, ures )
Lista *egyelemu = (Lista*)malloc(sizeof(Lista));
egyelemu->type = 1;
egyelemu->nemuresLista = (NemuresLista*)malloc(sizeof(NemuresLista));
egyelemu->nemuresLista->head = 3;
egyelemu->nemuresLista->tail = ures;

Még egyszer: amit csak látunk és nem ,,primitív'' típus, az mind valójában ,,referencia'' (a különbség referencia és pointer között: a referencia nem null, mindig egy valid objektumra mutat, nincs referencia-aritmetika, azaz nem tudjuk ,,eltolni'' mint egy pointert, hogy egy tömbben ,,arrébb'' indexeljünk, és szintaktikusan úgy kezeljük a referenciát, mintha egy tényleges objektum lenne - pl. ponttal és nem nyíllal hivatkozzuk a mezőit).

Ez azt is jelenti, hogy pl. egy Lista konstruálásakor valójában konstans idő alatt készül az új lista, mert nem másoljuk a farkát, csak elrakjuk a címét egy mezőbe. (ahogy a kód legvégén, az egyelemu->nemuresLista->tail = ures; hívásban is.)

Továbbá azt is jelenti, hogy az == operátor először is a két oldalán lévő referenciák címét hasonlítja össze, és ha azok egyenlőek, akkor máris le tudja jelenteni, hogy true; ha meg nem, akkor végignézi az adatmezők egyeztetését.

Mi a helyzet tehát akkor az üres listával:

1
2
3
4
case class UresLista() extends Lista

val list = UresLista()
val egyelemu = NemuresLista(5, UresLista())

Ahányszor csak létrehozunk egy UresLista értéket, mindig egy új rész foglalódik le neki a memóriában, ennek a címét megkapja az adott érték. Amikor meg ==-t tesztelünk rá, és két külön helyen lévő példányát nézzük, hogy egyenlőek-e (azok lesznek, hiszen adattag nincs), akkor eltart egy darabig, mire rájön a JVM, hogy ezek nem ugyanazon a címen vannak, de ugyanolyan típusúak, és minden mezejük megegyezik.

case object

Mindezt megspórolhatjuk, ha case class helyett case objectként deklaráljuk az üres listát:

1
2
3
4
case object UresLista extends Lista

val list = UresLista //nincs zárójel!
val egyelemu = NemuresLista(5, UresLista) //itt sincs

Mit tanultunk ebből a kódból?

  • Egy case object lényegében egy olyan típus, aminek egyetlenegy elemű az értéktartománya (tkp ez igaz volt az UresListara mint case classra is, csak több helyen is létrehozhattuk a memóriában ezt az egy értéket)
  • ez az egy érték a program futásának legelején létrejön egyetlen helyen a memóriában, és innentől kezdve minden értékadásnál ezt az egy referenciát kapja minden ilyen típusú érték, az UresLista maga ez a referencia lesz
  • ez többek közt azért jó, mert csak egyszer foglal helyet a memóriában és gyorsabb rá az == ellenőrzés.
  • nincs neki argumentumlistája (ha lenne, nem hozhatnánk létre egyetlen objectként), a zárójelek kitevése fordítási hibát is okoz.

Ebből a lényeg: ha egy típus az 1, akkor azt case objectként hozzuk létre; ha extendelnie kell egy traitet, azt is tudja probléma nélkül.

Az Int Listánk most

A jelenlegi implementációnk:

1
2
3
trait Lista
case object UresLista extends Lista
case class NemuresLista(head: Int, tail: Lista) extends Lista

mintailleszteni pedig szintén tudunk case objectre, nem kell és nem is szabad kitennünk a zárójeleket, de egyébként ugyanúgy, mint korábban:

1
2
3
4
5
def find(list: Lista, value: Int): Boolean = list match {
  case UresLista => false
  case NemuresLista(`value`, _) => true            //ez új minta! backtick!
  case NemuresLista(_, tail) => find(tail, value)
}

Itt láthatunk egy eddig nem tárgyalt mintaillesztést: ha backtick-ek (`) közé rakunk egy azonosítót, akkor az nem egy új azonosító lesz, ami mindenre illeszkedik és behelyettesítődik jobb oldalra, hanem egy már létező azonosítóhoz tartozó érték kerül ide.

Így ez a függvény azt adja vissza (tail rekurzív módon), hogy a paraméterként megadott listában előfordul-e a paraméterként megadott érték. Ha igen, akkor a második case illeszkedik akkor, amikor ahhoz a részlistához érünk, akinek a fejeleme pont a keresett elem (ekkor már mindegy, hogy mi a farok, ezért ott az underscore). Ha meg nem, akkor keresünk tovább a lista farkában (ekkor meg már nem számít, hogy mi is a fejelem; ha a harmadik case alternatívához érünk, akkor már tudjuk, hogy nem a keresett érték az).

Vagyis, egy case NemuresLista(‘value‘,_) case egyenértékű egy case NemuresLista(v,_) if v==value if guardos case-el.

Kérdések, feladatok

  • Mik a főbb különbségek referencia és pointer közt?
  • Hogy működik alapértelmezetten az == operátor?
  • Mi a különbség case class és case object között?

Utolsó frissítés: 2020-12-22 21:04:26