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

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 *


Type number two as digit *


Semhle vyplňte číslici dvě. Předchozí antispemové pole nefunguje úplně dokonale, zdá se, že jsou i spamery, které pochopily, že je občas potřeba vyplnit autora :-)

Email


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

Attachments