Dlaczego parametr zmienia wartość?

PortfelPraca z PowerShellem to swoista mieszana przewidywalnych zachowań i niespodzianek. Jak ktoś mądrzejszy kiedyś powiedział: PowerShell jest bardzo konsekwentny, chyba że akurat nie jest. O jednym z takich na pierwszy rzut oka niekonsekwentnych zachowań chciałem dziś Wam napisać. I pokazać, że w tym szaleństwie, mimo wszystko jest metoda. Przyjrzymy się temu jak zachowywać się będą zmienne przekazywane do funkcji jako parametry. Zanim jednak przejdziemy do konkretnych przypadków – musimy poświęcić parę słów zakresom w PowerShellu.

Zakresy w PowerShellu

PowerShell to język skryptowy z dynamiczną obsługą zakresów (z wyjątkiem klas). A miało być prosto. Winking smile

Zróbmy krok do tyłu. W każdym języku, czy to programowania czy to skryptowym, zwykło rozpatrywać się zależność rodzic – potomek. Jeśli mój skrypt Skrypt.ps1 wywoła funkcję Get-Foo, to funkcja staje się potomkiem mojego skryptu. I rodzic, i potomek mają swój zakres, w którym przechowują wszystkie swoje zmienne. Tata ma pensję, córka kieszonkowe.

Obsługa zakresów w gruncie rzeczy sprowadza do tego, na ile zakresy mogą się wzajemnie przenikać. W przypadku obsługi dynamicznej: potomkowie mają dostęp do zmiennych definiowanych przez rodzica. Córka (za jego zgodą, naturalnie!) wyciąga ojcu pieniądze z portfela. Tu analogia się nieco załamuje… Winking smile

W przypadku leksykalnej obsługi zakresów, potomkowie mają dostęp tylko do tego, co rodzic do nich przekaże. Portfel ojczulka pozostaje bezpieczny a córcia musi zadowolić się tym, co ojciec jej w chwili słabości przekaże.

Atak klonów

Aby uniknąć sytuacji, gdy potomek „namiesza” w „pożyczonej” zmiennej, w językach z dynamiczną obsługą zakresów zmiennych dostaje on „sklonowaną” wersję tej zmiennej. Pieniążki wyjęte z ojcowskiego portfela zmieniają się w banknoty z „Monopoly”. Wszelkie zmiany na tej zmiennej przestają mieć znaczenie w momencie, gdy potomek skończy swoje działanie:

$zmienna = 'Słowo'
function Set-Słowo {
$zmienna = 'Moje słowo...'
"Wewnątrz: $zmienna"
}
Set-Słowo
"Poza: $zmienna"
<#
Na wyjściu:
Wewnątrz: Moje słowo...
Poza: Słowo
#>
view raw DynamicznyZakres.ps1 hosted with ❤ by GitHub

Warto też wspomnieć, że poza przekazywaniem zmiennej do funkcji za pomocą tej „magii”, możemy to też uczynić za pomocą parametrów, w sposób jawny. Jawnie też możemy wynik działania „potomka” zapisać w zmiennej w zakresie rodzica:

$zmienna = 'Słowo'
function Set-Słowo {
param (
[String] $Słowo
)
"Moje $Słowo"
}
$nowaZmienna = Set-Słowo -Słowo $zmienna
"Wejście: $zmienna. Wyjście: $nowaZmienna"
<#
Na wyjściu:
Wejście: Słowo. Wyjście: Moje Słowo
#>

Taki sposób komunikacji między rodzicem i potomkiem powoduje, że świadomie „przełączamy się” na leksykalny sposób obsługi zakresów. W przypadku kodu pisanego „na kolanie” metody dynamiczne sprawdzają się doskonale. Kod dojrzalszy zwykle warto jednak przenieść na poziom wyżej. Przekazujemy więc śmiało wartość zmiennych od rodzica, do potomka, aż w pewnym momencie coś zaczyna się komplikować: zmienne u rodzica ulegają zmianie… Ki czort?

Przez wartość, przez referencję

Nasz „czort” w tej sytuacji to sposób przekazania zmiennej do potomka. Zmienne proste przekazane zostaną w postaci ich wartości. Rozsądny ojciec daje córci odliczoną gotówkę. Zmienne złożone przekazane zostaną za pomocą swojej referencji. Potomek, zyskawszy dostęp do referencji, może nie tylko skorzystać z obecnej wartości zmiennej, ale też dokonać w niej zmian. Tatuś-szaleniec daje córci swoją kartę do bankomatu z PINem.

Pierwsza grupa zmiennych/ typów jest dość liczna: mieszczą się w niej daty, liczby.

Druga grupa zawiera na ogół kolekcje: tablice skrótów, listy, ciągi znaków (będące de facto tablicami zawierającymi poszczególne litery). Zmienić możemy jedynie takie kolekcje, których wielkość nie jest ustalona (więc ciągi znaków i zwykłe tablice pozostaną nieczułe na nasze zmiany).

Jak sprawdzić, z czym mamy do czynienia? Powie nam o tym właściwość IsValueType na informacji o typie naszej zmiennej:

$zmienne = [ordered]@{
Data = Get-Date
Int = 5
Ciąg = 'mój ciąg znaków'
TablicaSkrótów = @{ a = 1; b = 2 }
Lista = [Collections.ArrayList]@(1, 2, 3)
}
foreach ($typ in $zmienne.Keys) {
$zmienna = $zmienne.$typ
if ($zmienna.GetType().IsValueType) {
"Zmienna prosta: $typ"
} else {
"Zmienna złożona: $typ"
}
}
<#
Na wyjściu:
Zmienna prosta: Data
Zmienna prosta: Int
Zmienna złożona: Ciąg
Zmienna złożona: TablicaSkrótów
Zmienna złożona: Lista
#>

Oczywiście, wiedzę tę możemy wykorzystać tam, gdzie zależy nam na tym, by nasz potomek dokonał zmian na przekazanej mu zmiennej. Dla przykładu: pomocnicza funkcja, która w oparciu o logikę w niej zawartą doda do tablicy skrótów wartość przekazaną w pozostałych parametrach:

function Add-BarInfo {
param (
[hashtable]$Bary,
[string]$Nazwa,
[Int]$IlePiw
)
if ($Bary.Contains($Nazwa)) {
$Bary.$Nazwa += $IlePiw
} else {
$Bary.$Nazwa = $IlePiw
}
}
$mojeBary = @{}
Add-BarInfo -Bary $mojeBary -Nazwa Miś -IlePiw 2
Add-BarInfo -Bary $mojeBary -Nazwa Tani -IlePiw 5
Add-BarInfo -Bary $mojeBary -Nazwa Miś -IlePiw 2
$mojeBary.Miś
<#
Na wyjściu:
4
#>

Co jednak, gdy chcemy takich niespodzianek uniknąć? Musimy sklonować przekazaną nam przez rodzica zmienną i operować na własnoręcznie stworzonym „klonie”:

$rodzic = @{
A = 1
}
function Add-Lokalnie {
param (
[hashtable]$OdRodzica
)
$potomna = $OdRodzica.Clone()
$potomna.Add(1, 2)
"Klucze lokalnie: $($potomna.Keys)"
}
"Klucze przed: $($rodzic.Keys)"
Add-Lokalnie -OdRodzica $rodzic
"Klucze po: $($rodzic.Keys)"
<#
Na wyjściu:
Klucze przed: A
Klucze lokalnie: A 1
Klucze po: A
#>

Co ciekawe: jeśli działamy w zakresach dynamicznych dokładnie to stanie się w sytuacji, gdy zechcemy sięgnąć po złożoną zmienną zdefiniowaną przez naszego rodzica. Znów „klon” będzie de facto referencją do rodzicielskiej zmiennej, więc wszelkie zmiany przetrwają zakończenie potomka. Podobne niespodzianki spotkają nas też wtedy, gdy w PowerShellu skorzystamy z klas:

class Foo {
[void] DodajElement (
[hashtable]$tablica,
[string]$klucz,
[string]$wartość
) {
$tablica.Add($klucz, $wartość)
}
}
$mojaLista = @{}
$mojeFoo = [Foo]::new()
$mojeFoo.DodajElement(
$mojaLista,
'A',
'B'
)
$mojaLista.A
<#
Na wyjściu:
B
#>
view raw ReferenceKlasy.ps1 hosted with ❤ by GitHub

Warto o tym wiedzieć, zanim zaczniemy rwać sobie włosy z głowy, gdy coś, co powinno pozostać stałe, nagle zacznie się zmieniać…

~ - autor: Bartek Bielawski w dniu 12 Maj, 2020.

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Wyloguj /  Zmień )

Zdjęcie na Google

Komentujesz korzystając z konta Google. Wyloguj /  Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Wyloguj /  Zmień )

Zdjęcie na Facebooku

Komentujesz korzystając z konta Facebook. Wyloguj /  Zmień )

Połączenie z %s

 
%d blogerów lubi to: