Powershell 中的数组类型 - System.Object[] 与特定类型的数组

Array Types In Powershell - System.Object[] vs. arrays with specific types

为什么在字符串数组 return Object[] 而不是 String[] 上计算 GetType().Name? 这似乎发生在任何元素类型上,例如 Import-Csv 会给你一个 Object[] 但每个元素都是 PSCustomObject.

这是一个数组 String

的示例
$x = @('a','b','c')

$x[0].GetType().Name #String
$x.GetType().Name #Object[]

因为你没有明确指定数组的数据类型。

例如,将整数分配给 $x[1] 是可行的,因为数组的类型是 Object[].

如果在构造数组时指定数据类型,以后将无法分配不兼容类型的值:

C:\PS> [int[]] $myArray = 12,64,8,64,12

C:\PS> $myArray.GetType()

IsPublic IsSerial Name                                     BaseType                   
-------- -------- ----                                     --------                   
True     True     Int32[]                                  System.Array               



C:\PS> $myArray[0] = "asd"
Cannot convert value "asd" to type "System.Int32". Error: "Input string was not in a c
orrect format."
At line:1 char:1
+ $myArray[0] = "asd"
+ ~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvalidCastFromStringToInteger

感谢 PetSerAl 的帮助。

要用 补充 为什么 PowerShell 默认创建 System.Object[] 个数组 和其他背景信息:

PowerShell 的默认数组是 灵活的:

  • 它们允许您存储任何类型的对象(包括$null),
  • 甚至允许您在一个数组中混合不同类型的对象.

要启用此功能,数组必须(隐式)键入为 [object[]] ([System.Object[]]),因为 System.Object 是单个所有其他类型都从中派生的整个 .NET 类型层次结构的根。

例如,下面创建了一个 [object[]] 数组,其元素的类型分别为 [string][int][datetime]$null

$arr = 'hi', 42, (Get-Date), $null  # @(...) is not needed; `, <val>` for a 1-elem. arr.

当你:

  • 使用数组构造运算符创建数组,,

  • 使用数组子表达式运算符[=强制命令输出到数组中31=]

  • 保存到变量 命令的输出a 集合 包含 2 个或更多 个元素的对象,不考虑原始集合的具体类型 ,或通过 将其包含在 (...)

    中在另一个命令的上下文中对其进行操作

总是得到一个System.Object[]数组——即使所有元素碰巧 与您的示例中的类型相同。


可选的进一步阅读

PowerShell 的默认数组很方便,但也有缺点:

  • 它们提供无类型安全:如果你想确保所有元素都是特定类型(或者如果可能的话应该转换成它) ,默认数组不会做;例如:

      $intArray = 1, 2      # An array of [int] values.
      $intArray[0] = 'one'  # !! Works, because a [System.Object[]] array can hold any type.
    
  • [System.Object[]] 数组对于 值类型 低效的,例如 [int],因为boxing and unboxing 必须执行 - 尽管这在现实世界中通常并不重要。

由于 PowerShell 提供对 .NET 类型系统的访问,如果您 创建一个仅限于特定类型的数组,使用 cast类型约束变量:

[int[]] $intArray = 1, 2  # A type-constrained array of [int] variable.
$intArray[0] = 'one'      # BREAKS: 'one' can't be converted to an [int]

请注意,使用 cast 来创建数组 - $intArray = [int[]] (1, 2) - 也可以,但只有类型约束变量才能确保你不能 稍后将不同类型的值分配给变量(例如,$intArray = 'one', 'two'会失败)。

casts的语法陷阱:[int[]] 1, 2 按预期工作,因为转换具有高 operator precedence,所以表达式被计算为 ([int[]] 1), 2,这将创建一个常规 [object[]] 数组,其 第一个 元素是一个 嵌套 [int[]] 数组,只有一个元素 1.
如有疑问,请在数组元素周围使用 @(...)[1],如果要确保表达式可能 return 只有 单个 项始终被视为数组。


陷阱

PowerShell 在幕后执行许多类型转换,这通常非常有用,但也有陷阱:

  • PowerShell 自动尝试将值强制转换为目标类型,您并不总是想要并且可能不会注意到:

      [string[]] $a = 'one', 'two'
      $a[0] = 1    # [int] 1 is quietly coerced to [string]
    
      # The coercion happens even if you use a cast:
      [string[]] $a = 'one', 'two'
      $a[0] = [int] 1    # Quiet coercion to [string] still happens.
    

    注意:即使是显式强制转换 - [int] 1 - 也会导致安静的强制转换,这可能会让您感到惊讶,也可能不会。我的惊讶来自 - 错误地 - 假设在诸如 PowerShell 转换之类的自动强制语言中可能是 bypass 强制的一种方式 - 这是 not 是的。[2]

    鉴于 any 类型可以转换为 string[string[]] 数组是最棘手的情况。
    如果无法执行(自动)强制转换,您 do 会收到错误消息,例如 with
    [int[]] $arr = 1, 2; $arr[0] = 'one' # error

  • “添加到”特定类型的数组会创建一个 new 类型 [object[]] 的数组:

    PowerShell 允许您使用 + 运算符方便地“添加到”数组。
    实际上,一个 new 数组是在幕后创建的 并附加了附加元素,但是 new 数组是默认情况下再次为 [object[]] 类型,与输入数组的类型无关 :

      $intArray = [int[]] (1, 2)
      ($intArray + 4).GetType().Name # !! -> 'Object[]'
      $intArray += 3 # !! $intArray is now of type [object[]]
    
      # To avoid the problem...
      # ... use casting:
      ([int[]] ($intArray + 4)).GetType().Name # -> 'Int32[]'
      # ... or use a type-constrained variable:
      [int[]] $intArray = (1, 2) # a type-constrained variable
      $intArray += 3 # still of type [int[]], due to type constraint.
    
  • 输出到成功流将任何集合转换为[object[]]:

    命令或管道输出(到成功流)的具有至少 2 个元素 的任何集合是 自动转换成[object[]]类型的数组,可能会出乎意料:

      # A specifically-typed array:
      # Note that whether or not `return` is used makes no difference.
      function foo { return [int[]] (1, 2) }
      # Important: foo inside (...) is a *command*, not an *expression*
      # and therefore a *pipeline* (of length 1)
      (foo).GetType().Name # !! -> 'Object[]'
    
      # A different collection type:
      function foo { return [System.Collections.ArrayList] (1, 2) }
      (foo).GetType().Name # !! -> 'Object[]'
    
      # Ditto with a multi-segment pipeline:
      ([System.Collections.ArrayList] (1, 2) | Write-Output).GetType().Name # !! -> 'Object[]'
    

    此行为的原因是 PowerShell 基本上是基于集合的发送任何命令的输出通过管道逐项;请注意,即使 单个 命令也是一个管道(长度为 1)。

    也就是说,PowerShell 总是 首先 unwraps 集合,然后,如果需要,重新组装它们 - 用于分配给变量,或作为中间结果嵌套在 (...) 中的 命令 - 重新组合的集合 始终为 [object[]].

    如果对象的类型实现了 IEnumerable interface, except if it also implements the IDictionary 接口,PowerShell 会将其视为集合。
    此异常意味着 PowerShell 的哈希表 ([hashtable]) and ordered hashtables (the PSv3+ literal variant with ordered keys, [ordered] @{...}, which is of type [System.Collections.Specialized.OrderedDictionary]) 是通过管道 作为一个整体 发送的,要改为单独枚举它们的条目(键值对),您必须调用它们.GetEnumerator() 方法。

  • PowerShell 设计 总是 unwraps 单个-元素输出集合到该单个元素:

    换句话说:输出单元素集合时,PowerShell不会return一个数组,而是数组的单个元素本身.

      # The examples use single-element array ,1 
      # constructed with the unary form of array-construction operator ","
      # (Alternatively, @( 1 ) could be used in this case.)
    
      # Function call:
      function foo { ,1 }
      (foo).GetType().Name # -> 'Int32'; single-element array was *unwrapped*
    
      # Pipeline:
      ( ,1 | Write-Output ).GetType().Name # -> 'Int32'
    
      # To force an expression into an array, use @(...):
      @( (,1) | Write-Output ).GetType().Name # -> 'Object[]' - result is array
    

    松散地说,数组子表达式运算符@(...)目的是:始终将包含的值视为集合,即使它只包含(或通常会展开)一个单个项目:
    如果它是一个 单个 值,将它包装成一个包含 1 个元素的 [object[]] 数组。
    已经是集合的值仍然是集合,尽管它们 转换为 新的 [object[]] 数组 ,即使值本身已经 一个数组:
    $a1 = 1, 2; $a2 = @( $a1 ); [object]::ReferenceEquals($a1, $a2)
    输出 $false,证明数组 $a1$a2 不相同。

    对比一下:

    • 只是 (...)本身不会改变值的type - 它的目的仅仅是为了澄清优先级或强制一个新的解析上下文:

      • 如果封闭的结构是一个表达式(在表达式模式), 类型改变;例如,([System.Collections.ArrayList] (1, 2)) -is [System.Collections.ArrayList]([int[]] (1,2)) -is [int[]] 都是 return $true - 类型被保留。

      • 如果封闭的结构是命令(单段或多段管道 ),然后应用 默认展开行为;例如:
        (&{ , 1 }) -is [int] returns $true(单元素数组被展开)和 (& { [int[]] (1, 2) }) -is [object[]][int[]] 数组被重组为 [object[]] 数组) return $true,因为调用运算符 & 的使用使封闭的构造成为 command.

    • (常规)子表达式运算符$(...),通常用于可扩展字符串,表现出默认展开行为: $(,1) -is [int]$([System.Collections.ArrayList] (1, 2)) -is [object[]] 都 return $true.

  • 从函数或脚本返回一个集合作为一个整体:

    有时您可能希望将集合作为一个整体输出,即将其输出为单个项目,保留其原始类型。

    正如我们在上面看到的,按原样输出集合会导致 PowerShell 解包并最终将其重新组合成一个常规 [object[]] 数组。

    为防止这种情况,一元形式的数组构造运算符,可用于将集合包装在 outer 数组 中,然后 PowerShell 将其解包为原始集合:

      # Wrap array list in regular array with leading ","
      function foo { , [System.Collections.ArrayList] (1, 2) }
      # The call to foo unwraps the outer array and assigns the original
      # array list to $arrayList.
      $arrayList = foo
      # Test
      $arrayList.GetType().Name # -> 'ArrayList'
    

    PSv4+中,使用Write-Output -NoEnumerate:

      function foo { write-output -NoEnumerate ([System.Collections.ArrayList] (1, 2)) }
      $arrayList = foo
      $arrayList.GetType().Name # -> 'ArrayList'
    

[1] 请注意 使用 @(...) 创建数组 文字 不是 必要的,因为数组构造运算符 , 单独 创建数组 .
在 PSv5.1 之前的版本中,您还需要付出(在大多数情况下可能可以忽略不计)性能损失,因为 @() 中的 , 构造的数组实际上是 克隆的 作者 @() - 详见我的
也就是说,@(...)有优势

  • 您可以使用相同的语法,无论您的数组文字包含单个 (@( 1 ) 还是多个元素 (@( 1, 2 ))。将此与仅使用 , 进行对比:1, 2, 1.
  • 您不需要 , 分隔 多行 @(...) 语句的行(但请注意,每一行在技术上都变成了自己的语句) .
  • 没有运算符优先级陷阱,因为 $(...)@(...) 具有最高优先级。

[2] PetSerAl 提供此高级代码片段以显示 PowerShell 尊重强制转换的有限场景 ,即在 .NET 方法调用的重载解析的上下文中 :

# Define a simple type that implements an interface
# and a method that has 2 overloads.
Add-Type '
  public interface I { string M(); }
  public class C : I {
           string I.M()       { return "I.M()"; } // interface implementation
    public string M(int i)    { return "C.M(int)"; } 
    public string M(object o) { return "C.M(object)"; } 
  }
'
# Instantiate the type and use casts to distinguish between
# the type and its interface, and to target a specific overload.
$C = New-Object C
$C.M(1)           # default: argument type selects overload -> 'C.M(int)' 
([I]$C).M()       # interface cast is respected -> 'I.M()'
$C.M([object]1)   # argument cast is respected -> 'C.M(object)'