【问题标题】:Why does PowerShell convert an ArrayList to a PSCustomObject when adding only one PSCustomObject?为什么只添加一个 PSCustomObject 时 PowerShell 会将 ArrayList 转换为 PSCustomObject?
【发布时间】:2019-12-30 09:42:12
【问题描述】:

我正在编写一个代码,它可以获取所有符合条件的文件。对于找到的每个文件,我都创建了一个只有少数文件 ($Obj) 值的自定义对象,然后将其添加到 ArrayList ($Files)。这个ArrayList 然后返回给调用函数并存储在它的变量($OldFiles) 中。

当没有找到文件或找到多个文件时,这可以正常工作。但是,如果只找到一个文件,ArrayList 会将自身转换为PSCustomObject,然后由于返回无法将PSCustomObject 转换为ArrayList,因此引发异常。

奇怪的是,如果我在 return 语句之前添加简单的行 "$Files.getType()",则没有错误,ArrayList 仍然是 ArrayList

基本上,这不起作用: 在第一个函数中简化:

$Files = [System.Collections.ArrayList]@()
$Files.Add($Obj) > $null
return $Files

这是它在顶部函数中的样子:

$OldFiles = [System.Collections.ArrayList](Check-OldFiles -Path $Directory -Age $Age -FilesOnly:$FilesOnly -Recurse:$Recurse -UserCreationTime:$UserCreationTime)

但这很好用: 在第一个函数中简化:

$Files = [System.Collections.ArrayList]@()
$Files.Add($Obj) > $null
$Files.getType()
return $Files

这是它在顶部函数中的样子:

$OldFiles = [System.Collections.ArrayList](Check-OldFiles -Path $Directory -Age $Age -FilesOnly:$FilesOnly -Recurse:$Recurse -UserCreationTime:$UserCreationTime)

为什么会发生这种情况?

PowerShell 版本为 5.0

【问题讨论】:

  • powershell unrolls 在通过管道发送集合或从函数返回集合时。这意味着它一次发送一个......这也意味着当它到达呼叫的另一端时,一个项目集合将是只有一个项目。您可以使用前导逗号 ,$Collection 将集合包装在一个数组中,然后再将其发送出去......这将作为一个集合展开。
  • @Lee_Dailey 我在使用管道时知道解包,但我不知道从函数返回时它会执行相同的操作。您在集合前使用逗号的提示很有魅力,非常感谢!

标签: powershell arraylist


【解决方案1】:

澄清、谨慎(@Bender the Greatest)和解释(@Phragos):

  1. @() 不会“包装”其内容。它将其内容转换为Array。如果它的内容已经是Array,那么它会按原样返回。如果它是一个值或单个对象,那么它将它复制到一个元素 Object[] 中。如果是其他形式的集合(例如ArrayList),那么它将每个元素复制到Object[]。 (注意:“@()”是一个空的Object[]。)

  2. 不要使用,(...) 来强制...成为Array。输出对象(显示器、文件、打印机等)时的自动展开行为可能会产生误导。

    $myCollection = ,(get-process)
    

    除非只有一个进程(例如来自get-process -name),否则$myCollection 不会留下Array 个进程。相反,$myCollection 将是包含整个输出的单个元素的 Array(单个对象或输出对象的 Array)。格式输出 cmdlet(Format-TableFormat-ListFormat-Wide)掩盖了这一事实,这些 cmdlet 检查接收到的对象,如果有 Arrays,则打开它们。 (更多详情How PowerShell Formatting and Outputting REALLY works)。

    你说“但如果我这样做,效果很好”:

    $myCollection | foreach { $_.starttime }
    

    我说,是的,你得到的开始时间列表不是你想的那样。在上面,$myCollection 被解包,每个元素被发送到管道中到foreach。只有一个元素是Array,因此foreach 循环一次,$_ 设置为Array 进程。由于 Array 是一个集合但没有 starttime 成员,PowerShell (V3+) 执行 member enumeration 创建一个 Array,该 Array$_ 的每个元素的 starttime 成员的值组成。然后将这个Array 解包并发送到管道中。

    你说“那又怎样,效果是一样的”,我回答,是的,直到你尝试更复杂的东西:

    $myFiles = ,(gci -file *)
    $myFiles | foreach { $_.name+' '+$_.length }
    

    预期的输出是文件名和长度的列表。实际输出是名称列表、空行和文件数。为什么? $myFiles 是一个包含文件列表的元素的Array,因此foreach 循环一次,$_ 是这个列表。 Array 没有 name 成员,因此 PowerShell 执行 member 枚举 生成 Array 的名称。接下来,将一个空格的String 添加(连接)到此Array。最后,添加$_.length。因为Array 确实有一个长度成员,所以这个值(Int32)被添加到名称的Array' ' 中。这个Array 然后被打开并发送出去。

    总结,如果您希望某个可能是也可能不是 Array 的东西绝对是 Array,请不要使用 ,(...)。始终使用@(...),这就是它的用途。

    但是,在其他情况下,使用 ,(...) 是合适的,但仍会造成混淆。

    解析命令行时,空格分隔 tokens 而不是参数。因此,以下是等价的:

    1,2,3
    
    1  , 2,      3
    

    这可能会让你在使用 prepend 时绊倒你,创建一个包含一个项目的数组。假设您有一个将Array 作为参数的cmdlet,但您只想提供一个本身就是Array 的元素。例如,

    new-object collections.arraylist ,(1,2,3)
    New-Object : Cannot convert 'System.Object[]' to the type 'System.String' required by parameter 'ComObject'. Specified method is not supported.
    At line:1 char:12
    + new-object collections.arraylist ,(1,2,3)
    +            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [New-Object], ParameterBindingException
    + FullyQualifiedErrorId : CannotConvertArgument,Microsoft.PowerShell.Commands.NewObjectCommand
    

    由于 , 之前的空格基本上被解析器忽略(, 是单个字符标记,因此不需要分隔),因此将其解析为:

     new-object ("collections.arraylist",(1,2,3))
    

    所以在这种情况下应该是:

     new-object collections.arraylist (,(1,2,3))
    

    注意:@(1,2,3) 不起作用,因为 1,2,3 已经是 Array

  3. Powershell 中的 return 语句与函数式语言不同,它不是产生输出的唯一方式。每个产生除void 之外的值的表达式都被发送到管道中,即收集以供返回(展开数组的值并单独收集元素)。 return 使函数在值生成后返回(它不会“返回”值)。我假设函数的看不见的部分没有生成任何值。但是,将 $Files.gettype() 放入函数会导致生成一个值(System.RuntimeType)。因此,该函数总是返回比以前多 1 个对象。我必须假设没有测试 0 个匹配文件的情况(该函数已经适用于这种情况,对吗?)因为现在我们回到了一个对象,所以该函数再次返回一个“值”(System.RuntimeType)。我不知道返回的对象是如何使用的,但没有提到ArrayList 的第一个元素是System.RuntimeType 而不是PSCustomObject 引起的问题。据推测,该用法不介意为不存在的属性获取 $null(只有 [String]Name 和 [String]Fullname 对 System.RuntimeTypeSystem.IO.FileInfo 是通用的)并且 Attributes 不会导致类型冲突(System.RuntimeType.AttributesSystem.IO.FileInfo.Attributes 都是 System.Enum,但 [Collections.ArrayList].Attributes 的值不是有效的 IO.FileInfo.Attributes 值,或者 PowerShell 说)。

† 赋值 statements 属于 void 类型,而赋值 expressions(表达式中的赋值)具有非 void 类型。因此,

$a = 5;       // void
($a = 5)      // int
($a = 5);     // also int
if ($a = 5){} // valid
// the assignment is evaluated as part of a conditional expression (value 5 -> $true)

【讨论】:

    【解决方案2】:

    当单个元素沿管道传递时,集合会在 PowerShell 中自动展开。如果你想保证一个变量即使只有一个元素也仍然是一个集合,你可以使用一些技巧(下面的示例使用Get-Process,但可以使用任何返回集合的集合变量或 cmdlet):

    1. 将您的命令包装在一个数组中(例如$myCollection = @(Get-Process)
    2. 用逗号, 为您的潜在收藏添加前缀(例如$myCollection = , (Get-Process)

    @() 指定了一个数组,您可以为它提供一个元素列表或使用一个可以返回集合的 cmdlet。

    , (Get-Process) 语法看起来很奇怪,但这与为数组指定硬编码元素列表的语法相同。考虑以下几点:

    $myArray = 1, 2, 3, 4, 5, "Bender"
    

    这将创建一个包含元素 12345"Bender" 的数组。不过,在上面的简写中,仅提供 , 作为表达式中的第一个字符就表示这是一个数组,请照此处理。由于 PowerShell 展开集合的性质,在初始逗号之后返回的任何后续数组/集合都将添加到最终集合中。

    【讨论】:

      猜你喜欢
      • 2021-09-14
      • 2021-02-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-09-26
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多