我有一个函数可以将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)
";童车;代码返回我:
{"level1":{"CliXml":"<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">rn <Obj RefId="0">rn <TN RefId="0">rn <T>System.Object[]</T>rn <T>System.Array</T>rn <T>System.Object</T>rn </TN>rn <LST>rn <S>e</S>rn <S>f</S>rn </LST>rn </Obj>rn</Objs>"}}
正确的代码返回给我:
{"level1":["e","f"]}
如果从技术上讲,在PowerShell中处理对象时,这些有缺陷的代码看起来是等效的,那么为什么它们不能工作呢?
谢谢!
之所以出现这种情况,是因为PowerShell喜欢将事物包装在PSObject
的s中。
";管道";CCD_ 2(和所有其他二进制cmdlet(通过其发出标准输出的方式是以强制在CCD_。
因此,从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,2,3)
# Is actually: @([psobject]::new(1),[psobject]::new(2),[psobject]::(3))
$b = 1..3
# Behaves like: @(1,2,3)
# Is actually : @(1,2,3)
你可能会认为这会带来问题,但PowerShell花了很长时间才对用户完全隐藏这个包装层。当运行时随后评估像$a[1]
这样的语句并找到PSObject
包装器时,它透明地返回基值(例如2
(,就好像它是底层数组引用的实际值一样。
但是[JsonConvert]::SerializeObject()
不是在PowerShell中编写的,当它开始在PowerShell语言引擎的范围之外遍历对象层次结构时,它会遇到包装PSObject
实例,并选择其默认序列化格式(CliXml
(,而不是本应被视为本机JSON类型的格式。
另一方面,表达式$collection
是而不是二进制cmdlet,并且没有下游管道使用者,因此它的值被枚举并直接写入输出流,从而绕过PSObject
包装/装箱步骤。因此,生成的数组直接引用输出值,而不是它们各自的PSObject
包装器,序列化再次按预期工作。
您可以通过引用隐藏的psobject
成员集上的ImmediateBaseObject
属性来打开包装对象:
$a = 1,2 |Write-Output
# Actual: @([psobject]::new(1),[psobject]::new(2))
$a = $a |ForEach-Object { $_.psobject.ImmediateBaseObject }
# Actual: @(1,2)
请注意,每次对象通过|
:时都会重新进行包装
$a = 1,2
# Actual: @(1,2)
$a = $a |ForEach-Object { $_ }
# Actual: @([psobject]::new(1),[psobject]::new(2))
如果您想知道表达式是否从PowerShell中返回PSObject
包装的对象,请将输出传递给Type.GetTypeArray()
:
PS ~> [type]::GetTypeArray(@(1..3|Write-Output)).Name
PSObject
PSObject
PSObject
PS ~> [type]::GetTypeArray(@(1..3)).Name
Int32
Int32
Int32