Powershell 数组扁平化是如何工作的?

mar*_*ark 5 arrays powershell

请注意:

C:\> $x = @(1)
C:\> $x = @($x,2)
C:\> $x = @($x,3)
C:\> $x = @($x,4)
C:\> $x = @($x,5)
C:\> $x.Length
2
C:\> @($x |% { $_ }).Length
3
C:\> $x


Length         : 2
LongLength     : 2
Rank           : 1
SyncRoot       : {System.Object[] 2, 3}
IsReadOnly     : False
IsFixedSize    : True
IsSynchronized : False
Count          : 2

4
5


C:\>
Run Code Online (Sandbox Code Playgroud)

我预计管道会压平列表。但这并没有发生。我究竟做错了什么?

mkl*_*nt0 8

我预计管道会压平列表。

  • PowerShell枚举数组(列表)以及(大多数)可枚举数据类型,即将其元素一一发送到成功输出流管道充当管道)。

    • 如果一个元素本身恰好是另一个可枚举的,则它也不会被枚举。

    • 将 PowerShell 中的输出概念化为不确定长度的对象流比将其视为数组(列表)及其展平更好- 请参阅底部部分。

  • 另外,仅在用于显示格式期间,可枚举数被枚举为两层深度,因此在视觉上您无法区分@(1, 2)(枚举的数组,导致输出两个整数)和Write-Output -NoEnumerate @(1, 2)(作为输出的数组)单个对象) - 即使在数据输出方面这些命令有所不同。

    • 这种仅用于显示的嵌套枚举在第二层停止,因此该层的任何元素都会按照其本身的格式进行格式化,即使它碰巧是另一个可枚举的元素;例如,对于数组,该数组根据给定对象的常用 PowerShell 规则进行格式化:因为 .NETArray类型具有超过 4 个属性,因此Format-List隐式应用格式化,从而导致逐行显示Array实例的属性,如下例所示:

      # The 2nd element - a nested array - is formatted as a whole.
      PS> Write-Output -NoEnumerate @(1, @(2))
      1
      
      Length         : 1
      LongLength     : 1
      Rank           : 1
      SyncRoot       : {2}
      IsReadOnly     : False
      IsFixedSize    : True
      IsSynchronized : False
      Count          : 1
      
      Run Code Online (Sandbox Code Playgroud)

将上述内容应用于您的具体示例:

# Your nested array.
$x = @(1); $x = @($x,2); $x = @($x,3); $x = @($x,4); $x = @($x,5)
Run Code Online (Sandbox Code Playgroud)

要可视化生成的嵌套数组,请将其传递给ConvertTo-Json

PS> ConvertTo-Json -Depth 4 $x

[
  [
    [
      [
        [
          1
        ],
        2
      ],
      3
    ],
    4
  ],
  5
]
Run Code Online (Sandbox Code Playgroud)

这告诉我们,您已经创建了一个元素数组,其第一个元素恰好是一个嵌套数组,它本身包含两个元素,其中第一个元素包含另一个嵌套数组。

  • 因此,$x.Length输出2.

  • @($x | % { $_ }).Length输出3的原因如下:

    • 发送$x到管道运算符|会枚举其元素,并且输出每个元素(通过( ) 脚本块$_中)会导致每个元素被枚举(如果它恰好是一个数组)。%ForEach-Object
    • 因此,嵌套在 的第一个元素内的双元素数组的元素$x被单独输出,然后是5、 的第二个元素$x,总共产生三个输出对象。
    • 在数组 ( @(...)) 中捕获这些输出对象会创建一个三元素数组,其.Length报告3.

由输出本身产生的用于显示的表示$x遵循上面的解释。


背景资料:

  • PowerShell管道是成功输出流的管道,成功输出流是不确定长度的对象,并且仅当捕获该流时- 在分配给变量( $var = ...) 或使命令输出参与表达式的上下文中(例如(...).Foo) -数组的概念是否进入了图片:

    • 如果输出流恰好只包含一个对象,则该对象将按原样捕获。
    • 否则,PowerShell 引擎必然会捕获集合中的多个对象,该集合是一个[object[]]类型数组
  • 因此,输出流没有数组的概念,最终会导致用数组及其扁平化来讨论它会造成混乱。

    • 请注意,输出流(管道)不仅在使用管道运算符( |)在命令之间显式传输数据时使用,...

    • ...它也可以被任何单个命令隐式使用来发送其输出。

    • 但是,它用于表达式单独),例如1 + 2or [int[]] (1, 2, 3)命令是任何 cmdlet、函数、脚本、脚本块或外部程序)或将参数传递给命令时。

      • 也就是说,如果您将表达式的结果发送到命令 via |(仅当表达式是第一个管道段时才有效),则会再次涉及管道,并且通常的枚举行为(下面讨论)适用于表达式的结果;例如[int[]] (1, 2, 3) | ForEach-Object { 1 + $_ }

      • 也许令人惊讶的是,@(...)and$(...)运算符的使用总是涉及管道,因此即使在其中包含独立表达式也会导致枚举(和重新收集);例如,@([int[]] (1, 2, 3)).GetType().Namereports Object[]( [object[]]),因为枚举了强类型[int[]]数组,并且结果收集在常规 PowerShell 数组中。
        唯一的例外是数组文字,例如@(1, 2, 3),其中(在 PowerShell 版本 5 及更高版本中)此行为已被优化掉。
        相比之下,该(...)运算符不枚举表达式结果。

  • 默认情况下,将一个数组输出到成功输出流(更一般地说,是实现接口[1]的大多数.NET 类型的实例),会导致它被枚举,即它的元素被一一发送IEnumerable

    • 因此,输出数组与捕获输出是否也会产生(新)数组之间没有保证的关系。

    • 值得注意的是,输出单个对象与输出包装在(单元素)数组中的单个对象没有区别。

  • 将数组(集合)作为一个整体发送到管道需要额外的工作


[1] 有关 PowerShell 认为可枚举的类型的摘要(既排除已实现的选定类型IEnumerable,又包括未实现的类型),请参阅此答案的底部部分。