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

Posted

技术标签:

【中文标题】为啥要避免使用递增赋值运算符 (+=) 创建集合【英文标题】:Why should I avoid using the increase assignment operator (+=) to create a collection为什么要避免使用递增赋值运算符 (+=) 创建集合 【发布时间】:2020-06-27 17:15:56 【问题描述】:

增加赋值运算符(+=)常用于 *** 站点的[PowerShell]问答中,用于构造集合对象,例如:

$Collection = @()
1..$Size | ForEach-Object 
    $Collection += [PSCustomObject]@Index = $_; Name = "Name$_"

然而这似乎是一个非常低效的操作。

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

【问题讨论】:

【参考方案1】:

是的,在构建对象集合时应避免使用递增赋值运算符 (+=),另请参阅:PowerShell scripting performance considerations。 除了使用+= 运算符通常需要更多语句(因为数组初始化= @())这一事实之外,它鼓励将整个集合存储在内存中而不是将其中间推入管道,效率低下

之所以效率低,是因为每次你使用+=操作符,它都会做:

$Collection = $Collection + $NewObject

由于数组在元素计数方面是不可变的,因此每次迭代都会重新创建整个集合。

正确的 PowerShell 语法是:

$Collection = 1..$Size | ForEach-Object 
    [PSCustomObject]@Index = $_; Name = "Name$_"

注意: 与其他 cmdlet 一样;如果只有一个项目(迭代),输出将是 scalar 而不是数组,要将其强制为数组,您可以使用 [Array] 类型:[Array]$Collection = 1..$Size | ForEach-Object ... 或使用Array subexpression operator @( ):$Collection = @(1..$Size | ForEach-Object ... )

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

1..$Size | ForEach-Object 
    [PSCustomObject]@Index = $_; Name = "Name$_"
 | ConvertTo-Csv .\Outfile.csv

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

另见:Fastest Way to get a uniquely index item from the property of an array 和 Array causing 'system.outofmemoryexception'

性能测量

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

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

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

更正脚本的步骤

显然,有些人在纠正已经使用增加赋值运算符 (+=) 的脚本时遇到了困难。因此,我为此创建了一个小指令:

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

 

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

    

本质上等同于:

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

    

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

    将迭代的输出(放在管道上的所有内容)分配给相关的变量。这通常与初始化数组的位置相同 ($Array = @())。例如:

 

$Array = ForEach  ... 

注意 1: 同样,如果您希望单个对象充当数组,您可能希望使用 Array subexpression operator @( ),但您也可以考虑在您使用数组时执行此操作,例如:@($Array).CountForEach ($Item in @($Array))注意 2: 同样,您'最好分配输出。相反,将管道输出直接传递给下一个 cmdlet 以释放内存:... | ForEach-Object ... | Export-Csv .\File.csv

    移除数组初始化<Variable> = @()

有关完整示例,请参阅:Comparing Arrays within Powershell

请注意,这同样适用于使用+= 构建字符串, 见:Is there a string concatenation shortcut in PowerShell?

【讨论】:

太棒了!这对我优化脚本有很大帮助。非常感谢您的努力。

以上是关于为啥要避免使用递增赋值运算符 (+=) 创建集合的主要内容,如果未能解决你的问题,请参考以下文章

C语言指针变量为啥要赋初值?

C++ 为啥赋值运算符应该返回一个 const ref 以避免 (a=b)=c

JavaScript:赋值运算符以及运算符优先级

为啥要使用三元运算符而不为“真”条件赋值 (x = x ?: 1)

为啥赋值运算符要返回对对象的引用?

如何避免共享指针的复制赋值运算符c ++