Jak už jsem tu psal minule, PowerShell je sice pěkná věcička, ale někdy se z toho můžete opravdu posráááát. Jako třeba testování nějaké proměnné na hodnotu $null. Rok a půl jsem to řešil všelijak, asi tak pětkrát jsem to předělával a pokaždé to zase na pár měsíců vydrželo, ale vždycky se vyskytlo něco, kde to nefungovalo.
Tak jsem to snad už konečně přemohl doufám definitivně.
Porovnávání na $null
Porovnání hodnoty proměnné na $null je základní programátorská akce, která se v sofistikovanějších programech o více než jednom řádku vyskytuje s pravděpodobností 99,999999%. Člověk by řekl, že když píšete jenom do příkazovky a používáte pajpu, že to není potřeba. A ouha. Ono totiž i když třeba pajpnete něco, co má hodnotu $null do ForEach-Object, tak se to, světe div se, provede:
$kdeNicNeni = $null
$kdeNicNeni | % { echo 'stejne se to vypise' }
Takže si pojďme vysvětlit, kde to soudruzi z júesej podělali a později také, jak jsem to, na šestý pokus, snad už konečně vyřešil.
Pro jednoduché proměnné se to chová korektně:
$prazdno = $null
$prazdno -eq $null
Blbé je, že pro kolekce, které jde projet (asi IEnumerable) se to chová jako debil:
[System.Collections.ArrayList] $nulovyList = $null
$nulovyList -eq $null
[System.Collections.ArrayList] $listBezPrvku = @()
$listBezPrvku -eq $null
[System.Collections.ArrayList] $listSPrvky = @('a', 'b', 'c')
$listSPrvky -eq $null
Když to předchozí vyzkoušíte, zjistíte, že $true nebo $false to vrátí pouze v prvním případě. V dalších to na první pohled nevrátí vůbec nic. Ono to ve skutečnosti totiž vrací pole, které obsahuje jen tolik prvků, kolik jich v původním poli splňuje zadanou podmínku:
[System.Collections.ArrayList] $listSPrvkyANulami = @('a', 'b', $null, 'c', $null, 'd')
$listSPrvkyANulami.Count
$vysledekPorovnani = $listSPrvkyANulami -eq $null
$vysledekPorovnani
Get-Member -i $vysledekPorovnani
$vysledekPorovnani.Count
V předchozím je vidět, že $listSPrvkyANulami má 6 prvků, z čehož jsou dvakrát $null. Když tedy tuto kolekci porovnáte na $null, výsledek v proměnné $vysledekPorovnani se vůbec nezobrazí, ale to jen na oko. Když zkusíte zjistit, co to je vlastně za typ, tak se ukáže, že to je ve skutečnosti pole typu [object[]], které má dva prvky. Tzn. dva podle toho, že právě dva prvky $listSPrvkyANulami splnily naši porovnávací podmínku.
Kdyby ty prvky byly aspoň $true. Ale ony jsou $null.
Už z toho serete krev? Já jo.
Kdosi radil, že by se dalo používat například toto:
[System.Collections.ArrayList] $listSPrvkyANulami = @('a', 'b', $null, 'c', $null, 'd')
-not $listSPrvkyANulami
[System.Collections.ArrayList] $nulovyList = $null
-not $nulovyList
[System.Collections.ArrayList] $listBezPrvku = @()
-not $listBezPrvku
A zase fekál. V prvních dvou případech to vypadá nadějně, že to snad bude nakonec fungovat. Nakonec to fungovat nebude, protože to splňuje i inicializované byť prázdné pole. Takže zase na nic.
Jak jsem to řešil dlouho a dlouho to fungovalo, dokud...
Řešil jsem to tímhle, upozorňuju že špatným, kódem, protože se mi s tím nechtělo nikdy drbat a chtěl jsem to mít co nejrychleji z krku:
function Is-Null ([object] $object)
{
[bool] $res = $false
$testNull = $object -eq $null
if ($testNull -is [bool]) {
$res = $testNull
} else { # elseif ($testNull -is [object[]]) {
$res = $object.Count -eq $null
}
return $res
}
Prostě jsem to zkusil porovnat, a pokud z toho nevypadl rovnou [bool], tak jsem zkoumal, jestli ta zdrojová kolekce obsahuje vůbec Count nebo neobsahuje. Pokud neobsahovala, a protože to byla kolekce, tak to znamenalo, že ta kolekce byla $null. Tohle fungovalo v mém projektu s 16 000 řádků až do dneška. Dneska jsem začal testovat SqlDataReader a skončil na tom, že tenhle idiot je sice kolekce, ale Count to nemá ani když obsahuje 30 000 položek.
Jak relativně korektně porovnávat na $null
Nedá se nic dělat, musíme jít rovnou do C#. Použil jsem inline C# kód, udělal si na to statickou třídu NullTester a do ní statickou metodu IsNull. Program vypadá následovně - ale pořáááád to ještě není úplný konec příběhu:
function global:Define-NullTester ()
{
$nullTesterClass = @"
public static class NullTester
{
public static bool IsNull (object ObjectToTest)
{
return (ObjectToTest == null);
}
}
"@
if (-not ('NullTester' -as [type])) {
Add-Type -TypeDefinition $nullTesterClass
} else {
# The type already exists. Skipping.
}
}
function global:Is-Null ([object] $object)
{
return [NullTester]::IsNull($object)
}
Define-NullTester
[System.Collections.ArrayList] $listSPrvkyANulami = @('a', 'b', $null, 'c', $null, 'd')
Is-Null $listSPrvkyANulami
[System.Collections.ArrayList] $nulovyList = $null
Is-Null $nulovyList
[System.Collections.ArrayList] $listBezPrvku = @()
Is-Null $listBezPrvku
Jenže tohle taky ještě není úplně ono. Panebože? No problém je v PowerShell 2.0 (verze PowerShell 3.0 už to nedělá) při porovnávání [ref] proměnných. V takovém případě to vrací chybu: "Argument 1 should not be a System.Management.Automation.PSReference. Do not use [ref]."
Zřejmě PowerShell 2.0 neumí posílat [ref] hodnoty do inline C# funkcí. Takže jsem to stejně musel ještě trošku ošulit. Zatím to funguje. Uvidíme, na co narazím za pár měsíců.
Finální řešení funkce Is-Null, která snad už funguje
function global:Define-NullTester ()
{
$nullTesterClass = @"
public static class NullTester
{
public static bool IsNull (object ObjectToTest)
{
return (ObjectToTest == null);
}
}
"@
if (-not ('NullTester' -as [type])) {
Add-Type -TypeDefinition $nullTesterClass
} else {
# The type already exists. Skipping.
}
}
function global:Is-Null ([object] $object)
{
if ($object -is [System.Management.Automation.PSReference]) {
return ($object -eq $null)
} else {
return [NullTester]::IsNull($object)
}
}
Define-NullTester
[System.Collections.ArrayList] $listSPrvkyANulami = @('a', 'b', $null, 'c', $null, 'd')
Is-Null $listSPrvkyANulami
[System.Collections.ArrayList] $nulovyList = $null
Is-Null $nulovyList
[System.Collections.ArrayList] $listBezPrvku = @()
Is-Null $listBezPrvku
Is-Null ([ref] $listSPrvkyANulami)