An interesting and weird thing I noticed writing PowerShell classes lines:
class A {
[object] WhereObject(){
return @(1,2) | Where-Object {$_ -gt 2}
}
[object] Where(){
return @(1,2).Where( {$_ -gt 2})
}
}
$a = new-object A
$a.WhereObject() # Throw exception Index was out of range. Must be non-negative and less than the size of the collection.
$a.Where() # Works well
It looks like it is by design. Why does it work so?
Workaround
Function which explicitly convert "empty" value to $null:
function Get-NullIfEmpty {
param(
[Parameter(ValueFromPipeline=$true)][array] $CollectionOrEmtpy
)
begin { $output = $null }
process
{
if($output -eq $null -and $CollectionOrEmtpy -ne $null){
$output = @()
}
foreach ($element in $CollectionOrEmtpy)
{
$output += $element
}
}
end { return $output }
}
In this case, the method will look like:
[object] WhereObject() {
return @(1,2) | Where-Object {$_ -gt 2} | Get-NullIfEmpty
}
I tried to return an empty array from the class method, but it is also tricky because for a regular function an empty array means "nothing" as well. If you have a call chain like method1 -> function -> method2 - method1 throw the same exception. Because the function converts an empty array to nothing.
So converting to $null is optimal in my case :)
The (PowerShell v4+) .Where()
method, which is evaluated in expression mode, always returns an instance of [System.Collections.ObjectModel.Collection[psobject]]
:
.Count
property returns 0
).By contrast, the Where-Object
cmdlet uses pipeline semantics, which implies the following output behavior:
If nothing is output (if nothing matches the filter script block), the return value is a "null collection", which is technically the [System.Management.Automation.Internal.AutomationNull]::Value
singleton.
If a single item matches, that item is output as-is.
If multiple items match and they are collected in a variable / evaluated as part of an expression, they are collected in an [object[]]
array.
As for the specific symptom - which Bruce Payette's answer has since confirmed to be a bug.
AutomationNull
) is now coerced to $null
; see the original bug report on GitHub.An internal [List[object]]
instance is used to collect the method call's output, executed via an internal pipeline. If that internal pipeline outputs "nothing" - i.e., [System.Management.Automation.Internal.AutomationNull]::Value
- no object is added to the list. However, subsequent code assumes that there is at least one object in the list and blindly accesses index 0
, causing the error at hand.
A simpler reproduction of the problem:
class A {
# Try to return [System.Management.Automation.Internal.AutomationNull]::Value
# (which is what `& {}` produces).
[object] WhereObject(){ return & {} }
}
$a = new-object A
$a.WhereObject() # Throw exception Index was out of range. Must be non-negative and less than the size of the collection.
As for the desirable behavior:
It seems that the fix will result in $null
getting output if the method's code returns the "null collection", using C#'s default-value feature - see this comment.
The .Where()
operator always returns a Collection<PSObject>
. The pipeline case however, returns nothing. This is a problem because the code that invokes the scriptblock expects there to be an object in the result List
i.e. result.Count == 1
. There are no objects in the pipeline case so you get an index-out-of-range error. So this is a bug. We should still generate an error but it should be "non-void methods must return a value" or some such. BTW - the code in question is here.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With