PSCustomObject
Collection-like behavior
Permissive mode
When not running in strict mode, most objects in PowerShell have a
Count property:
$array = @(1, 2, 3)Write-Output "Array Count: $($array.Count)"$dictionary = @{A=1; B=2; C=3; D=4}Write-Output "Dictionary Count: $($dictionary.Count)"$number = 5Write-Output "Number Count: $($number.Count)"$string = 'hello there'Write-Output "String Count: $($string.Count)"$process = (Get-Process)[0]Write-Output "Process object Count: $($process.Count)"
Array Count: 3 Dictionary Count: 4 Number Count: String Count: Process object Count:
Array Count: 3 Dictionary Count: 4 Number Count: 1 String Count: 1 Process object Count: 1
Notice that all these objects have a Count (except for
non-collections in version 2). However, PSCustomObject doesn't
have an accurate Count of 1 until version 6.1. In version 2, it
has the same count as the dictionary it was created out of.
$object = [PSCustomObject]@{A=1; B=2; C=3; D=4; E=5}Write-Output "PSCustomObject Count: $($object.Count)"
PSCustomObject Count: 5
PSCustomObject Count:
PSCustomObject Count: 1
Basically, PSCustomObject sometimes doesn't act like a collection
when other single types do until version 6.1.
We can also look at other collection invocations. The Length
property behaves the same as Count in that most objects act like
collections, but PSCustomObject doesn't start acting like a
collection until version 6.1. There's just the one difference that
in version 2, Length on PSCustomObject doesn't return the
number of properties in the object like with Count.
Naturally, other types like strings also behave differently with
Length, but we're just interested in PSCustomObject here.
$array = @(1, 2, 3)Write-Output "Array Length: $($array.Length)"$dictionary = @{A=1; B=2; C=3; D=4}Write-Output "Dictionary Length: $($dictionary.Length)"$number = 5Write-Output "Number Length: $($number.Length)"$string = 'hello there'Write-Output "String Length: $($string.Length)"$process = (Get-Process)[0]Write-Output "Process object Length: $($process.Length)"$object = [PSCustomObject]@{A=1; B=2; C=3; D=4; E=5}Write-Output "PSCustomObject Length: $($object.Length)"
Array Length: 3 Dictionary Length: Number Length: String Length: 11 Process object Length: PSCustomObject Length:
Array Length: 3 Dictionary Length: 1 Number Length: 1 String Length: 11 Process object Length: 1 PSCustomObject Length:
Array Length: 3 Dictionary Length: 1 Number Length: 1 String Length: 11 Process object Length: 1 PSCustomObject Length: 1
The ForEach method behaves similarly to Count and Length
except it throws rather than returning null. Also note that
dictionaries behave like singular objects with ForEach.
try {$array = @(1, 2, 3)$i = -1Write-Output "Array ForEach: $($array.ForEach({$i += 1; $i}))"} catch {Write-Output "Array ForEach threw: $_"}try {$dictionary = @{A=1; B=2; C=3; D=4}$i = -1Write-Output "Dictionary ForEach: $($dictionary.ForEach({$i += 1; $i}))"} catch {Write-Output "Dictionary ForEach threw: $_"}try {$number = 5$i = -1Write-Output "Number ForEach: $($number.ForEach({$i += 1; $i}))"} catch {Write-Output "Number ForEach threw: $_"}try {$string = 'hello there'$i = -1Write-Output "String ForEach: $($string.ForEach({$i += 1; $i}))"} catch {Write-Output "String ForEach threw: $_"}try {$process = (Get-Process)[0]$i = -1Write-Output "Process object ForEach: $($process.ForEach({$i += 1; $i}))"} catch {Write-Output "Process object ForEach threw: $_"}try {$object = [PSCustomObject]@{A=1; B=2; C=3; D=4; E=5}$i = -1Write-Output "PSCustomObject ForEach: $($object.ForEach({$i += 1; $i}))"} catch {Write-Output "PSCustomObject object ForEach threw: $_"}
Array ForEach threw: Method invocation failed because [System.Object[]] doesn't contain a method named 'ForEach'. Dictionary ForEach threw: Method invocation failed because [System.Collections.Hashtable] doesn't contain a method name d 'ForEach'. Number ForEach threw: Method invocation failed because [System.Int32] doesn't contain a method named 'ForEach'. String ForEach threw: Method invocation failed because [System.String] doesn't contain a method named 'ForEach'. Process object ForEach threw: Method invocation failed because [System.Diagnostics.Process] doesn't contain a method na med 'ForEach'. PSCustomObject object ForEach threw: Method invocation failed because [System.Collections.Hashtable] doesn't contain a method named 'ForEach'.
Array ForEach: 0 1 2 Dictionary ForEach: 0 Number ForEach: 0 String ForEach: 0 Process object ForEach: 0 PSCustomObject object ForEach threw: Method invocation failed because [System.Management.Automation.PSCustomObject] does not contain a method named 'ForEach'.
Array ForEach: 0 1 2 Dictionary ForEach: 0 Number ForEach: 0 String ForEach: 0 Process object ForEach: 0 PSCustomObject ForEach: 0
Zero indexing, interestingly, works in all versions 5 through 7,
whether number, process object, or PSCustomObject. In version 2,
it returns null on PSCustomObject rather than throwing like with
numbers and process objects. This matches the behavior that the
PSCustomObject cast in version 2 really just produces a
dictionary.
try {$array = @(1, 2, 3)Write-Output "Array [0]: $($array[0])"} catch {Write-Output "Array [0] threw: $_"}try {$dictionary = @{A=1; B=2; C=3; D=4}Write-Output "Dictionary [0]: $($dictionary[0])"} catch {Write-Output "Dictionary [0] threw: $_"}try {$number = 5Write-Output "Number [0]: $($number[0])"} catch {Write-Output "Number [0] threw: $_"}try {$string = 'hello there'Write-Output "String [0]: $($string[0])"} catch {Write-Output "String [0] threw: $_"}try {$process = (Get-Process)[0]Write-Output "Process object [0]: $($process[0])"} catch {Write-Output "Process object [0] threw: $_"}try {$object = [PSCustomObject]@{A=1; B=2; C=3; D=4; E=5}Write-Output "PSCustomObject [0]: $($object[0])"} catch {Write-Output "PSCustomObject [0] threw: $_"}
Array [0]: 1 Dictionary [0]: Number [0] threw: Unable to index into an object of type System.Int32. String [0]: h Process object [0] threw: Unable to index into an object of type System.Diagnostics.Process. PSCustomObject [0]:
Array [0]: 1
Dictionary [0]:
Number [0]: 5
String [0]: h
Process object [0]: System.Diagnostics.Process (conhost)
PSCustomObject [0]: @{A=1; B=2; C=3; D=4; E=5}
Strict mode
If we enable strict mode, we get some more differing behavior between versions.
In version 2, we fail to get the count on the number, string, and
process objects. Everything else is the same. In versions 5 and
6.0, we throw getting the count on everything but the actual
collections. Like when not in strict mode, we only start getting
Count on PSCustomObject starting in version 6.1. This is
interesting, however, because PSCustomObject is now the only
non-collection type which doesn't throw in strict mode.
Set-StrictMode -Version Latesttry {$array = @(1, 2, 3)Write-Output "Array Count: $($array.Count)"} catch {Write-Output "Array Count threw: $_"}try {$dictionary = @{A=1; B=2; C=3; D=4}Write-Output "Dictionary Count: $($dictionary.Count)"} catch {Write-Output "Dictionary Count threw: $_"}try {$number = 5Write-Output "Number Count: $($number.Count)"} catch {Write-Output "Number Count threw: $_"}try {$string = 'hello there'Write-Output "String Count: $($string.Count)"} catch {Write-Output "String Count threw: $_"}try {$process = (Get-Process)[0]Write-Output "Process object Count: $($process.Count)"} catch {Write-Output "Process object Count threw: $_"}try {$object = [PSCustomObject]@{A=1; B=2; C=3; D=4; E=5}Write-Output "PSCustomObject Count: $($object.Count)"} catch {Write-Output "PSCustomObject Count threw: $_"}
Array Count: 3 Dictionary Count: 4 Number Count threw: Property 'Count' cannot be found on this object. Make sure that it exists. String Count threw: Property 'Count' cannot be found on this object. Make sure that it exists. Process object Count threw: Property 'Count' cannot be found on this object. Make sure that it exists. PSCustomObject Count: 5
Array Count: 3 Dictionary Count: 4 Number Count threw: The property 'Count' cannot be found on this object. Verify that the property exists. String Count threw: The property 'Count' cannot be found on this object. Verify that the property exists. Process object Count threw: The property 'Count' cannot be found on this object. Verify that the property exists. PSCustomObject Count threw: The property 'Count' cannot be found on this object. Verify that the property exists.
Array Count: 3 Dictionary Count: 4 Number Count threw: The property 'Count' cannot be found on this object. Verify that the property exists. String Count threw: The property 'Count' cannot be found on this object. Verify that the property exists. Process object Count threw: The property 'Count' cannot be found on this object. Verify that the property exists. PSCustomObject Count: 1
We see something similar with the Length property. Up until
version 6.1, all types but arrays and strings throw. However, once
we reach 6.1, PSCustomObject now has a Length property, even in
strict mode.
Set-StrictMode -Version Latesttry {$array = @(1, 2, 3)Write-Output "Array Length: $($array.Length)"} catch {Write-Output "Array Length threw: $_"}try {$dictionary = @{A=1; B=2; C=3; D=4}Write-Output "Dictionary Length: $($dictionary.Length)"} catch {Write-Output "Dictionary Length threw: $_"}try {$number = 5Write-Output "Number Length: $($number.Length)"} catch {Write-Output "Number Length threw: $_"}try {$string = 'hello there'Write-Output "String Length: $($string.Length)"} catch {Write-Output "String Length threw: $_"}try {$process = (Get-Process)[0]Write-Output "Process object Length: $($process.Length)"} catch {Write-Output "Process object Length threw: $_"}try {$object = [PSCustomObject]@{A=1; B=2; C=3; D=4; E=5}Write-Output "PSCustomObject Length: $($object.Length)"} catch {Write-Output "PSCustomObject Length threw: $_"}
Array Length: 3 Dictionary Length threw: Property 'Length' cannot be found on this object. Make sure that it exists. Number Length threw: Property 'Length' cannot be found on this object. Make sure that it exists. String Length: 11 Process object Length threw: Property 'Length' cannot be found on this object. Make sure that it exists. PSCustomObject Length threw: Property 'Length' cannot be found on this object. Make sure that it exists.
Array Length: 3 Dictionary Length threw: The property 'Length' cannot be found on this object. Verify that the property exists. Number Length threw: The property 'Length' cannot be found on this object. Verify that the property exists. String Length: 11 Process object Length threw: The property 'Length' cannot be found on this object. Verify that the property exists. PSCustomObject Length threw: The property 'Length' cannot be found on this object. Verify that the property exists.
Array Length: 3 Dictionary Length threw: The property 'Length' cannot be found on this object. Verify that the property exists. Number Length threw: The property 'Length' cannot be found on this object. Verify that the property exists. String Length: 11 Process object Length threw: The property 'Length' cannot be found on this object. Verify that the property exists. PSCustomObject Length: 1
ForEach works on all types in versions 5 and 6.0 in strict mode,
except for PSCustomObject. In version 6.1 it works for
PSCustomObject as well. This differs from Count and Length,
in that supporting ForEach on PSCustomObject in strict mode
actually produces consistency with other types.
Set-StrictMode -Version Latesttry {$array = @(1, 2, 3)$i = -1Write-Output "Array ForEach: $($array.ForEach({$i += 1; $i}))"} catch {Write-Output "Array ForEach threw: $_"}try {$dictionary = @{A=1; B=2; C=3; D=4}$i = -1Write-Output "Dictionary ForEach: $($dictionary.ForEach({$i += 1; $i}))"} catch {Write-Output "Dictionary ForEach threw: $_"}try {$number = 5$i = -1Write-Output "Number ForEach: $($number.ForEach({$i += 1; $i}))"} catch {Write-Output "Number ForEach threw: $_"}try {$string = 'hello there'$i = -1Write-Output "String ForEach: $($string.ForEach({$i += 1; $i}))"} catch {Write-Output "String ForEach threw: $_"}try {$process = (Get-Process)[0]$i = -1Write-Output "Process object ForEach: $($process.ForEach({$i += 1; $i}))"} catch {Write-Output "Process object ForEach threw: $_"}try {$object = [PSCustomObject]@{A=1; B=2; C=3; D=4; E=5}$i = -1Write-Output "PSCustomObject ForEach: $($object.ForEach({$i += 1; $i}))"} catch {Write-Output "PSCustomObject ForEach threw: $_"}
Array ForEach threw: Method invocation failed because [System.Object[]] doesn't contain a method named 'ForEach'. Dictionary ForEach threw: Method invocation failed because [System.Collections.Hashtable] doesn't contain a method name d 'ForEach'. Number ForEach threw: Method invocation failed because [System.Int32] doesn't contain a method named 'ForEach'. String ForEach threw: Method invocation failed because [System.String] doesn't contain a method named 'ForEach'. Process object ForEach threw: Method invocation failed because [System.Diagnostics.Process] doesn't contain a method na med 'ForEach'. PSCustomObject ForEach threw: Method invocation failed because [System.Collections.Hashtable] doesn't contain a method named 'ForEach'.
Array ForEach: 0 1 2 Dictionary ForEach: 0 Number ForEach: 0 String ForEach: 0 Process object ForEach: 0 PSCustomObject ForEach threw: Method invocation failed because [System.Management.Automation.PSCustomObject] does not contain a method named 'ForEach'.
Array ForEach: 0 1 2 Dictionary ForEach: 0 Number ForEach: 0 String ForEach: 0 Process object ForEach: 0 PSCustomObject ForEach: 0
Finally, zero indexing works on all types in versions 5 through 7, even in strict mode.
Set-StrictMode -Version Latesttry {$array = @(1, 2, 3)Write-Output "Array [0]: $($array[0])"} catch {Write-Output "Array [0] threw: $_"}try {$dictionary = @{A=1; B=2; C=3; D=4}Write-Output "Dictionary [0]: $($dictionary[0])"} catch {Write-Output "Dictionary [0] threw: $_"}try {$number = 5Write-Output "Number [0]: $($number[0])"} catch {Write-Output "Number [0] threw: $_"}try {$string = 'hello there'Write-Output "String [0]: $($string[0])"} catch {Write-Output "String [0] threw: $_"}try {$process = (Get-Process)[0]Write-Output "Process object [0]: $($process[0])"} catch {Write-Output "Process object [0] threw: $_"}try {$object = [PSCustomObject]@{A=1; B=2; C=3; D=4; E=5}Write-Output "PSCustomObject [0]: $($object[0])"} catch {Write-Output "PSCustomObject [0] threw: $_"}
Array [0]: 1 Dictionary [0]: Number [0] threw: Unable to index into an object of type System.Int32. String [0]: h Process object [0] threw: Unable to index into an object of type System.Diagnostics.Process. PSCustomObject [0]:
Array [0]: 1
Dictionary [0]:
Number [0]: 5
String [0]: h
Process object [0]: System.Diagnostics.Process (conhost)
PSCustomObject [0]: @{A=1; B=2; C=3; D=4; E=5}
See also
- PSCustomObject does not have surrogate Count and Length | PowerShellTraps
- Treating scalars implicitly as collections doesn't fully work with custom objects ([pscustomobject]) - lacks a .Count property | GitHub Issue
- Treating scalars implicitly as collections doesn't work with all objects - some lack a .Count property, as do some objects that are implicitly treated as collections | GitHub Issue
- Set-Strictmode should not complain about COUNT & LENGTH properties on elements | GitHub Issue