可以在 PowerShell 中简化以下 Nested foreach 循环吗?

Can the following Nested foreach loop be simplified in PowerShell?

我创建了一个循环遍历数组并排除在第二个数组中找到的任何变量的脚本。

虽然代码有效;这让我想知道是否可以简化或管道化它。

   $result = @()
   $ItemArray = @("a","b","c","d")
   $exclusionArray = @("b","c")

    foreach ($Item in $ItemArray)
    {
        $matchFailover = $false
        :gohere
        foreach ($ExclusionItem in $exclusionArray)
        {
            if ($Item -eq $ExclusionItem)
            {
                Write-Host "Match: $Item = $ExclusionItem"
                $matchFailover = $true
                break :gohere
            }
            else{
            Write-Host "No Match: $Item != $ExclusionItem"
            }
        }
        if (!($matchFailover))
        {
            Write-Host "Adding $Item to results"
            $result += $Item
        }
    }
    Write-Host "`nResults are"
    $result

您可以将 Where-Object-notcontains 一起使用:

$ItemArray | Where-Object { $exclusionArray -notcontains $_ }

输出:

a, d

给你的任务起一个名字:你正在寻找两个数组之间的relative complement又名设置差异

在集合论表示法中,它将是 $ItemArray \ $ExclusionArray,即 $ItemArray 中不在 $ExclusionArray.

中的那些元素

This related question is looking for the symmetric difference 两组之间,即 任一侧 唯一的一组元素 - 最后这就是基于 Compare-Object 的解决方案有实现,但前提是每个数组都没有重复项


概念简单明了

一个潜在的问题是性能:必须对排除数组中的每个元素执行查找输入数组.

对于小型阵列,这在实践中可能无关紧要。

对于更大的数组,LINQ 提供了一个明显更快的解决方案:

注意为了受益于 LINQ 解决方案,您的数组应该已经在内存中,排除数组越大,收益越大。如果您的输入通过管道流式传输,则执行管道的开销可能会使优化数组处理的尝试毫无意义甚至适得其反,在这种情况下,坚持使用本机 PowerShell 解决方案是有意义的 - 请参阅 .

# Declare the arrays as [string[]]
# so that calling the LINQ method below works as-is.
# (You could also cast to [string[]] ad hoc.)
[string[]] $ItemArray = 'a','b','c','d'
[string[]] $exclusionArray = 'b','c'

# Return only those elements in $ItemArray that aren't also in $exclusionArray
# and convert the result (a lazy enumerable of type [IEnumerable[string]])
# back to an array to force its evaluation
# (If you directly enumerate the result in a pipeline, that step isn't needed.)
[string[]] [Linq.Enumerable]::Except($ItemArray, $exclusionArray) # -> 'a', 'd'

请注意,需要通过静态方法显式使用 LINQ 类型,因为从 v7 开始,PowerShell 不支持 扩展方法 。 但是,有一个 proposal on GitHub to add such support; this related proposal 要求改进对调用泛型方法的支持。

有关当前如何从 PowerShell 调用 LINQ 方法的概述,请参阅


性能比较:

iRon 致敬。

以下基准代码使用 Time-Command function 来比较这两种方法,分别使用具有大约 4000 和 2000 个元素的数组,正如问题中一样,它们仅相差 2 个元素。

请注意,为了公平竞争,使用.Where() 数组方法 (PSv4+) 而不是基于管道的Where-Object cmdlet,因为 .Where() 对于已经在内存中的数组更快。

这是 10 运行 秒的平均结果;注意相对性能,如 Factor 列中所示;来自单核 Windows 10 VM 运行ning Windows PowerShell v5.1.:

Factor Secs (10-run avg.) Command                              TimeSpan
------ ------------------ -------                              --------
1.00   0.046              # LINQ...                            00:00:00.0455381
8.40   0.382              # Where ... -notContains...          00:00:00.3824038

LINQ 解决方案要快得多——快了 8 倍以上(尽管更慢的解决方案只用了大约 0.4 秒就达到 运行)。

PowerShell Core 中的性能差距似乎更大,我发现 v7.0.0-preview.4 的性能差距约为 19。有趣的是,这两个测试 运行 单独比在 Windows PowerShell 中更快。

基准代码:

# Script block to initialize the arrays.
# The filler arrays are randomized to eliminate caching effects in LINQ.
$init = {
  $fillerArray = 1..1000 | Get-Random -Count 1000
  [string[]] $ItemArray = $fillerArray + 'a' + $fillerArray + 'b' + $fillerArray + 'c' + $fillerArray + 'd'
  [string[]] $exclusionArray = $fillerArray + 'b' + $fillerArray + 'c'
}

# Compare the average of 10 runs.
Time-Command -Count 10 { # LINQ
  . $init
  $result = [string[]] [Linq.Enumerable]::Except($ItemArray, $exclusionArray)
}, { # Where ... -notContains
  . $init
  $result = $ItemArray.Where({ $exclusionArray -notcontains $_ })
}

提倡原生PowerShell:
根据 @mklement0's answer, with no doubt, Language Integrated Query (LINQ) 是 //Fast...
但在某些情况下,@EylM 建议的使用管道的本机 PowerShell 命令仍然可以击败 LINQ。这不仅是理论上的,而且在相关进程空闲并等待缓慢输入的用例中可能会发生。例如。输入来自哪里:

  • 远程服务器(例如 Active Directory)
  • 一个慢速设备
  • 必须进行复杂计算的单独线程
  • 互联网 ...

尽管我还没有看到 easy 证明这一点,但在几个网站上都建议这样做,并且可以从网站中扣除,例如High Performance PowerShell with LINQ and Ins and Outs of the PowerShell Pipeline.

证明

为了证明上述论点,我创建了一个小的 Slack cmdlet,它会减慢放入管道的每个项目的速度 1 毫秒(默认情况下):

Function Slack-Object ($Delay = 1) {
    process {
        Start-Sleep -Milliseconds $Delay
        Write-Output $_
    }
}; Set-Alias Slack Slack-Object

现在让我们看看原生 PowerShell 是否真的可以打败 LINQ:
(为了获得良好的性能比较,应通过例如启动新的 PowerShell 会话来清除缓存。)

[string[]] $InputArray = 1..200
[string[]] $ExclusionArray = 100..300

(Measure-Command {
    $Result = [Linq.Enumerable]::Except([string[]] ($InputArray | Slack), $ExclusionArray)
}).TotalMilliseconds

(Measure-Command {
    $Result = $InputArray | Slack | Where-Object {$ExclusionArray -notcontains $_}
}).TotalMilliseconds

结果:

      LINQ: 411,3721
PowerShell: 366,961

要排除 LINQ 缓存,应该进行单个 运行 测试,但正如@mklement0 所评论的那样,单个 运行 的结果可能每个 运行.
结果还高度依赖于输入数组的大小、结果的大小、松弛度、测试系统等。

结论:

PowerShell might still be faster than LINQ in some scenarios!

引用mklement0的评论:
"总的来说,可以说在这种情况下性能差异很小,不值得选择基于性能的方法 - 使用更多的 PowerShell 是有意义的 -类似方法 (Where-Object),因为 LINQ 方法远非显而易见。底线是:仅当内存中已经有大型数组时才选择 LINQ。如果涉及管道,单独的管道开销可能让优化变得毫无意义。"