$null czy nie $null, o to jest pytanie

jak sprawdzić czy zmienna została prawidłowo zainicjowana czy może jest wyzerowana? sposobów jest oczywiście kilka, zależnie od kontekstu. ale ponieważ trafiłem na brzegową sytuację, kilka ciekawostek…

pierwsze pytanie – co to znaczy 'wyzerowana zmienna’? od razu przychodzi mi do głowy pytanie-killer, dobre na sprawdzenie sprytu początqjącego deva – 'ile wartości można zakodować na jedno-bitowej zmiennej?’. dość oczywistą odpowiedzią jest 'dwie – 0 i 1′, ale prawidłową jest 'trzy – zero, jeden i null’. [oczywiście przykład wyssany z palca i można z tym polemizować, ale chodzi o ideę]. pokazuje to pewien niuans w sprawdzaniu wartości zmiennej, który może nieźle namieszać – zmienna pusta [empty] to nie to samo co zainicjowana zmienna z wartością $null [o ile $null można nazwać wartością, ale nie chodzi o filozofowanie tylko praktyczne konsekwencje].

początkowo zatem używałem prostego porównania:

if($zmienna -eq $null) { }

potem dowiedziałem się, że $null powinien być zawsze po lewej stronie, ponieważ w specyficznych przypadkach zachowa się 'nieoczekiwanie’ – polecam poczytać na docsach. dla tego poprawnym zapisem jest:

if($null -eq $zmienna) {}

przeważnie jednak, ze względu na logikę działania skryptów, chcę sprawdzić czy coś nie jest $null LUB puste. zacząłem więc przez długi czas używać magii PowerShell:

if(-not $zmienna) { }

co w zasadzie powinno uniwersalnie sprawdzić zmienną. tego zapisu nie używam od dłuższego czasu – nie pamiętam jednak dokładnie jakie tutaj występowały aberracje, ale z jakiegoś powodu przesiadłem się na natywną funkcję .NET dla klasy string:

[string]::isNullOrEmpty($zmienna)

i bardzo długo tą funkcję uważałem za uniwersalny tester i myślałem że znalazłem moje panaceum… do czasu aż nie trafiłem na kolejne brzegowe sytuacje, które nieoczekiwanie zwracają głupotę. najpierw przykład oczywisty, tutaj nie ma co się dziwić:

PS> $emptyArray=@()
PS> $emptyArray.count
0
PS> [string]::isNullOrEmpty($emptyArray)
True

kolejny nadal dość oczywisty przykład, choć warto zwrócić uwagę na olbrzymią różnię pomiędzy 'tablicą pustą’ z poprzedniego przykładu, a tablicą zawierającą element $null – zwróć uwagę na ilość elementów w tablicy:

PS> $nullArray=@($null)
PS> $nullArray.count
1
PS> [string]::isNullOrEmpty($nullArray)
True

i tu doszedłem do PS Borderlands – granic swojej wiedzy i możliwości zrozumienia mechanizmów pod spodem. dalszych przykładów nie potrafię wyjaśnić – znam je z obserwacji:

PS> $nullArray=@($null,$null)
PS> $nullArray.count
2
PS> [string]::isNullOrEmpty($nullArray)
False

wychodzi na to że jedno nic to nadal nic, ale dwa nic to już coś. miałem na studiach matematykę nieskończoności, ale nigdy matematyki nicości, najwyraźniej muszę tą dziedzinę lepiej zgłębić (;

a dalej… to już kosmos. przypadek tak zawikłany, że zajęło mi masę czasu póki go wyizolowałem i umiałem pokazać na przykładzie. w przeszłości również trafiałem na podobne zachowanie, więc scenariuszy jest prawdopodobnie więcej, ale dopiero teraz postanowiłem to zrozumieć. opiszę, może znajdzie się harpagan, który to rozwikła…

poza granicami

jeśli funkcja implementuje 'ValueFromRemainingArguments’, typ zwracanego argumentu będzie zależny od tego czy się z tej funkcjonalności skorzysta. tak działa między innymi 'write-host’ – wywołanie polecenia:

PS> write-host tu sobie cos tam napiszemy bez zadnych cudzyslowiow
tu sobie cos tam napiszemy bez zadnych cudzyslowiow

bez cudzysłowiów – czyli de fakto polecenie powinno potraktować każdy wyraz jako oddzielą wartość kolejnego parametru. a jednak zinterpretował to jako pojedynczą wiadomość. to właśnie instrukcja ValueFromRemainingArguments upakowała wszystkie nienazwane wartości pod jedną zmienną. podobny myk stosuję w swoim write-log dzięki czemu mogę uzyskać podobny efekt. na tej też funkcji mogłem zbadać, że przy takim wywołaniu:

PS> write-log -message "to cos"
to cos

zmienna $message będzie typu [string]. ale przy takich wywołaniach:

PS> write-log "to cos"
to cos
PS> write-log to inne cos
to inne cos

zmienna $message jest typu [`list] [nie pytajcie skąd ten backtick w nazwie typu – bardzo dziwaczny to zapis, dodatkowa przyprawa w tym i tak dziwnym przypadq]. to pierwsza ważna różnica, ponieważ problem pojawia się właśnie przy tym typie. a że zazwyczaj ułatwiamy sobie życie i jeśli można ’-message’ nie pisać, to go nie piszemy, scenariusz więc dość codzienny.

jeśli jako wiadomość przekaże się obiekt, to zostanie stworzona lista zawierająca obiekty. można to zasymulować tak:

PS> $przekazaneWartosci = new-object System.Collections.Generic.List[system.object]
PS> $mojObiekt = [PSCustomObject] @{page='w-files.pl';proto='https'}
PS> $mojObiekt

page proto
---- -----
w-files.pl https

PS> $przekazaneWartosci.add($mojObiekt)
PS> [string]::isNullOrEmpty($przekazaneWartosci)
True

jeśli obiekt wrzucony do listy jest typu PSCustomObject – nawet jeśli nie jest pusty, funkcja zwraca informację, że lista jest pusta. można by powiedzieć – 'no ale panie, to jest metoda dla stringa, a tutaj masz obiekt, to pewnie dlatego’. tyle, że sprawdzałem na wielu innych typach obiektów – i zachowuje się prawidłowo. póki co ten bug wydaje się działać tylko dla PSCustomObject (-> update dalej). już nawet taka zmiana w deklaracji, z custom na generic:

$myobj = [PSObject] @{page='w-files.pl';proto='https'}

spowoduje, że będzie działać prawidłowo. różnica pomiędzy PSObject a PSCustomObject mogłoby być też fajnym wpisem PS Borderlands i straciłem kiedyś sporo czasu na braq spostrzegawczości, że jest inna deklaracja… ale to nie teraz. dodatkowego smaczq dodaje fakt, że nie do końca wiadomo, kiedy coś zostanie przetworzone na PSCustomObject. ot proszę przykład:

PS> (get-process).gettype()

IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array

PS> (get-process|select processname).gettype()

IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array

PS> $ou='OU=borderlands,DC=w-files,DC=pl'
PS> (Get-ADOrganizationalUnit -Identity $ou).gettype()

IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False ADOrganizationalUnit Microsoft.ActiveDirectory.Management.ADObject

PS C:\CloudTeam-ScriptLAB> (Get-ADOrganizationalUnit -Identity $ou|select name).gettype()

IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False PSCustomObject System.Object

z jakiegoś powodu filtrowanie niektórych typów obiektów zmienia ich typ – kolejna zagadka, już poza granicami zrozumienia, ponieważ to nie jest normalne zachowanie.

update

dopiero co skończyłem ten wpis i zabrałem się za kolejny skrypt. kolejny przykład obiektu, dla którego [string]::isNullOrEmpty zwraca głupotę – i to dużo prostszy. wiedziałem, że trafiałem na więcej takich przypadqw!

PS> $storageAccountList = get-AzStorageAccount -resourceGroupName 'myRG'
PS> $storageAccountList

StorageAccountName ResourceGroupName PrimaryLocation SkuName Kind AccessTier CreationTime Provisioning State
------------------ ----------------- --------------- ------- ---- ---------- ------------ ------------
nexortestsa myRG westeurope Standard_LRS StorageV2 Hot 07.05.2021 10:56:16 Succeeded

PS> $storageAccountList.GetType()

IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False PSStorageAccount System.Object

PS> [string]::IsNullOrEmpty($storageAccountList)
True

podsumowanie

cała informatyka opiera się na determiniźmie [no w każdym razie do czasu pojawienia się AI/ML, które jest w IT odpowiednikiem fizyki kwantowej w fizyce (; ]. brak determinizmu powoduje, że trzeba się nieźle napocić żeby wyłapać błędy i prawidłowo je obsłużyć.

strasznie mnie drażni, kiedy nie potrafię czegoś zrozumieć. takie 'niuanse’ to jakieś górne wartości zasady pareto – ten promil promila całego systemu, na który trzeba poświęcić mnóstwo, mnóstwo czasu, żeby w ogóle go uchwycić. nie wspominając o zrozumieniu. w tym przypadq – to jest kilka brzegowych sytuacji, które nakładają się na siebie. istna qmulacja. IMHO to nie jest coś 'do zrozumienia’ a bardziej błędy w implementacji, ale póki co – wpada do teczki w-Files.

eN.

 

-o((:: sprEad the l0ve ::))o-

Comments (2)

  1. Grzegorz Hejman

    Odpowiedz

    Bardzo fajny wpis. Oczywiście wszystko co dotyczy komputerów jest zupełnie deterministyczne. Przeważnie trochę skomplikowane, ale jednak deterministyczne. Czasami, zresztą jest to właśnie przyczyną nieporozumień i problemów. Komputery nie potrafią „zrozumieć” jaka była intencja autora i wykonują polecenia bardzo dokładnie – bardzo często wbrew intencji tego autora ; )
    Przedstawionych przykładów nie traktowałbym jako błędów w implementacji, a raczej właśnie niuansów implementacji, które nie każdy zna.
    Dla przykładu:

    1. Testowanie czegoś funkcją [string]::isNullOrEmpty($zmienna) wymusza konwersję $zmienna na typ string. Należy zatem znać zasady konwersji typu $zmienna na typ String, aby uniknąć zaskoczenia. W wypadku gdy $zmienna jest tablicą (Array) konwersja do typu String tworzy string złożony z elementów tablicy rozdzielonych stringiem, który znajduje się w zmiennej $OFS (output field separator).
    W przypadku, gdy tablica zawiera jeden element, string będzie zawierał tylko ten jeden element.

    PS> [string]@(‘cos’)
    cos
    PS> [string]@(‘cos’, ‘cos’)
    cos cos

    Domyślnie $OFS to whitespace (kod 32), ale można ją zmienić na cokolwiek:

    PS> $OFS = „-”
    PS> [string]@(‘cos’, ‘cos’)
    cos-cos

    Wracając do przykładu [string]@($null) zwraca string z jednym elementem $null – czyli „prawdziwy” $null, ale [string]@($null, $null) zwróci już string zawierający $OFS (domyślnie spację), który rozdziela oba $null’e . Jeśli spróbujemy dodać kolejny $null to otrzymamy string zawierający dwie spacje itd.
    Dlatego funkcja IsNullorEmpty przy jednym $null zwraca True, a przy 2 lub więcej $null już nie, bo po konwersji mamy „prawdziwy” string z jedną, lub więcej spacją (zależnie od ilości $nulli).
    Można to łatwo sprawdzić:

    PS> [int]([string]@($null, $null))[0]
    32
    Wychodzi więc (na szczęście) na to, że jedno nic i dwa nic to ciągle nic, ale liczy się to co pomiędzy ; )

    2. Zapis [list`1] to zapis listy typu generic. Jedynka oznacza że posiada jeden parametr określający typ elementu np. System.Collections.Generic.List[int]. Może być też więcej np. typ [system.collections.generic.dictionary[string,int]] będzie miał nazwę [dictionary`2], bo słownik jest zdefiniowany jako posiadający klucze typu string i wartości typu int.

    3. Przypadek z [PSObject] i [PSCustomObject] znowu łatwo porównać sprawdzając jak zachowują się oba typy wykonując na nich funkcję ToString(), która jest wykorzystywana przy konwersji w [string]::isNullOrEmpty($przekazaneWartosci). Dla [PSCustomObject] funkcja ToString() zwraca pusty string (string o długości 0), natomiast dla [PSObject] – tak jak dla większości typów, które nie definiują jej bezpośrednio – zwraca string z nazwą typu:

    PS> $myobj.ToString()
    System.Collections.Hashtable

    Widać więc, że [string]::isNullOrEmpty poprawnie interpretuje te wyniki – true dla [PSCustomObject] i false dla [PSObject]

    4. Jeśli chodzi o zmianę typu na PSCustomObject przy filtrowaniu obiektów to proszę zwrócić uwagę, że w przykładzie

    get-process | select processname

    wynik jest cały czas tablicą obiektów (w tym wypadku stringów), nie zmienia się zatem typ wyniku (get-process również zwraca tablicę obiektów). Natomiast w przykładzie

    Get-ADOrganizationalUnit -Identity $ou | select name

    zwracany jest jeden obiekt (PSCustomObject) stworzony z property ‘name’ jednego obiektu typu Microsoft.ActiveDirectory.Management.ADObject
    Dla Get-Process możemy zrobić to samo:

    PS> ((get-process)[0] | select name).GetType().name
    PSCustomObject

    Jak widać, gdy wybieramy property z jednego obiektu tutaj również stworzony został PSCustomObject zawierający to jedno wybrane property.

    5. Ostatni przykład ze Storage Account jest podobny do punktu nr. 3. Funkcja ToString() dla typu PSStorageAccount zwraca pusty string i dlatego [string]::isNullOrEmpty prawidłowo zwraca wartość True.

    Uff, opisałem się, ale mam nadzieję, że rzuciłem trochę światła na ciemne zakamarki PowerShella. Z mojej strony mogę dodać, że czasami przeprowadzając rozmowy rekrutacyjne prosiłem kandydatów o wytłumaczenie dlaczego:

    PS> 2 + „2”
    4
    Ale:
    PS> „2” + 2
    22

    Wiem, że nie jest to zbyt skomplikowane, ale pozwala dość szybko zorientować się, czy kandydat nie tylko zna cmdlety PowerShellowe, ale również rozumie jak działa PowerShell na trochę innym poziomie.

    Reasumując – może kiedyś nadejdzie ten dzień, gdy maszyny wymkną się z kajdan determinizmu, ale to jeszcze nie ten dzień ; )

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Time limit is exhausted. Please reload CAPTCHA.