【问题标题】:What determines whether the Powershell pipeline will unroll a collection?什么决定了 Powershell 管道是否会展开集合?
【发布时间】:2009-12-01 18:07:20
【问题描述】:
# array
C:\> (1,2,3).count
3
C:\> (1,2,3 | measure).count
3

# hashtable
C:\> @{1=1; 2=2; 3=3}.count
3
C:\> (@{1=1; 2=2; 3=3} | measure).count
1

# array returned from function
C:\> function UnrollMe { $args }
C:\> (UnrollMe a,b,c).count
3
C:\> (UnrollMe a,b,c | measure).count
1
C:\> (1,2,3).gettype() -eq (UnrollMe a,b,c).gettype()
True

与 HashTables 的差异是 fairly well known,尽管 official documentation 只是间接提及(通过示例)。

不过,函数的问题对我来说是个新闻。我有点震惊它以前没有咬过我。我们脚本编写者可以遵循一些指导原则吗?我知道在 C# 中编写 cmdlet 时,有一个 overload of WriteObject 可以显式控制枚举,但是 AFAIK 在 Posh 语言本身中没有这样的构造。正如最后一个示例所示,Posh 解释器似乎相信管道对象的类型没有区别。我怀疑引擎盖下可能存在一些 Object 与 PSObject 的怪异之处,但是当您编写纯 Posh 并期望脚本语言“正常工作”时,这几乎没有用处。

/编辑/

Keith 正确地指出,在我的示例中,我传递的是单个 string[] 参数而不是 3 个字符串参数。换句话说,Measure-Object 说 Count=1 的原因是因为它看到了一个数组数组,其第一个元素是 @("a", "b", "c")。很公平。这些知识使您可以通过多种方式解决该问题:

# stick to single objects
C:\> (UnrollMe a b c | measure).count
3

# rewrite the function to handle nesting
C:\> function UnrollMe2 { $args[0] }
C:\> (UnrollMe2 a,b,c | measure).count
3

# ditto
C:\> function UnrollMe3 { $args | %{ $_ } }
C:\> (UnrollMe3 a,b,c | measure).count
3

然而,它并不能解释一切......

# as seen earlier - if we're truly returning @( @("a","b","c") ) why not count=1?
C:\> (UnrollMe a,b,c).count
3

# our theory must also explain these results:
C:\> ((UnrollMe a,b,c) | measure).count
3
C:\> ( @(@("a","b","c")) | measure).count
3
C:\> ((UnrollMe a,b,c d) | measure).count
2

据我推测,还有另一条规则在起作用:如果你有一个数组只有一个元素并且解析器在expression mode,那么解释器将“解包”所述元素。我还缺少什么细节吗?

【问题讨论】:

  • WriteObject 的等价物是人们很少使用的 Write-Output cmdlet(别名为 echo),因为值隐式输出到 stdout 流。
  • 是的,虽然 Write-Output 没有像 WriteObject(object, bool) 那样的 -EnumerateCollection 参数。

标签: collections powershell ienumerable pipeline


【解决方案1】:

$args 已展开。请记住,函数参数通常使用空格来分隔它们。当你传入 1,2,3 时,你传入的是一个由三个数字组成的数组,分配给 $args[0]:

PS> function UnrollMe { $args }
PS> UnrollMe 1 2 3 | measure

Count    : 3

将结果(数组)放入分组表达式(或子表达式,例如$())使其再次符合展开条件,因此以下展开包含由 UnrollMe 返回的 1,2,3 的 object[]:

PS> ((UnrollMe 1,2,3) | measure).Count
3

相当于:

PS> ((1,2,3) | measure).Count
3

顺便说一句,它不仅仅适用于具有一个元素的数组。

PS> ((1,2),3) | %{$_.GetType().Name}
Object[]
Int32

在已经是数组的东西上使用数组子表达式 (@()) 无论应用多少次都没有效果。 :-) 如果您想防止展开,请使用逗号运算符,因为它会总是创建另一个展开的外部数组。请注意,在这种情况下,您并没有真正阻止展开,您只需通过引入一个展开的外部“包装器”数组而不是原始数组来解决展开问题,例如:

PS> (,(1,2,3) | measure).Count
1

最后,当你执行这个时:

PS> (UnrollMe a,b,c d) | %{$_.GetType().Name}
Object[]
String

您可以看到 UnrollMe 返回两个项目 (a,b,c) 作为一个数组和 d 作为一个标量。这两项分别沿管道发送,结果计数为 2。

【讨论】:

  • 好电话。澄清一下:在我的示例中,$args 不仅仅是一个数组,而是一个数组数组。它的第一个也是唯一的元素是@(1,2,3)。
  • 嗯,经过进一步思考,这引发的问题多于答案:) 请参阅编辑。
  • >> 将结果(数组)放入分组表达式(或子表达式,例如 $())使其再次符合展开的条件。 //// 很好的总结,谢谢。
【解决方案2】:

这似乎与 Measure-Object 的工作原理以及对象如何沿管道传递有关。

当你说

1,2,3 | measure

您将 3 个 Int32 对象传递到管道,测量对象然后计算它在管道上看到的每个对象。

当您使用您的函数“展开”它时,您会得到一个传递到管道中的单个数组对象,其度量对象计数为 1,它不会尝试遍历数组中的对象,如下所示:

PS C:\> (measure -input 1,2,3).count
1

一种可能的解决方法是使用 foreach 将数组“重新滚动”到管道上:

PS C:\> (UnrollMe 1,2,3 | %{$_} | measure).count
3

【讨论】:

    猜你喜欢
    • 2021-07-02
    • 1970-01-01
    • 1970-01-01
    • 2012-05-09
    • 2013-10-15
    • 2014-05-09
    • 2016-02-16
    • 2021-12-15
    相关资源
    最近更新 更多