为什么要避免使用递增赋值运算符 (+=) 来创建集合

iRo*_*Ron 4 collections powershell variable-assignment operator-keyword

StackOverflow 站点的问答中+=经常使用递增赋值运算符 ( )[PowerShell]来构造集合对象,例如:

$Collection = @()
1..$Size | ForEach-Object {
    $Collection += [PSCustomObject]@{Index = $_; Name = "Name$_"}
}
Run Code Online (Sandbox Code Playgroud)

然而,它似乎是一种非常低效的操作。

一般情况下,+=在 PowerShell 中构建对象集合时应该避免使用递增赋值运算符 ( ) 是否可以?

iRo*_*Ron 10

是的,在+=构建对象集合时应该避免增加赋值运算符 ( ),另请参阅:PowerShell 脚本性能考虑
除了使用+=运算符通常需要更多语句(因为数组初始化= @())并且它鼓励将整个集合存储在内存中而不是将其中间推入管道的事实之外它是低效的

它效率低下的原因是因为每次使用+=运算符时,它只会执行以下操作:

$Collection = $Collection + $NewObject
Run Code Online (Sandbox Code Playgroud)

因为数组在元素数量方面是不可变的,所以每次迭代都会重新创建整个集合。

正确的 PowerShell 语法是:

$Collection = 1..$Size | ForEach-Object {
    [PSCustomObject]@{Index = $_; Name = "Name$_"}
}
Run Code Online (Sandbox Code Playgroud)

注意:与其他 cmdlet 一样;如果只有一个项目(迭代),则输出将是标量而不是数组,要将其强制为数组,您可以使用[Array]type:[Array]$Collection = 1..$Size | ForEach-Object { ... }或使用Array 子表达式运算符@( )$Collection = @(1..$Size | ForEach-Object { ... })

建议甚至不要将结果存储在变量 ( $a = ...) 中,而是立即将其传递到管道中以节省内存,例如:

1..$Size | ForEach-Object {
    [PSCustomObject]@{Index = $_; Name = "Name$_"}
} | ConvertTo-Csv .\Outfile.csv
Run Code Online (Sandbox Code Playgroud)

注意:也可以考虑使用System.Collections.ArrayList该类,这通常几乎与 PowerShell 管道一样快,但缺点是它比(正确地)使用 PowerShell 管道消耗更多的内存。

另请参阅:从数组的属性中获取唯一索引项的最快方法

绩效衡量

要显示与集合大小和性能下降的关系,您可以检查以下测试结果:

1..20 | ForEach-Object {
    $size = 1000 * $_
    $Performance = @{Size = $Size}
    $Performance.Pipeline = (Measure-Command {
        $Collection = 1..$Size | ForEach-Object {
            [PSCustomObject]@{Index = $_; Name = "Name$_"}
        }
    }).Ticks
    $Performance.Increase = (Measure-Command {
        $Collection = @()
        1..$Size | ForEach-Object {
            $Collection  += [PSCustomObject]@{Index = $_; Name = "Name$_"}
        }
    }).Ticks
    [pscustomobject]$Performance
} | Format-Table *,@{n='Factor'; e={$_.Increase / $_.Pipeline}; f='0.00'} -AutoSize

 Size  Increase Pipeline Factor
 ----  -------- -------- ------
 1000   1554066   780590   1.99
 2000   4673757  1084784   4.31
 3000  10419550  1381980   7.54
 4000  14475594  1904888   7.60
 5000  23334748  2752994   8.48
 6000  39117141  4202091   9.31
 7000  52893014  3683966  14.36
 8000  64109493  6253385  10.25
 9000  88694413  4604167  19.26
10000 104747469  5158362  20.31
11000 126997771  6232390  20.38
12000 148529243  6317454  23.51
13000 190501251  6929375  27.49
14000 209396947  9121921  22.96
15000 244751222  8598125  28.47
16000 286846454  8936873  32.10
17000 323833173  9278078  34.90
18000 376521440 12602889  29.88
19000 422228695 16610650  25.42
20000 475496288 11516165  41.29
Run Code Online (Sandbox Code Playgroud)

这意味着20,000使用+=运算符的对象集合大小40x比为此使用 PowerShell 管道慢。

纠正脚本的步骤

显然,有些人难以纠正已经使用递增赋值运算符 ( +=)的脚本。因此,我创建了一个小说明来这样做:

  1. <variable> +=从相关迭代中删除所有分配,只留下对象 item。通过不分配对象,对象将简单地放在管道上。
    迭代中是否有多个增加赋值或者是否有嵌入的迭代或函数都没有关系,最终结果将是相同的。
    意思是,这个:

 

ForEach ( ... ) {
    $Array += $Object1
    $Array += $Object2
    ForEach ( ... ) {
        $Array += $Object3
        $Array += Get-Object

    }
}
Run Code Online (Sandbox Code Playgroud)

本质上是一样的:

$Array = ForEach ( ... ) {
    $Object1
    $Object2
    ForEach ( ... ) {
        $Object3
        Get-Object

    }
}
Run Code Online (Sandbox Code Playgroud)

注意:如果没有迭代,则可能没有理由更改您的脚本,因为可能只涉及一些添加

  1. 将迭代的输出(放入管道的所有内容)分配给相关变量。这通常与初始化数组的级别 ( $Array = @())处于同一级别。例如:

 

$Array = ForEach { ... 
Run Code Online (Sandbox Code Playgroud)

注 1:同样,如果您希望单个对象充当数组,您可能想要使用Array 子表达式运算符,@( )但您也可能会在使用数组时考虑这样做,例如:@($Array).CountForEach ($Item in @($Array))
注 2:再次,您最好指定输出都而是直接通过管道输出到下一个cmdlet来释放内存:ForEach ( ... ) { ... } | Export-Csv .\File.csv

  1. 删除数组初始化 <Variable> = @()

有关完整示例,请参阅:在 Powershell 中比较数组

  • 惊人的!这对我优化脚本有很大帮助。非常感谢您付出的努力。 (2认同)