Поиск по сайту:

Объяснение PowerShell ForEach-Object и цикла ForEach


Циклы PowerShell ForEach и ForEach-Objects являются одними из самых часто используемых функций. Они позволяют вам легко перебирать коллекции. Но в чем разница между ForEach-Object и ForEach() командлетами?

Обе команды выглядят похоже, и, по сути, они позволяют выполнять одно и то же действие. Но знание разницы между ними позволяет вам улучшить ваш скрипт PowerShell.

В этой статье мы рассмотрим, как использовать PowerShell ForEach и ForEach-Object, а также объясним разницу между ними.

ForEach() против ForEach-Object

Команды PowerShell ForEach() и ForEach-Object обе позволяют вам проходить через коллекцию и выполнять действие над каждым элементом в ней. Однако есть разница в том, как они обрабатывают данные и в удобстве использования.

Cmdlet ForEach() загружает всю коллекцию в память, прежде чем начать обработку элементов. Загружая коллекцию в память вашего компьютера, он работает быстрее, чем cmdlet ForEach-Object.

foreach ($fruit in $fruitsAvailable) {
    Write-Host $fruit
}

С другой стороны, командлет PowerShell ForEach-Object также позволяет вам передавать его за другим командлетом. Таким образом, он может обрабатывать каждый элемент по мере его поступления в конвейер. Это также означает, что вы можете передавать другие командлеты за ним.

$fruitsAvailable | ForEach-Object {
    Write-Host $_
}

Кроме этих двух командлетов, у нас также есть два псевдонима ForEach (обратите внимание, без скобок () ) и %. Оба являются псевдонимами для командлета ForEach-Object, но это может быть немного запутанным иногда. Все команды ниже являются одинаковыми:

$fruitsAvailable | ForEach-Object {
    Write-Host $_
}

$fruitsAvailable | ForEach {
    Write-Host $_
}

$fruitsAvailable | % {
    Write-Host $_
}

Теперь для небольших коллекций разница в производительности и использовании памяти минимальна. В приведенном выше примере оператор ForEach() только на 4 мс быстрее, и блок скрипта легче читать. Однако при работе с большими коллекциями стоит учитывать использование памяти.

Когда вы загружаете большой CSV-файл, например, более эффективно использовать конвейер cmdlet ForEach-Object после него, вместо того чтобы сначала сохранять результаты в переменной:

Import-Csv -Delimiter ";" -Path c:\temp\EmailAddresses.csv | Foreach-Object {Get-ADUser -Identity $_.Name}

PowerShell ForEach

Оператор ForEach в PowerShell позволяет вам перебирать массив или список элементов. Синтаксис оператора ForEach довольно прост:

foreach ($item in $collection) {
   # Script to execute with $item
}

Как вы можете видеть, мы можем указать коллекцию, по которой мы хотим пройтись с помощью цикла, это может быть массив или список, и мы указываем переменную для итератора. Эта переменная будет содержать значение каждого элемента, находящегося внутри коллекции.

Например, чтобы вывести каждый фрукт в консоль, мы можем сделать следующее:

$fruitsAvailable = @("Apple", "Banana", "Orange", "Grapes", "Mango", "Strawberry", "Pineapple")

foreach ($fruit in $fruitsAvailable) { 
   Write-Host $fruit 
}

Использование коллекций

Мы также можем использовать оператор ForEach PowerShell с командлетом, который возвращает коллекцию. Большинство используют командлет ForEach-Object после командлета, но это не всегда необходимо. Преимущество использования оператора ForEach заключается в том, что его легче читать по сравнению с командлетом ForEach-Object.

Например, командлет Get-ChildItem возвращает все файлы и папки из заданного пути. Чтобы перебрать файлы, мы можем сделать следующее:

foreach ($item in Get-ChildItem -path "c:\temp") {
    Write-Host $item.Name
}

Как вы видите, Get-ChildItem возвращает объект файла или папки. Внутри блока скрипта мы можем получить доступ ко всем свойствам элемента, в данном случае имени элемента.

Перерыв и продолжение

При работе с циклами ForEach в PowerShell иногда нужно остановить итерации, когда найдено определенное значение, или пропустить элемент на основе его значения. Для этого мы можем использовать ключевые слова Break и Continue.

Например, если мы хотим проверить, делится ли значение на 7 без остатка. Если нет, то продолжаем к следующему элементу:

foreach ($i in 1..100) {
    if ($i % 7 -ne 0 ) { continue }
    Write-Host "$i is a multiple of 7"
}

Ключевое слово Break позволяет останавливать цикл foreach в PowerShell, когда выполняется определенное условие. Я не использую их часто, чаще всего вы можете написать лучший код, используя cmdlet where-object или подходящие фильтры. Но ниже приведен пример, чтобы дать вам представление о том, как вы могли бы использовать ключевое слово Break:

foreach ($number in 1..10) {
    if ($number -gt 5) {
        Write-Host "Exiting the loop because $number is greater than 5"
        break
    }

    Write-Host "Processing number: $number"
}

Вложенные циклы ForEach

Внутри блока скрипта вы также можете использовать другие операторы foreach. Старайтесь держать их на минимальном уровне, потому что при использовании слишком многих вложенных циклов foreach ваш код станет труднее читать и отлаживать.

Рассмотрим следующий пример: у нас есть массив команд, и внутри каждой команды есть массив игроков. Внешний цикл foreach проходит по каждой команде в массиве $teams. Внутренний цикл foreach затем проходит по игрокам внутри каждой команды.

Define an array of teams, where each team has an array of players
$teams = @(
    @{
        TeamName = "Team A"
        Players  = @("Player1A", "Player2A", "Player3A")
    },
    @{
        TeamName = "Team B"
        Players  = @("Player1B", "Player2B", "Player3B")
    },
    @{
        TeamName = "Team C"
        Players  = @("Player1C", "Player2C", "Player3C")
    }
)

Nested foreach loop to iterate over teams and players
foreach ($team in $teams) {
    Write-Host "Team: $($team.TeamName)"

    foreach ($player in $team.Players) {
        Write-Host "  Player: $player"
    }
}

PowerShell ДляКаждогоОбъекта

Команда PowerShell ForEach-Object используется, когда вы хотите пройти через элементы, которые поступают из конвейера. Преимущество ForEach-Object заключается в том, что она начинает обрабатывать элементы по мере их поступления в конвейер, и вы также можете передавать другие команды за ней.

Вы хотите использовать этот командлет, когда работаете с большими коллекциями или наборами данных, потому что, хотя он и медленнее, чем оператор foreach, он не потребляет так много памяти.

Еще одно преимущество ForEach-Object заключается в том, что вы можете ускорить его с помощью параметра параллельности в PowerShell 7. Это позволяет обрабатывать несколько элементов одновременно, но об этом мы поговорим позже.

Как упоминалось, командлет ForEach-Object подключается через пайп к другому командлету. Вы можете использовать один (или несколько) блоков сценариев, чтобы указать операцию, которую вы хотите выполнить над текущим элементом. Например, чтобы получить все имена процессов работающих процессов, вы можете сделать это:

Get-Process | ForEach-Object {$_.ProcessName}

Как вы можете видеть в приведенном выше примере, мы ссылаемся на текущий элемент, используя переменную $_.

Вы можете соединять другие команды за блоком скрипта PowerShell ForEach-Object. Так что, если вы хотите сохранить каждое имя процесса в текстовом файле, вы могли бы сделать следующее:

Get-Process | ForEach-Object {$_.ProcessName} | Out-File "c:\temp\runningprocs.txt"

В приведенных выше примерах мы выполняем только небольшой скрипт в блоке скрипта, который позволяет нам писать всё в одной строке. Но вы также можете добавлять более крупные скрипты внутри блока скрипта:

$files = Get-ChildItem -Path "C:\temp\" -File

$files | ForEach-Object {
    $fileName = $_.Name
    $fileSizeKB = [math]::Round($_.Length / 1KB, 2)

    Write-Host "Processing file: $fileName"
    Write-Host "File size: $fileSizeKB KB"

    Write-Host "Processing complete for $fileName"
}

Начало, Конец и несколько блоков скриптов

При использовании командлета ForEach-Object вы можете использовать блоки скриптов begin и end. Эти блоки скриптов выполняются только один раз, в то время как обычный блок скриптов (процессный блок скриптов) выполняется для каждого элемента в коллекции.

Блоки скриптов "Начало" и "Конец" отличны, когда вам нужно зарегистрировать время начала и окончания скрипта или когда вы хотите зарегистрировать начало или окончание процесса:

1..5 | ForEach-Object -Begin {
    Write-Host "Starting the processing..."
} -Process {
    $squared = $_ * $_
    Write-Host "Number: $_, Squared: $squared"
} -End {
    Write-Host "Processing complete."
}

В приведенном выше примере мы обозначили каждый блок скрипта тем, что он собой представляет: блок начала, процесса и конца. Но вы не обязаны это указывать (я рекомендую это делать, потому что так ваш код становится более читаемым).

Если вы укажете несколько блоков скриптов, то первый блок всегда воспринимается как начальный.

1..2 | ForEach-Object { 'Begin Block' } { 'Process Block' }

Result
Begin Block
Process Block
Process Block

Если у вас больше двух блоков, то последний всегда рассматривается как конечный блок. Каждый блок между ними является процессным блоком.

1..2 | ForEach-Object { 'Begin' } { 'Process A Block' } { 'Process B Block' } { 'End' }

Result
Begin
Process A Block
Process B Block
Process A Block
Process B Block
End

Если вы хотите запустить несколько блоков процессов, но не хотите использовать блоки begin или end, тогда вам нужно будет указать параметры begin, process и end, и сопоставить значение $null с первым и последним:

1..2 | ForEach-Object -Begin $null -Process { 'Process A Block' } { 'Process B Block' } -End $null

Result
Process A Block
Process B Block
Process A Block
Process B Block

Использование параллельной обработки

Если у вас есть действительно медленный блок скрипта или вам нужно обработать много элементов (более 10.000), вы можете использовать параметр -parallel в PowerShell 7.x и выше. Таким образом, блок скрипта будет выполняться параллельно для каждого элемента, позволяя вам обрабатывать несколько элементов одновременно.

По умолчанию параметр параллели будет обрабатывать элементы партиями по 5. Однако с помощью параметра -Trottelimit мы можем определить количество рабочих пространств, которые хотим использовать. Пример ниже будет обрабатывать 8 элементов партиями по 4:

1..8 | ForEach-Object -Parallel {
    Write-Host "Slow script to process $_"
    Start-Sleep 1
} -ThrottleLimit 4

С помощью параметра parallel создается новое пространство выполнения для каждой партии. Это означает, что если вам нужно сослаться на переменную за пределами блока скрипта, вам необходимо использовать переменную $using: для её ссылки:

$Message = "Slow script to process:"

1..8 | ForEach-Object -Parallel {
    Write-Host $using:Message $_
    Start-Sleep 1
} -ThrottleLimit 4

Теперь хорошо знать, что выполнение скриптов параллельно не всегда быстрее. Честно говоря, в 98% случаев это не быстрее. Проблема выполнения вашего блока скрипта параллельно заключается в том, что на создание пространства выполнения для его выполнения уходит время.

Без параллельной функции ваш скрипт выполняется в вашем текущем потоке PowerShell. Таким образом, он имеет доступ ко всем вашим переменным, конвейеру и загруженной памяти. Но для выполнения скрипта параллельно ему потребуется запустить новый поток PowerShell (создать новое пространство выполнения), и это занимает время (примерно до 1 секунды).

Так что это имеет смысл только когда ваш скрипт действительно медленный, требующий много ресурсов ЦП, или нуждается в ожидании ответа API, к примеру.

Например, написание «Привет, мир» занимает всего несколько миллисекунд:

Measure-Command { Write-Host "Hello World" } | Select TotalMilliseconds

Result
TotalMilliseconds
-----------------
             4,65

Но при выполнении в параллельном режиме это займет почти больше 30 миллисекунд для выполнения:

Measure-Command { 1 | ForEach-Object -Parallel { Write-Host "Hello World" } } | Select TotalMilliseconds

Result
TotalMilliseconds
-----------------
             31,36

Если у нас есть скрипт, который выполняется более 1 секунды (имитируем это с помощью pause), то параллельный метод будет иметь преимущество:

Measure-Command { 1..5 | ForEach-Object { Start-Sleep -Seconds 1 } } | Select TotalMilliseconds

Result
TotalMilliseconds
-----------------
          5054,55

Execute in parallel, with the 5 default threads:
Measure-Command { 1..5 | ForEach-Object -Parallel { Start-Sleep -Seconds 1 } } | Select TotalMilliseconds

Result
TotalMilliseconds
-----------------
          1082,24

Перерыв, Продолжить и Вернуть

Так же, как и с оператором foreach, мы можем использовать Break для остановки цикла ForEach-Object в PowerShell, но Continue использовать нельзя. Оператор break полностью остановит итерацию в цикле ForEach-Object и выйдет из цикла:

1..10 | ForEach-Object {
    if ($_ -gt 5) {
        Write-Host "Exiting the loop because $_ is greater than 5"
        break
    }

    Write-Host "Processing number: $_"
}

Однако, если мы попробуем использовать Continue внутри цикла ForEach-Object, вы увидите, что он также останавливается и выходит из цикла. Поэтому приведенный ниже пример не сработает:

This won't work
1..100 | ForEach-Object {
    if ($_ % 7 -ne 0 ) { continue }
    Write-Host "$_ is a multiple of 7"
}

Вместо этого нам нужно будет использовать ключевое слово return, чтобы перейти к следующему элементу в коллекции.

1..100 | ForEach-Object {
    if ($_ % 7 -ne 0 ) { return}
    Write-Host "$_ is a multiple of 7"
}

Заключение

В большинстве случаев использование оператора foreach() является самым быстрым и читаемым способом использования цикла ForEach. Однако, если вам нужно обрабатывать большое количество данных или необходимо подключать другие командлеты после него, то лучше использовать командлет ForEach-Object.

Имейте в виду, что параллельная функция может казаться удобной для использования, но в большинстве случаев она медленнее из-за времени, необходимого для запуска нового пространства выполнения. Поэтому обязательно измерьте различия.

Надеюсь, вам понравилась эта статья. Если у вас есть вопросы, просто оставьте комментарий ниже. Подпишитесь на рассылку, если хотите читать больше таких статей!