Можно ли упростить следующий вложенный цикл foreach в 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
3 ответа
Вы можете использовать Where-Object
с -notcontains
:
$ItemArray | Where-Object { $exclusionArray -notcontains $_ }
Выход:
a, d
Чтобы дать вашей задаче имя: вы ищете относительное дополнение, также известное как разница между двумя массивами:
В обозначениях теории множеств это было бы $ItemArray \ $ExclusionArray
, т. е. те элементы в $ItemArray
которых также нет в $ExclusionArray
.
Этот связанный вопрос ищет симметричную разницу между двумя наборами, то есть набор элементов, которые уникальны для каждой стороны - наконец, это то, чтоCompare-Object
Решения на их основе там реализуются, но только при условии, что каждый массив не имеет дубликатов.
Полезно ответ EyIM в это концептуально простой и лаконичный.
Потенциальная проблема производительности: а поиск в массиве исключения должны быть выполнены для каждого элемента во входном массиве.
С небольшими массивами на практике это вряд ли будет иметь значение.
С большими массивами LINQ предлагает значительно более быстрое решение:
Примечание. Чтобы воспользоваться решением LINQ, ваши массивы должны быть уже в памяти, и преимущество тем больше, чем больше массив исключений. Если ваши входные данные передаются через конвейер, накладные расходы на выполнение конвейера могут сделать попытки оптимизации обработки массива бессмысленными или даже контрпродуктивными, и в этом случае имеет смысл придерживаться собственного решения PowerShell - см . Ответ iRon.
# 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 с помощью их статических методов, поскольку PowerShell, начиная с версии 7, не поддерживает методы расширения. Однако на GitHub есть предложение добавить такую поддержку; это связанное предложение требует улучшенной поддержки для вызова общих методов.
См. Этот ответ для обзора того, как в настоящее время вызывать методы LINQ из PowerShell.
Сравнение производительности:
Совет iRon за его вклад.
В следующем тестовом коде используется Time-Command
функция для сравнения двух подходов, используя массивы примерно с 4000 и 2000 элементов, соответственно, которые, как в вопросе, отличаются всего на 2 элемента.
Обратите внимание, что для выравнивания игрового поля .Where()
метод массива (PSv4+) используется вместо конвейерногоWhere-Object
командлет, как.Where()
быстрее с массивами, уже находящимися в памяти.
Вот результаты, усредненные по 10 запускам; обратите внимание на относительную производительность, как показано наFactor
колонны; с одноядерной виртуальной машины Windows 10 под управлением 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, где я видел коэффициент около 19 с v7.0.0-preview.4.; Интересно, что оба теста по отдельности выполнялись быстрее, чем в 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 $_ })
}
mklement0 PowerShell:
в mklement0 с ответом mklement0, без сомнения, Language Integrated Query (LINQ) является / / быстрым...
Но в некоторых обстоятельствах собственные команды PowerShell, использующие конвейер, предложенный EylM все же могут превзойти LINQ. Это не только теоретически, но и может произойти в тех случаях, когда соответствующий процесс простаивает и ожидает медленного ввода. Например, откуда поступает ввод:
- Удаленный сервер (например, Active Directory)
- Медленное устройство
- Отдельный поток, который должен произвести сложный расчет
- Интернет...
Несмотря на то, что я еще не видел простого доказательства этого, это предлагается на нескольких сайтах и может быть вычтено из таких сайтов, как, например, High Performance PowerShell с LINQ и входы и выходы конвейера PowerShell.
Доказать
Чтобы доказать вышеизложенный тезис, я создал небольшой Slack
командлет, который замедляет каждый элемент, попадающий в конвейер, на 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 может быть быстрее LINQ в некоторых сценариях!
Цитата mklement0 комментария mklement0:
" В целом, будет справедливо сказать, что разница в производительности в этом сценарии настолько мала, что не стоит выбирать подход, основанный на производительности, - и имеет смысл использовать подход, более похожий на PowerShell ( где -Object), учитывая, что подход LINQ далеко не очевиден. Суть в следующем: выбирайте LINQ только в том случае, если у вас есть большие массивы, которые уже находятся в памяти. Если задействован конвейер, одни накладные расходы конвейера могут сделать оптимизацию бессмысленной ".