PowerShell closure

Eddig úgy gondoltam, a PowerShell Script Blockjai pontosan ugyanúgy viselkednek, mint bármilyen más nyelv closure-jei. Azonban belefutottam egy kódba ami teljesen máshogy viselkedett, mint amire számítottam. Rá kellett szánnom némi időt, mire sikerült megértenem, mi is történik valójában.

A problémás kód leegyszerűsítve:

function foo {
    param ( [ScriptBlock] $Callback )

    'foo'
    & $Callback
}

function bar {
    param ( [ScriptBlock] $Callback )

    'bar'
    foo {
        'inner script block'
        & $Callback
    }
}

Kimenet:

PS> foo { 'outer script block' }
foo
test
PS> bar { 'outer script block' }
bar
foo
inner script block
inner script block
inner script block

Vagyis bar végtelen rekurzióra fut, mert $Callback értéke a foo-nak átadott paraméter lesz, nem pedig a bar-nak átadott, azaz a script block foo-ban önmagát hívja. Nem igazán értettem ezt a viselkedést, mégis hogy láthatja a bar-ban definiált script block a még meg sem hívott foo paraméterét?

A dokumentációból kiderült, hogy a függvények és script blockok egyaránt saját scope-ban futnak, ahogy arra számítottam:

Functions normally create a scope. The items created in a function, such as variables, exist only in the function scope.

Like Invoke-Command, the call operator executes the script block in a child scope.

Első ránézésre a PowerShell scope-jai is hasonlóan működnek mint más nyelveken.

Mivel a dokumentációból nem lettem okosabb, írtam pár tesztet.

Az első 3 teszt egybevág a dokumentációban leírtakkal, azaz a parent scope változói a child scopeban láthatóak, de nem módosíthatóak, hanem eltakarhatóak. Azonban a negyedik teszt már meglepő, ezek szerint nem a függvénydefiníció helye számít, hanem a hívás helye. Ugyanez igaz a script blockokra is, ez a második 4 teszt.

Ennek a viselkedésnek az elkerülésére több megoldás is lehetséges, ezekből kettőt próbáltam ki.

Az egyik megoldás, hogy a script blockból valódi closure-t lehet csinálni a GetNewClosure() metódussal. Ezzel a bar függvény így módosul:

function bar {
    param ( [ScriptBlock] $Callback )

    'bar'
    foo {
        'inner script block'
        & $Callback
    }.GetNewClosure()
}

Kimenet:

PS> bar { 'outer script block' }
bar
foo
inner script block
outer script block

A másik megoldás a private módosító használata. Ezzel foo függvény így módosul:

function foo {
    param ( [ScriptBlock] $Private:Callback )

    'foo'
    & $Callback
}

Kimenet:

PS> bar { 'outer script block' }
bar
foo
inner script block
outer script block

A tanulság az, hogy minden nyelvnek vannak elsőre meglepő tulajdonságai, ezért soha nem szabad azt feltételezni, hogy egy nyelv adott funkcionalitása pontosan ugyanúgy működik mint más nyelvek hasonló funkcionalitása. A meglepő viselkedésnek érdemes utánajárni, kipróbálni, hogy legközelebb már ne legyen min meglepődni. Szerencsés esetben olyan technikákra is rá lehet lelni, amik más problémák megoldásában is segíthetnek.

Hozzászólások