3

I recently made a mistake where I had an array of objects, and I accidentally did a boolean evaluation of a property against the array, but I meant to do it against a single object within that array. I only found my error during testing because I was getting a $TRUE value when I expected $FALSE. I've created a small script illustrating the situation. What I don't understand is why this nonexistent array property always evaluates as true?

# Set-StrictMode -Version Latest; # Catch dumb programmer mistakes.
$PSVersionTable.PSVersion
Write-Output "`n"

$coArray = @()
foreach ($id in 0,1)
  {
    $cObj = New-Object PSobject
    $cObj | Add-Member -type NoteProperty -name ID   -value $id
    $cObj | Add-Member -type NoteProperty -name Bool -value $false
    $coArray += $cObj
  }

foreach ($cObj in $coArray)
  {
    # What I meant to do...

    if ($cObj.Bool)     { Write-Output 'ON OBJECT: Bool is true'  }
    else                { Write-Output 'ON OBJECT: Bool is false' }

    # What I actually did...

    if ($coArray.Bool)  { Write-Output 'ON ARRAY: Bool is true'   }
    else                { Write-Output 'ON ARRAY: Bool is false'  }

    # What if there is no such object property?

    if ($coArray.Bogus) { Write-Output 'ON ARRAY: Bogus is true'  }
    else                { Write-Output 'ON ARRAY: Bogus is false' }
  }

Here is the output:

Major  Minor  Build  Revision
-----  -----  -----  --------
5      1      14393  2339


ON OBJECT: Bool is false
ON ARRAY: Bool is true
ON ARRAY: Bogus is true
ON OBJECT: Bool is false
ON ARRAY: Bool is true
ON ARRAY: Bogus is true

At first I thought it was returning true for the misplaced existing object property because it was confirming that at least one of the objects in the array had that property. But if you turn off strict and reference a totally bogus property, as shown above, it still returns true.

KillerRabbit
  • 173
  • 1
  • 8

2 Answers2

2

Hmmm...So Here is what I think. I think it is an effect of the way Powershell now unrolls properties on array objects. Powershell definitely is truthy.

The first test if ($cObj.Bool) does work the way one would expect. The second test if ($coArray.Bool) causes powershell to create a new array that has just the contents of that property from each object. This can be shown by:

C:\Users\Rob> $coArray.bool
False
False

So in truthy Powershell world this is a thing that does exist so it is true.

The third test if ($coArray.Bogus) pretty much does the same thing. This can be shown by:

Get-Member -InputObject $coArray.bogus
  TypeName: System.Object[]

Name           MemberType            Definition
----           ----------            ----------
Count          AliasProperty         Count = Length
Add            Method                int IList.Add(System.Object value)
Address        Method                System.Object&, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a...
Clear          Method                void IList.Clear()
Clone          Method                System.Object Clone(), System.Object ICloneable.Clone()
CompareTo      Method                int IStructuralComparable.CompareTo(System.Object other, System.Collections.ICo...
Contains       Method                bool IList.Contains(System.Object value)
CopyTo         Method                void CopyTo(array array, int index), void CopyTo(array array, long index), void...
Equals         Method                bool Equals(System.Object obj), bool IStructuralEquatable.Equals(System.Object ...
Get            Method                System.Object Get(int )
GetEnumerator  Method                System.Collections.IEnumerator GetEnumerator(), System.Collections.IEnumerator ...
GetHashCode    Method                int GetHashCode(), int IStructuralEquatable.GetHashCode(System.Collections.IEqu...
GetLength      Method                int GetLength(int dimension)
GetLongLength  Method                long GetLongLength(int dimension)
GetLowerBound  Method                int GetLowerBound(int dimension)
GetType        Method                type GetType()
GetUpperBound  Method                int GetUpperBound(int dimension)
GetValue       Method                System.Object GetValue(Params int[] indices), System.Object GetValue(int index)...
IndexOf        Method                int IList.IndexOf(System.Object value)
Initialize     Method                void Initialize()
Insert         Method                void IList.Insert(int index, System.Object value)
Remove         Method                void IList.Remove(System.Object value)
RemoveAt       Method                void IList.RemoveAt(int index)
Set            Method                void Set(int , System.Object )
SetValue       Method                void SetValue(System.Object value, int index), void SetValue(System.Object valu...
ToString       Method                string ToString()
Item           ParameterizedProperty System.Object IList.Item(int index) {get;set;}
IsFixedSize    Property              bool IsFixedSize {get;}
IsReadOnly     Property              bool IsReadOnly {get;}
IsSynchronized Property              bool IsSynchronized {get;}
Length         Property              int Length {get;}
LongLength     Property              long LongLength {get;}
Rank           Property              int Rank {get;}
SyncRoot       Property              System.Object SyncRoot {get;}

So as pointed out, just doing one object in the array causes all the tests to be false. This is because with just one item in the array means there is only one property for it to unroll. Powershell unrolls this to a singleton object not an array of values. So if there is no property with that name the singleton is null. See here:

C:\Users\Rob> $b = @((New-Object PSCustomObject -Property @{'notBogus'='foo'}))
C:\Users\Rob> $b

notBogus
--------
foo

C:\Users\Rob> gm -in $b.Notbogus


   TypeName: System.String

Name             MemberType            Definition
----             ----------            ----------
Clone            Method                System.Object Clone(), System.Object ICloneable.Clone()
CompareTo        Method                int CompareTo(System.Object value), int CompareTo(string strB), int IComparab...
Contains         Method                bool Contains(string value)
CopyTo           Method                void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int co...
EndsWith         Method                bool EndsWith(string value), bool EndsWith(string value, System.StringCompari...
.
.
.

C:\Users\Rob> $b.bogus -eq $null
True

So as you can see, $b.NotBogus is simply a single value so it would test appropriately.

Still not sure about the counter example that @tessellatingHeckler presented. I'll keep looking.

EBGreen
  • 36,735
  • 12
  • 65
  • 85
  • But if you set the loop to create just one object, then all the tests return false. – KillerRabbit Jul 09 '18 at 23:42
  • 1
    if you `$a = get-childitem` (in a folder with some things in it), then `if ($a.bogus) { "yes" }` does nothing, suggesting it's not just loop unrolling, but maybe specific to pscustomobjects ..? – TessellatingHeckler Jul 09 '18 at 23:43
  • 1
    Hmmm...I think it may in some way related to PSCustomObjects. I'm not sure how though. I'll keep looking. – EBGreen Jul 09 '18 at 23:46
1

Let's start with the following fundamental behaviors:

  • PSv3 introduced member-access enumeration, the ability to access a property directly on a collection (array) and have an array of the elements' property values returned.

    • ([pscustomobject] @{foo=1}, [pscustomobject] @{foo=2}).foo returns the array of .foo values, i.e., 1, 2.
  • Unless Set-StrictMode -Version 2 or higher is in effect, accessing a nonexistent property on an object returns $null.

    • $null -eq ($PSVersionTable).Bogus yields $True
  • An array with 2 or more elements is always truthy (evaluates to $True in a Boolean context), irrespective of the element values, e.g., even if they are all $null.

    • [bool] ($null, $null) yields $True

Therefore, given that your $coArray array has 2 elements, even accessing a bogus property is truthy, because if ($coArray.Bogus) is effectively the same as if ($null, $null) and ($null, $null) is truthy, as stated:

if ($null, $null) { 'true' }  # -> 'true', because the conditional is a 2+-element array

Curiously, however, returning an array of $nulls for a bogus property seems to apply to only to custom objects ([pscustomobject] instances):

# Accessing a nonexistent property on a collection of *custom objects*
# returns a $null for each element:
PS> ([pscustomobject] @{foo=1}, [pscustomobject] @{foo=2}).Bogus.GetType().Name
Object[]  # 2-element array whose elements are $null

# By contrast, *other types* seem to evaluate to a $null *scalar* (just $null),
# as evidenced by the .GetType() call failing:
PS> ($PSVersionTable, $PSVersionTable).Bogus.GetType().Name
You cannot call a method on a null-valued expression.

This inconsistency is discussed in GitHub issue #7261.

mklement0
  • 382,024
  • 64
  • 607
  • 775