问题描述
我有一个将PSObject转换为哈希表的函数。功能运作良好,但是我试图理解其中的一些细微之处,无法真正抓住我的头脑。
我正在使用PowerShell Core 7.0.3
功能:
function Convert-PSObjectToHashtable
{
param (
[Parameter(ValueFromPipeline)]
$InputObject
)
process
{
if ($null -eq $InputObject) { return $null }
if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string])
{
$collection = @(
foreach ($object in $InputObject) { Convert-PSObjectToHashtable $object }
)
# buggy
#Write-Output -NoEnumerate $collection
# correct
$collection
}
elseif ($InputObject -is [psobject])
{
$hash = @{}
foreach ($property in $InputObject.PSObject.Properties)
{
$hash[$property.Name] = Convert-PSObjectToHashtable $property.Value
}
$hash
}
else
{
$InputObject
}
}
}
我执行以下代码:
$obj = "{level1: ['e','f']}"
$x = $obj | ConvertFrom-Json | Convert-PSObjectToHashtable
[Newtonsoft.Json.JsonConvert]::SerializeObject($x)
“ buggy”代码返回我:
{"level1":{"CliXml":"<Objs Version=\"1.1.0.1\" xmlns=\"http://schemas.microsoft.com/powershell/2004/04\">\r\n <Obj RefId=\"0\">\r\n <TN RefId=\"0\">\r\n <T>System.Object[]</T>\r\n <T>System.Array</T>\r\n <T>System.Object</T>\r\n </TN>\r\n <LST>\r\n <S>e</S>\r\n <S>f</S>\r\n </LST>\r\n </Obj>\r\n</Objs>"}}
正确的代码返回我:
{"level1":["e","f"]}
为什么从技术上讲,错误代码无法在PowerShell中工作,而在对象结果上看起来却等效?
谢谢!
解决方法
发生这种情况是因为PowerShell喜欢wrap things in PSObject
。
Write-Output
(以及所有其他二进制cmdlet)通过其发出标准输出的“管道”是通过强制将输入对象显式包装在PSObject
中的方式实现的。
因此,从PowerShell用户的角度来看,这两个变量具有相同的值:
$a = 1..3 |Write-Output
$b = 1..3
根据任何合理的指示,两个变量都包含一个包含整数1,2,3的数组:
PS ~> $a.GetType().Name
Object[]
PS ~> $b.GetType().Name
Object[]
PS ~> $a[0] -is [int]
True
PS ~> $a[0] -eq $b[0]
True
尽管在幕后,对象层次实际上看起来像这样:
$a = 1..3 |Write-Output
# Behaves like: @(1,3)
# Is actually: @([psobject]::new(1),[psobject]::new(2),[psobject]::(3))
$b = 1..3
# Behaves like: @(1,3)
# Is actually : @(1,3)
您可能会认为这会带来问题,但是PowerShell会花很长时间来使该包装层完全对用户隐藏。当运行时随后评估诸如$a[1]
之类的语句并找到PSObject
包装程序时,它将透明地返回基值(例如2
),就好像它是基础数组所引用的实际值一样。
但是[JsonConvert]::SerializeObject()
不是用PowerShell编写的,并且当它开始遍历对象层次结构在PowerShell语言引擎的范围之外时,它遇到了包装的PSObject
实例并选择其默认的序列化格式(CliXml
),而不是原本应视为本机JSON类型的格式。
另一方面,表达式$collection
不是二进制cmdlet,并且没有下游管道使用者,因此它的值被枚举并直接写到输出流,绕过{ {1}}包装/装箱步骤。因此,结果数组将直接引用输出值 而不是它们各自的PSObject
包装器,并且序列化将再次按预期工作。
您可以通过引用隐藏的PSObject
成员集上的ImmediateBaseObject
属性来解开对象:
psobject
请注意,每次对象经过$a = 1,2 |Write-Output
# Actual: @([psobject]::new(1),[psobject]::new(2))
$a = $a |ForEach-Object { $_.psobject.ImmediateBaseObject }
# Actual: @(1,2)
时,包装都会重新出现:
|
如果您想知道表达式是否从PowerShell中返回$a = 1,2
# Actual: @(1,2)
$a = $a |ForEach-Object { $_ }
# Actual: @([psobject]::new(1),[psobject]::new(2))
包装的对象,请将输出传递到PSObject
:
Type.GetTypeArray()