Skip Ribbon Commands
Skip to main content

Ondrej Sevecek's Blog

:

Engineering and troubleshooting by Directory Master!
Ondrej Sevecek's Blog > Posts > Nesnesitelné porovnávání na $null a co s tím můžete udělat
leden 16
Nesnesitelné porovnávání na $null a co s tím můžete udělat

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)

 

Comments

There are no comments for this post.

Add Comment

Sorry comments are disable due to the constant load of spam *


Omlouvám se, ale příval spamu nelze kontrolovat, takže mi prosím pošlete email, pokud máte nějaký dotaz, nebo připomínku.

Title


Pole Title nemusíte vyplňovat, doplní se to samo na stejnou hodnotu jako je nadpis článku.

Author *


Pole Author nesmí být stejné jako pole Title! Mám to tu jako ochranu proti spamu. Roboti to nevyplní dobře :-)

Body *


Email


Emailová adresa, pokud na ni chcete ode mě dostat odpověď. Nikdo jiný než já vaši emailovou adresu neuvidí.

Attachments