select cmdlet 不接受计算属性中生成的字符串名称

问题描述

我想生成下表:

AAA BBB CCC
--- --- ---
 10  10  10
 10  10  10
 10  10  10
 10  10  10
 10  10  10

所以我使用 foreach 循环编写以下代码生成列名:

$property = @('AAA','BBB','CCC') | foreach {
    @{ name = $_; expression = { 10 } }
}
@(1..5) | select -Property $property

但我收到以下错误,指出 name 不是 string

select : The "name" key has a type,System.Management.Automation.PSObject,that is not valid; expected type is System.String.
At line:4 char:11
+ @(1..5) | select -Property $property
+           ~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [Select-Object],NotSupportedException
    + FullyQualifiedErrorId : DictionaryKeyIllegalValue2,Microsoft.PowerShell.Commands.SelectObjectCommand

要使代码正常工作,我必须将 $_ 转换为 string,如下所示:

$property = @('AAA','CCC') | foreach {
    @{ name = [string]$_; expression = { 10 } }
}
@(1..5) | select -Property $property

或者像下面这样:

$property = @('AAA','CCC') | foreach {
    @{ name = $_; expression = { 10 } }
}
$property | foreach { $_.name = [string]$_.name }
@(1..5) | select -Property $property

问题是:$_ 已经是 string。为什么我必须再次将其转换为 string?为什么 select 认为 namePSObject

为了确认它已经是 string,我编写了以下代码来打印 name 的类型:

$property = @('AAA','CCC') | foreach {
    @{ name = $_; expression = { 10 } }
}
$property | foreach { $_.name.GetType() }

以下结果确认它已经是一个 string

IsPublic IsSerial Name                                     BaseType            
-------- -------- ----                                     --------            
True     True     String                                   System.Object       
True     True     String                                   System.Object       
True     True     String                                   System.Object       

我知道还有许多其他更简单的方法生成表格。但我想了解为什么我必须将 string 转换为 string 才能使代码工作,以及为什么 select 不认为 string 是 {{ 1}}。就其价值而言,我的 string 是:

$PsversionTable.Psversion

解决方法

您会看到 PowerShell 在幕后使用的偶然的、通常不可见 [psobject] 包装器的不幸影响。

在您的情况下,由于输入字符串是通过管道提供的,因此它们被包裹起来并存储在您的哈希表中作为 [psobject] 实例,这就是问题的原因。

解决方法 - 这既不明显也不应该是必要的 - 是通过访问 .psobject.BaseObject丢弃包装器:

$property = 'AAA','BBB','CCC' | ForEach-Object {
    @{ name = $_.psobject.BaseObject; expression = { 10 } }
}
1..5 | select -Property $property

注意:

  • 在您的情况下,考虑到您想要一个字符串,一个更简单的 .psobject.BaseObject 替代方法(请参阅概念性的 about_Intrinsic Members 帮助主题)是调用 .ToString()

  • 要测试给定值/变量是否存在此类包装器,请使用 -is [psobject];使用您的原始代码,例如,以下产生 $true

    • $property[0].name -is [psobject]
    • 但是请注意,此测试对于 [pscustomobject] 实例毫无意义,因为它always $true(自定义对象本质上是 [psobject] 实例,没有.NET 基础对象 - 它们只有动态属性)。

通常不可见的 [psobject] 包装器在情况下、模糊地导致行为差异可以说是一个错误GitHub issue #5579


更简单、更快的替代方案,使用 .ForEach() array method

$property = ('AAA','CCC').ForEach({
  @{ name = $_; expression = { 10 } }
})
1..5 | select -Property $property

与管道不同,.ForEach() 方法$_包裹在[psobject]中,所以问题不会出现,也不需要解决方法。

使用该方法也更快,但请注意,与管道不同的是,它必须预先收集内存中的所有输入(显然不是数组文字的问题)。