I've been using PowerShell for a number of years, and I thought I had a handle on some of its more 'eccentric' behaviour, but I've hit an issue I can't make head nor tail of...
I've always used "return" to return values from functions, but recently I thought I'd have a look at Write-Output as an alternative. However, PowerShell being PowerShell, I've found something that doesn't seem to make sense (to me, at least):
function Invoke-X{ write-output @{ "aaa" = "bbb" } };
function Invoke-Y{ return @{ "aaa" = "bbb" } };
$x = Invoke-X;
$y = Invoke-Y;
write-host $x.GetType().FullName
write-host $y.GetType().FullName
write-host ($x -is [hashtable])
write-host ($y -is [hashtable])
write-host ($x -is [pscustomobject])
write-host ($y -is [pscustomobject])
output:
System.Collections.Hashtable
System.Collections.Hashtable
True
True
True
False
What is the difference between $x and $y (or 'write-output' and 'return') that means they're both hashtables, but only one of them '-is' a pscustomobject? And is there a generalised way I can determine the difference from code, other than obviously checking whether every hashtable I have in a variable is also a pscustomobject)?
My $PSVersionTable looks like this, in case this behaviour is specific to a particular version of PowerShell:
Name Value
---- -----
PSVersion 5.1.16299.492
PSEdition Desktop
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
BuildVersion 10.0.16299.492
CLRVersion 4.0.30319.42000
WSManStackVersion 3.0
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
Cheers,
M
return
and [pscustomobject]
are red herrings here, in a way.
What it comes down to is:
Implicit expression output vs. cmdlet -produced output; using return
(without a cmdlet call) falls into the former category, using Write-Output
into the latter.
Output objects getting wrapped in - mostly invisible - [psobject]
instances only in cmdlet -produced output.
# Expression output: NO [psobject] wrapper:
@{ "aaa" = "bbb" } -is [psobject] # -> $False
# Cmdlet-produced output: [psobject]-wrapped
(Write-Output @{ "aaa" = "bbb" }) -is [psobject] # -> $True
Note that - surprisingly - [pscustomobject]
is the same as [psobject]
: they both refer to type [System.Management.Automation.PSObject]
, which is the normally invisible helper type that PowerShell uses behind the scenes.
(To add to the confusion, there is a separate [System.Management.Automation.PSCustomObject]
type.)
For the most part, this extra [psobject]
wrapper is benign - it mostly behaves as the wrapped object would directly - but there are instances where it causes subtly different behavior (see below).
And is there a generalised way I can determine the difference from code, other than obviously checking whether every hashtable I have in a variable is also a pscustomobject
Note that a hashtable is not a PS custom object - it only appears that way for - any - [psobject]
-wrapped object due to [pscustomobject]
being the same as [psobject]
.
To detect a true PS custom object - created with [pscustomobject] @{ ... }
or New-Object PSCustomObject
/ New-Object PSObject
or produced by cmdlets such as Select-Object
and Import-Csv
- use:
$obj -is [System.Management.Automation.PSCustomObject] # NOT just [pscustomobject]!
Note that using the related -as
operator with a true PS custom object is broken as of Windows PowerShell v5.1 / PowerShell Core v6.1.0 - see below.
As an example of a situation where the extra [psobject]
wrapper is benign, you can still test even a wrapped object for its type directly:
(Write-Output @{ "aaa" = "bbb" }) -is [hashtable] # $True
That is, despite the wrapper, -is
still recognizes the wrapped type. Therefore, somewhat paradoxically, both -is [psobject]
and -is [hashtable]
return $True
in this case, even though these types are unrelated.
There is no good reason for these discrepancies and they strike me as leaky abstractions (implementations): internal constructs accidentally peeking from behind the curtain.
The following GitHub issues discuss these behaviors:
Also note that adding Write-Output
debug messages, unlike .net, will change the return type to an array. Adding write lines can break functions.
function Invoke-X {
$o1 = [pscustomobject] @{ foo = 1, 2 }
return $o1
}
function Invoke-Y {
$o1 = [pscustomobject] @{ foo = 1, 2 }
Write-Output "Debug messageY"
return $o1
}
function Invoke-Z {
$o1 = [pscustomobject] @{ foo = 1, 2 }
Write-Output "Debug messageZ"
return ,$o1
}
$x = Invoke-X;
$y = Invoke-Y;
$z = Invoke-Z;
Write-Host
Write-Host "X Type: " $x.GetType().FullName $x.foo
Write-Host
Write-Host "Y Type: " $y.GetType().FullName
Write-Host "Y0 Type: " $y[0].GetType().FullName $y[0]
Write-Host "Y1 Type: " $y[1].GetType().FullName $y[1].foo
Write-Host
Write-Host "Z Type: " $z.GetType().FullName
Write-Host "Z0 Type: " $z[0].GetType().FullName $z[0]
Write-Host "Z1 Type: " $z[1].GetType().FullName $z[1].foo
Gives:
X Type: System.Management.Automation.PSCustomObject 1 2 Y Type: System.Object[] Y0 Type: System.String Debug messageY Y1 Type: System.Management.Automation.PSCustomObject 1 2 Z Type: System.Object[] Z0 Type: System.String Debug messageZ Z1 Type: System.Management.Automation.PSCustomObject 1 2
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.