Osztályok (class )

Az előző fejezetben láttuk az új típusok létrehozásának egy módszerét. A nehézség az volt, hogy C#-ban kellett megadni a definíciót, ami nem biztos, hogy mindenkinek egyszerűen megy. PowerShell 5.0-tól jelent meg az az új lehetőség, hogy PowerShell szintaxissal hozhatunk létre új típusokat.

Osztály létrehozása és példányosítása

Ezzel az új lehetőséggel tehát nem csak PSObject típusú egyedi objektumokat hozhatunk létre, hanem teljesen új típust definiálva hozhatjuk létre egyedi objektumainkat, C# forráskód használata nélkül, natívan PowerShell segítségével. Ráadásul rendelkezésünkre áll az öröklődés és a metódusok újradefiniálása (overload).

Nézzük mindezt néhány egyszerű példán! Szeretnénk szabályos síkidomokat kezelni PowerShellben. A szabályos síkidomok esetében leggyakrabban 2 paramétert kell megadni, gondoljunk a téglalapra, ellipszisre, vagy akár deltoidra. Elsőként ábrázoljuk ezt a közös jellegzetességet egy osztályban, amit nevezzük szabályos síkidomnak:

class SzabályosSíkidom {

    [double] $a = 1

    [double] $b = 1

 

    SzabályosSíkidom ([double] $ai, [double] $bi) {

        $this.a = $ai

        $this.b = $bi

    }

 

    SzabályosSíkidom () {

    }

 

    [String] ToString () {

        return "a:$($this.a) b:$($this.b)"

    }

}

 A típus, vagy más szóval osztály definiálására a class kulcsszó áll rendelkezésünkre. Ezután kell szerepeltetni a típus, osztály nevét, majd kapcsos zárójelek között jön az osztály definíciója. Ez áll tulajdonságokból, amelyeket mint változókat rakunk bele. És áll metódusokból, amelyek különösebb kulcsszó nélkül állnak, de fontos, hogy a metódus neve mögött ott kell állnia a gömbölyű zárójel-párnak.

A szabályos síkidomokra, ahogy korábban említettem, általánosan jellemző, hogy két adatot kell megadni velük kapcsolatban, ezek lettek az ’a’ és ’b’ tulajdonság. Amit láthatunk, hogy kötelező a tulajdonságok típusát jelölni, mindkettő [double] típusú és mindkettőnek 1 itt most az alaphelyzet szerinti értéke. A tulajdonságok nyilvánosak, azaz „kívülről” hozzáférhetők, értékük kiolvasható és át is írható.

Az osztálydefinícióban metódusokat is létrehozhatunk. Elsőként nézzük a legalsó, ToString metódust! Itt látható, hogy kötelező a metódusok visszatérési értékének típusát jelölni, ami itt [string]. Valamint kötelező szerepeltetni a return kulcsszót a kimenet jelzésére. Ha a metódusokban hivatkozni szeretnénk a tulajdonságokra, akkor nem $a és $b-vel tehetjük ezt meg, hanem $this.a és $this.b formában, ahol $this az osztály példányát magát szimbolizálja.

A fenti példában láthatunk még két ugyanolyan metódust, amelyeknek a neve megegyezik az osztály nevével. Ezek az osztály konstruktorai. A konstruktor egy speciális metódus, ami akkor hívódik meg, amikor az osztály egy új példányát hozzuk létre. A konstruktor neve tehát kötelezően megegyezik az osztály nevével, és itt nem kell jelezni a visszatérési érték típusát, hiszen pont az osztály egy példányát fogja visszaadni, és a return kulcsszót sem kell szerepeltetni, implicit módon a ’return $this’ kifejezést képzelhetjük oda.

Látható tehát, hogy ugyanazzal a névvel több metódusunk, akár konstruktorunk is lehet. Ezeknek valamilyen paraméterben el kell térniük egymástól. Az első konstruktorom két [double] típusú paramétert vár, a második meg semmit, így ezek egymástól különbözővé teszik a két konstruktort.

Ha lefuttatjuk a fenti típusdefiníciót, akkor az alábbi két módon is hozhatok létre példányt, azaz futtathatom a konstruktort. Hogy éppen melyiket használom a több lehetőség közül, azt a paraméterek döntik el:

PS C:\> $idom1 = New-Object -TypeName SzabályosSíkidom -ArgumentList 5,6

PS C:\> $idom1

a b

- -

5 6

 

PS C:\> $idom2 = [szabályossíkidom]::new()

PS C:\> $idom2

a b

- -

1 1

Az első esetben a New-Object-et használtam a példány létrehozására. Itt az első konstruktort használtam, hiszen két számot adtam át paraméterként. Ugyan nem közvetlenül [double] típusúakat, de a használt [int]-eket a PowerShell könnyedén át tudta lebegőpontos számokká alakítani.

Ha egy osztálynak nincs konstruktora, akkor is implicit módon van. Ha ezt az osztályt definiáljuk:

class Egyszerű {

    [int] $a

}

Akkor ennek a statikus metódusai között ott látjuk a new()konstruktort:

PS C:\> [egyszerű] | Get-Member -Static

 

 

   TypeName: Egyszerű

 

Name            MemberType Definition                                                        

----            ---------- ----------                                                        

Equals          Method     static bool Equals(System.Object objA, System.Object objB)        

new             Method     Egyszerű new()                                                    

ReferenceEquals Method     static bool ReferenceEquals(System.Object objA, System.Object objB)

Öröklődés

Az igazi előnye az osztályok használatának, hogy ha egy meglévőhöz hasonló tudású osztályt akarok létrehozni, akkor nem kell „nulláról” kezdeni, hanem a meglevőből öröklődéssel hozhatom létre az újat, ami az eredetihez képest valamit másképp vagy valamivel többet tud.

Nézzük a téglalapot, mint az előző általános szabályos síkidomból képzett új osztályt:

class Téglalap : SzabályosSíkidom {

    Téglalap () : Base (){

    }

 

    Téglalap ([double] $a, [double] $b) : Base ($a, $b) {

    }

 

    [double] Terület (){

        return $this.a * $this.b

    }

}

Az öröklődést az osztály neve melletti kettőspont utáni alap osztály nevével jelezzük. Ha az új osztály konstruktora ugyanaz, mint az eredeti osztályé, akkor csak egyszerűen jelezzük kettőspont után, hogy az eredeti konstruktort akarjuk meghívni a Base  névvel. Például ez a részlet:

    Téglalap () : Base (){

    }

Ez annyit jelent, hogy a Téglalap konstruktoraként meghívjuk a SzabályosSíkidom konstruktorát, és az ottaniakhoz képest nem kell semmi mást csinálni, így üresen hagytam a metódus törzsét. Az eredeti osztályhoz képest itt már tudunk területet számolni, így készítettem egy Terület metódust.

Nézzük, hogy egy téglalap példány milyen tagjellemzőkkel bír:

PS C:\> $téglalap = [téglalap]::new(12,3)

 

PS C:\> $téglalap | gm

 

 

   TypeName: Téglalap

 

Name        MemberType Definition                   

----        ---------- ----------                   

Equals      Method     bool Equals(System.Object obj)

GetHashCode Method     int GetHashCode()            

GetType     Method     type GetType()               

Terület     Method     double Terület()             

ToString    Method     string ToString()            

a           Property   double a {get;set;}          

b           Property   double b {get;set;}          

Látható, hogy az újonnan létrehozott Terület mellett ott van az öröklött ToString is, ami jól működik téglalappal is:

PS C:\> $téglalap.ToString()

a:12 b:3

 A Get-Member kimenetén látható, hogy „gyárilag” van Equals metódusunk is, ami mindig False értéket ad, de inkább adjunk neki értelmet akár már a SzabályosSíkidom szintjén:

     [bool] Equals ([object] $object){

        return ($this.a -eq $object.a -and $this.b -eq $object.b)

     }   

 Azaz akkor adunk True-t vissza, ha az a-k és a b-k is egyenlők. Ezzel tényleg összehasonlíthatóvá válik két egyforma téglalap:

PS C:\> $t1 = [téglalap]::new(2,5)

 

PS C:\> $t2 = [téglalap]::new(2,5)

 

PS C:\> $t1 -eq $t2

True

 Van még egy lehetőségünk az osztályokkal kapcsolatban. Tegyük fel, hogy a síkidom nevét is szeretnénk tárolni:

class Téglalap : SzabályosSíkidom {

    hidden [string] $név = "Téglalap"

    Téglalap () : Base (){

    }

 

    Téglalap ([double] $a, [double] $b) : Base ($a, $b) {

    }

 

    [double] Terület (){

        return $this.a * $this.b

    }

}

Látható, hogy felvettem egy új $név tulajdonságot, amihez az új hidden  kulcsszót használtam.

PS C:\> $tgllp = [téglalap]::new(4,7)

 

 

PS C:\> $tgllp

 

a b

- -

4 7

Idáig semmi különleges nem történt. Látszólag nincsen név tulajdonságunk, de ha explicit rákérdezünk, akkor azért kiderül, hogy van:

PS C:\> $tgllp.név

Téglalap

Ez azonban nem látható sem format-list, sem get-member-el:

PS C:\> $tgllp | fl *

 

a : 4

b : 7

 

PS C:\> $tgllp | gm

 

 

   TypeName: Téglalap

 

Name        MemberType Definition                       

----        ---------- ----------                      

Equals      Method     bool Equals(System.Object object)

GetHashCode Method     int GetHashCode()               

GetType     Method     type GetType()                  

Terület     Method     double Terület()                

ToString    Method     string ToString()               

a           Property   double a {get;set;}             

b           Property   double b {get;set;}             

Sajnos jelenleg még nem tudjuk ezt csak olvashatóvá tenni, azaz felül is tudjuk bírálni, bár ha valaki nem tud ennek a tulajdonságnak a létezéséről, akkor ennek esélye kicsi:

PS C:\> $tgllp.név = "amőba"

 

PS C:\> $tgllp.név

amőba

Valószínű ezen a területen sok fejlesztés lesz várható a következő verziókban.

Felsorolás osztály létrehozása

Új lehetőség szintén PowerShell 5.0-tól kezdve, hogy egyéni felsorolás-típusokat már beépítetten támogatja a nyelv, nincs szükség C# kód beágyazására. Nézzünk egy egyszerű felsorolást:

enum Színek {

    Kék = 1

    Zöld = 2

    Sárga = 3

    Piros = 4

    Barna = 5

    Fehér = 6

    Fekete = 7

}

A felsorolás osztály létrehozásához az enum  kulcsszót kell használnunk, majd az osztály nevét és ezután szkriptblokkban meg kell adni – hasonlóan a hashtáblákhoz – a címke-érték párokat. Ezután haszáljuk ezt:

PS C:\> $szín = [színek]::Fekete

PS C:\> $szín

Fekete

PS C:\> [int] $szín

7

Látszik, hogy minden színhez egy szám tartozik.

Van egy másik fajtája is a felsorolás típusnak, ahol nem egy-egy szám feleltethető meg a felsorolás címkéihez, hanem egy-egy bit. Ez azért jó, mert nem csak egy-egy címkét vehet fel a változó értéke, hanem címkék különböző kombinációját is. Ez a Flags  típusú felsorolás, nézzünk erre is egy példát:

[Flags()] enum Bitjeim {

    Egy = 1

    Kettő = 2

    Négy = 4

    Nyolc = 8

    Tizehat = 16

}

Itt az értékek 1-1 helyiértéknek feleltethetők meg a 2-es számrendszerben, így akár egyszerre többet is megadhatunk:

PS C:\> $b = [Bitjeim] "Egy, Kettő"

PS C:\> $b

Egy, Kettő

PS C:\> [int] $b

3

Látható, hogy egy olyan sztringet használtam értékadásnál, amiben vesszővel választottam el az értékeket, és a [Bitjeim] típuskonverzió ezt a formátumot ügyesen felismerte és a mögöttes szám a bebillentett biteknek megfelelő 3. Akár fordítva is megadhattam volna és a kis-nagy betű sem számít:

PS C:\> $b = [Bitjeim] "Kettő, egy"

PS C:\> $b

Egy, Kettő

Tömb formátumban is megadhatjuk az értéket:

PS C:\> $b = [Bitjeim] ("Négy", "Nyolc", "Egy")

PS C:\> [int]$b

13

Azt is lehet, hogy nem csak 1 bitet tartalmazó értékek szerepelnek a felsorolásban:

[Flags()] enum Bitjeim {

    Egy = 1

    Kettő = 2

    Három = 3

    Négy = 4

    Nyolc = 8

    Tizehat = 16

}

Itt a „Három” is szerepel az értékek között, így ez az értékadás erre fordul le:

PS C:\> $b = [Bitjeim] "Kettő, egy"

PS C:\> $b

Három

PS C:\> $b = [Bitjeim] "Kettő, egy, négy"

PS C:\> $b

Három, Négy

Az ilyen flag típusú felsorolások nagyon hasznosak lehetnek saját függvényeink paramétereinek megadásánál.

Egy összetett osztály példája: láncolt lista

Az egyéni osztályok illusztrálására nézzünk egy összetettebb példát. Ugyan nem vagyok híve a nagyon hosszú kódok szerepeltetésének a könyvben, de mivel ezt úgyis elektronikusan olvassák, ezért könnyű kimásolni ezt és felhasználni, elemezni a szkriptszerkesztőben.

A példa egy láncolt lista létrehozása lesz. Ugyan a .NET keretrendszerben van ilyen ([system.collections.generic.LinkedList]), de nekem nem tetszik túlzottan, hiszen az csak egynemű elemek tárolását teszi lehetővé, én meg tetszőleges elemet szeretnék tárolni.

Eleve, miért kell nekünk ilyen láncolt lista? Miért nem jó például a [system.collections.arraylist]? Vannak olyan feladatok, ahol az elemek egymáshoz képesti helyzete fontos, azaz például nagyon sok elemet kell beszúrni meglevő elemek közé. Az ArrayList-nek van Insert metódusa, de az nagy elemszámnál nagyon belassul, főleg ha a lista elejére szúrjuk be az új elemeket.

Nézzük a saját láncolt listát osztályomat:

class MyLinkedListElement {

    [PSObject] $Value = $null

    [PSObject] $NextElement = $null

    [PSObject] $PreviousElement = $null

    hidden [PSObject] $List

 

    MyLinkedListElement ($iValue, $iList){

        $this.Value = $iValue

        $this.List = $iList

        $this

    }

 

    LinkAfter ([PSObject] $newvalue) {

        $newelement = [MyLinkedListElement]::new($newvalue, $this.List)

        if($this.NextElement){

            $this.NextElement.PreviousElement = $newelement

        }

        $newelement.PreviousElement = $this

        $newelement.NextElement = $this.nextelement

        $this.nextelement = $newelement       

        $this.List.Count++   

    }

    LinkBefore ([PSObject] $newvalue){

        $newelement = [MyLinkedListElement]::new($newvalue, $this.List)

        if($this.PreviousElement){

            $this.PreviousElement.NextElement = $newelement

        }

        $newelement.PreviousElement = $this.previouselement

        $newelement.NextElement = $this

        $this.previouselement = $newelement           

        $this.List.Count++

    }

    BreakLink(){       

        if($this.List.ListElements().Where({$_.PreviousElement -eq $null -or $_.NextElement -eq $null})){

            return

        }

        $this.NextElement.PreviousElement = $null

        $this.NextElement = $null

    }

    Remove () {

        $this.Value = $null

        $this.List.Count--

        if($this.GetHashCode() -eq $this.List.RootElement.GetHashCode()){

            if($this.NextElement){

                $this.List.RootElement = $this.NextElement

            }

            elseif($this.PreviousElement){

                $this.List.RootElement = $this.PreviousElement

            }

        }

        if($this.PreviousElement -ne $null -and $this.NextElement -ne $null){

            $this.NextElement.PreviousElement = $this.PreviousElement

            $this.PreviousElement.NextElement = $this.NextElement

        }

        else{

            if($this.NextElement -ne $null){

                $this.NextElement.PreviousElement = $null

            }

            elseif($this.PreviousElement -ne $null){

                $this.PreviousElement.NextElement = $null

            }

        }

    }

 

    [string] ToString (){

        return $this.Value.ToString()

    }

}

 

class MyLinkedList {

    [MyLinkedListElement]$RootElement

    [MyLinkedListElement]$CurrentElement

    [uint32] $Count = 0

    

    MyLinkedList ([PSObject] $RootValue) {

        $this.RootElement = [MyLinkedListElement]::new($RootValue, $this)

        $this.CurrentElement = $this.RootElement

        $this.Count = 1

    }

 

    Reset(){

        $this.CurrentElement = $this.RootElement

    }

 

    [PSObject[]] LinkElements (){

        $prev = $this.ListElements().where({$_.PreviousElement -eq $null})

        $next = $this.ListElements().where({$_.NextElement -eq $null})

        if($prev -and $next){

            $prev[0].PreviousElement = $next[0]

            $next[0].NextElement = $prev[0]

            return $prev[0], $next[0]

        }

        else{

            return $null

        }

    }

 

    [PSObject] GetNextElement ([uint32] $step){

        for($i = 0; $i -lt $step; $i++){

            if($this.CurrentElement.nextelement -eq $null){

                break

            }

            $this.CurrentElement = $this.CurrentElement.NextElement

        }

        return $this.CurrentElement

    }

 

    [PSObject] GetNextElement (){

        return $this.GetNextElement(1)

    }

 

    [PSObject] GetPreviousElement ([uint32] $step){

        for($i = 0; $i -lt $step; $i++){

            if($this.currentelement.PreviousElement -eq $null){

                break

            }

            $this.CurrentElement = $this.CurrentElement.PreviousElement

        }

        return $this.CurrentElement

    }

 

    [PSObject] GetPreviousElement (){

        return $this.GetPreviousElement(1)

    }

 

     [PSObject[]] ListElements (){

        $i = $this.Count

        return $(

            ($this.CurrentElement = $this.RootElement)

            $i--

            while($i -ge 1 -and $this.CurrentElement.NextElement){

                ($this.CurrentElement = $this.CurrentElement.NextElement)

                $i--

            }

 

            if($i -ge 1){

                $this.CurrentElement = $this.RootElement

                while($i -ge 1 -and $this.CurrentElement.PreviousElement){

                    ($this.CurrentElement = $this.CurrentElement.PreviousElement)

                    $i--

                }

            }

        )

    }

 }

A láncolt lista valójában 2 osztályból áll. Az első a láncolt lista eleme ([MyLinkedListElement]) és maga a lista ([MyLinkedList]). Az elemnek 4 tulajdonsága van: az adott elem értéke, a következő elem, az előző elem és egy rejtett tulajdonság, ami magára a MyLinkedList objektumra mutat, amihez az elem tartozik. Metódusai:

a konstruktor

LinkAfter: az elem után fűzök be egy új elemet

LinkBefore: az elem elé fűzök be egy új elemet

BreakLink: ha gyűrűt csináltam a linkelésekkel, akkor ezzel lehet azt felszakítani az adott elem után

Remove: egy elem eltávolítása és a felszakított linkek begyógyítása

ToString: az elem szöveges reprezentációja

A MyLinkedList objektumnak 3 tulajdonsága van: a kezdő elem, aktuális elem és az összes elem száma. A metódusok:

a konstruktor

Reset: az aktuális elemet a kezdő elemre állítja

LinkeElements: gyűrűt csinál az láncolt listából, azaz a lánc két végét összeköti

GetNextElement: az adott elemhez képes előre megy megadott lépést, az lesz az új aktuális elem, amit vissza is ad. Ennek a metódusnak van egy olyan változata, amikor nem adunk meg lépésszámot, ilyenkor egyet lép előre.

GetPreviousElement: ugyanúgy működik, mint a GetNextElement, csak hátra lép.

ListElements: kilistázza az összes elemet a listából. Elsőként a kezdő elemhez képest a következőket adja vissza, majd az előzőket. Az aktuális elemet az utoljára kilistázott elemre állítja.

Hottunk létre egy egyszerű listát pár elemmel:

PS C:\> $ll = [mylinkedlist]::new("Root")

PS C:\> $ll.RootElement.LinkAfter("Egy")

PS C:\> $ll.RootElement.LinkAfter("Kettő")

PS C:\> $ll.RootElement.LinkBefore("Három")

Ha listázzuk az elemeket:

PS C:\> $ll.ListElements()

 

Value NextElement PreviousElement

----- ----------- ---------------

Root  Kettő       Három

Kettő Egy         Root

Egy               Kettő

Három Root

Grafikusan ábrázolva így néz ki a listánk:

57 . ábra Láncolt lista példa

Miután mindig a Root-hoz képes illesztettük be az újabb elemeket, ezért alakult ki a fenti, kicsit összevisszának tűnő sorrend. Az aktuális elem az ábrán kékkel van kiemelve, az a legutoljára berakott elem, a Három.

PS C:\> $ll.CurrentElement

 

Value NextElement PreviousElement

----- ----------- ---------------

Három Root

 

 

PS C:\> $ll.RootElement

 

Value NextElement PreviousElement

----- ----------- ---------------

Root  Kettő       Három

Ha előre lépünk kettőt, akkor pont a Kettő elemhez jutunk:

PS C:\> $ll.GetNextElement(2)

 

Value NextElement PreviousElement

----- ----------- ---------------

Kettő Egy         Root

Ezzel az aktuális elem is ez lett:

PS C:\> $ll.CurrentElement

 

Value NextElement PreviousElement

----- ----------- ---------------

Kettő Egy         Root

58 . ábra Lista állapota 2 lépés előre után

Itt a listánk nyitott, de a szabad végeket össze is köthetjük:

PS C:\> $ll.LinkElements()

 

Value NextElement PreviousElement

----- ----------- ---------------

Három Root        Egy

Egy   Három       Kettő

 

 

PS C:\> $ll.ListElements()

 

Value NextElement PreviousElement

----- ----------- ---------------

Root  Kettő       Három

Kettő Egy         Root

Egy   Három       Kettő

Három Root        Egy

Látható, hogy így már minden NextElement és PreviousElement mutat egy másik elemre. Így néz ki most a listánk:

59 . ábra Lista gyűrűvé alakítva

Ha már gyűrűnk van, akkor akár 101 elemmel is léphetünk előre, ami 4 elem esetében pont a Root-ot fogja megadni:

PS C:\> $ll.GetNextElement(101)

 

Value NextElement PreviousElement

----- ----------- ---------------

Root  Kettő       Három

Ha ez Egy és a Kettő közé be akarom illeszteni a Másfél nevű elemet, akkor vagy a Kettő utáni, vagy az Egy előtti elemként kell ezt megtennünk, ez utóbbi így néz ki:

PS C:\> $ll.GetNextElement(2)

 

Value NextElement PreviousElement

----- ----------- ---------------

Egy   Három       Kettő

 

PS C:\> $ll.CurrentElement.LinkBefore("Másfél")

PS C:\> $ll.ListElements()

 

Value  NextElement PreviousElement

-----  ----------- ---------------

Root   Kettő       Három

Kettő  Másfél      Root

Másfél Egy         Kettő

Egy    Három       Másfél

Három  Root        Egy

Ha el akarunk távolítani egy elemet, akkor először arra kell állni. Én most a Root-ot akarom törölni:

PS C:\> $ll.Reset()

PS C:\> $ll.CurrentElement

 

Value NextElement PreviousElement

----- ----------- ---------------

Root  Kettő       Három

Ezután jöhet az eltávolítás, ami helyre rakja a linkeléseket és szükség esetén új Root-ot jelöl ki:

PS C:\> $ll.CurrentElement.Remove()

PS C:\> $ll.ListElements()

 

Value  NextElement PreviousElement

-----  ----------- ---------------

Kettő  Másfél      Három

Másfél Egy         Kettő

Egy    Három       Másfél

Három  Kettő       Egy

 

 

PS C:\> $ll.RootElement

 

Value NextElement PreviousElement

----- ----------- ---------------

Kettő Másfél      Három

Nézzük, mennyire hatékony ez a láncolt lista adattípus, ha 200.000 új elemet illesztek bele, összehasonlítva egy ArrayList-hez képest:

$s = Get-Date

$l2 = [mylinkedlist]::new("Root")

for($i = 0; $i -lt 200000;$i++){

    $l2.RootElement.LinkAfter($i)

}

((Get-Date) - $s).totalseconds

 

 

$s = Get-Date

$l3 = [collections.arraylist]@()

for($i = 0; $i -lt 200000;$i++){

    [void]($l3.Insert(0,$i))

}

((Get-Date) - $s).totalseconds

 Ennek eredménye két futási idő másodpercben:

4,2184638

25,1276776

Látható, hogy a MyLinkedList majdnem hatszor olyan gyors volt, mint az ArrayList! Pedig a drámai lassulása az ArrayList-nek csak 500 ezer elem felett kezdődik igazán.



Word To HTML Converter