PowerShell Quirks: Where-Object vs. .Where()

Heute mal etwas kleines, aber jemand hat sich darüber zwei Tage den Kopf zerbrochen. Auf ein Array von beliebigen Objekten angewendet, sollte theoretisch Where-Object äquivalent zur Methode .Where() sein, richtig?

Beinahe. Was man hierbei wissen muss, ist folgendes: .Where() erzeugt immer eine Sammlung vom Typ Collection`1. Betrachten wir mal den folgenden Code:

$a = @("1","2","3")
$b = $a.Where({$_ -eq "1"})
$c = $a | Where-Object {$_ -eq "1"}
$d = $a.Where({$_ -eq "5"})
$e = $a | Where-Object {$_ -eq "5"}

Gibt man das Ergebnis auf die Konsole aus, sieht es identisch aus: $a und $b geben jeweils „1“ aus, $c und $d jeweils nichts. Möchte man Ergebnisse jedoch weiter verarbeiten, kommt es zu Problemen, und zwar immer dann, wenn weniger als zwei Ergebnisse gefunden wurden, also gar keines oder nur eines. Bei einem Ergebnis liefert Where-Object eben dieses, .Where() hingegen ein Array mit nur einem Member. Bei Null Ergebnissen liefert Where-Object $null und .Where(), wie nicht anders zu erwarten, ein leeres Array.

Problem #1: Checken, ob es Ergebnisse gibt

Diejenigen, die gern Abkürzungen nehmen, und einfach

if ($x) {
    # Ergebnis vorhanden
} else {
    # Ergebnis nicht vorhanden
}

schreiben, sind ausnahmsweise mal im Vorteil – das funktioniert mit beiden Techniken. Hat man sich hingegen über die Jahre auf das verbindlichere

if ($null -ne $x) { 
    # Ergebnis vorhanden 
} else { 
    # Ergebnis nicht vorhanden 
}

eingeschossen, so wird man feststellen, dass bei Verwendung von .Where() immer ein Ergebnis da ist. Ist ja auch klar, ein leeres Array ist nicht $null. Der korrekte Weg, das Ergebnis zu testen, wäre daher

if ($x.Count -gt 0) { 
    # Ergebnis vorhanden 
} else { 
    # Ergebnis nicht vorhanden
}

Das funktioniert übrigens in modernen PowerShell-Versionen mit beiden Where-Varianten. Da hat auch $null implizit die .Count-Eigenschaft.

Problem #2: Auf Eigenschaften des einzigen gefundenen Objektes schreibend zugreifen

Bei einfachen Strings wie im obigen Beispiel dargestellt, äußert sich dieses Problem nicht, da alle Properties nur lesend erreichbar sind. Haben wir es aber mit veränderlichen Objekten zu tun, ändert sich das Spiel dramatisch:

$a = @(
    [PSCustomObject]@{'id' = 1;'value' = 'One'}
    [PSCustomObject]@{'id' = 2;'value' = 'Two'}
    )
$b = $a.Where({$_.id -eq 1})
$c = $a | Where-Object {$_.id -eq 1}

Auf der Konsole wird man keinen Unterschied feststellen: $b und $c sehen gleich aus, ebenso wie $b.value und $c.value. Doch beim Verändern der Eigenschaften gibt es Unterschiede: während

$c.value = 'Oans'

wunderbar und mit dem gewünschten Ergebnis funktioniert, wirft

$b.value = 'Oans'

einen Fehler, der besagt, dass es die Eigenschaft nicht gibt. Dann ist man erst mal verwirrt, liefert doch $b.value das korrekte Ergebnis. Und erst die Typenbestimmung verrät uns dann, dass wir auf ein Array schauen.

Sollte man .Where() denn überhaupt benutzen?

Auf jeden Fall! Die Filterung ist bei großen Mengen und komplexen Objekten schneller als in der Pipe, und der Code sieht professioneller aus 😉 Man sollte sich aber immer bewusst sein, dass man am Ende mit Sammlungen zu tun haben wird. Daher bei der Benutzung von .Where() folgende Strategien verwenden:

  • Mit dem Ergebnis immer wie mit einer Sammlung umgehen, also danach nicht dirket verwenden, sondern mit foreach() durchiterieren
  • Falls man aufgrund des Filters erwartet, höchstens ein Element zu finden, einfach den Index [0] nach dem .Where()-Ausdruck setzen. War das Array leer, ist das „nullte“ Element auch tatsächlich gleich $null.
  • Alternativ das Ergebnis nach Select-Object -First 1 pipen. Gleiches Resultat, ein paar CPU-Zyklen mehr verbraucht.

Happy filtering und einen schönen ersten Advent!

Ersten Kommentar schreiben

Antworten

Deine E-Mail-Adresse wird nicht veröffentlicht.


*


Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.