Por que 'continue' se comporta como 'quebra' em um objeto Foreach?

123

Se eu fizer o seguinte em um script do PowerShell:

$range = 1..100
ForEach ($_ in $range) {
    if ($_ % 7 -ne 0 ) { continue; }
    Write-Host "$($_) is a multiple of 7"
}

Eu recebo a saída esperada de:

7 is a multiple of 7
14 is a multiple of 7
21 is a multiple of 7
28 is a multiple of 7
35 is a multiple of 7
42 is a multiple of 7
49 is a multiple of 7
56 is a multiple of 7
63 is a multiple of 7
70 is a multiple of 7
77 is a multiple of 7
84 is a multiple of 7
91 is a multiple of 7
98 is a multiple of 7

No entanto, se eu usar um pipeline e ForEach-Object, continueparece romper o loop do pipeline.

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

Posso ter um continuecomportamento semelhante enquanto ainda faço o ForEach-Object, para não precisar interromper meu pipeline?

Justin Dearing
fonte
Aqui está uma página com muitos comandos para uso com foreach: techotopia.com/index.php/...
bgmCoder
Encontrado uma explicação decente e amostra aqui ... powershell.com/cs/blogs/tips/archive/2015/04/27/...
Nathan Hartley

Respostas:

164

Basta usar o em returnvez do continue. Isso returnretorna do bloco de script que é chamado por ForEach-Objectuma iteração específica e, portanto, simula o continueloop.

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

Há uma pegadinha a ser lembrada ao refatorar. Às vezes, alguém deseja converter um foreachbloco de instruções em um pipeline com um ForEach-Objectcmdlet (ele ainda possui o alias foreachque ajuda a facilitar essa conversão e também a cometer erros). Todos os continues devem ser substituídos por return.

PS: Infelizmente, não é assim tão fácil de simular breakem ForEach-Object.

Roman Kuzmin
fonte
2
Pelo que OP está dizendo, aparentemente, continuepode ser usado para simular um breakem ForEach-Object:)
Richard Hauer
6
@ Richard Hauer Tal continuevai quebrar todo o script, não apenas ForEach-Objectonde é usado.
Roman Kuzmin
22

Porque For-Eachobjeto é um cmdlet e não um loop e continuee breaknão se aplicam a ele.

Por exemplo, se você tiver:

$b = 1,2,3

foreach($a in $b) {

    $a | foreach { if ($_ -eq 2) {continue;} else {Write-Host $_} }

    Write-Host  "after"
}

Você obterá a saída como:

1
after
3
after

Isso ocorre porque continueé aplicado ao loop foreach externo e não ao cmdlet foreach-object. Na ausência de um loop, o nível mais externo, dando a você uma impressão de que ele age como esse break.

Então, como você consegue um continuecomportamento semelhante? Uma maneira é o Where-Object, é claro:

1..100 | ?{ $_ % 7  -eq 0} | %{Write-Host $_ is a multiple of 7}
manojlds
fonte
Usar o cmdlet Where-Object é uma boa sugestão. No meu caso atual, não acho que faça sentido transformar as várias linhas de código que precedem minha instrução if em uma única linha longa de código difícil de ler. No entanto, isso funcionaria para mim em outras situações.
quer
@JustinDearing - In my actual case, I don't think it makes sense to make the multiple lines of code preceding my if statement into a single long line of hard to read code.Como assim?
manojlds
3
@manojlds talvez ele pense que sua solução de uma linha é "difícil de ler", pelo menos para mim é completamente o contrário. A maneira mais eficiente de fazer as coisas é realmente poderosa e clara e é a abordagem correta para coisas simples como essa. Escrever código no shell sem tirar proveito disso é inútil.
mjsr
No meu caso, essa foi a resposta certa, adicione uma condição where para filtrar os objetos nos quais eu continuaria ou retornaria, para que não seja necessário processá-los. +1
Chris Magnuson
3

Outra alternativa é uma espécie de hack, mas você pode agrupar seu bloco em um loop que será executado uma vez. Dessa forma, continueterá o efeito desejado:

1..100 | ForEach-Object {
    for ($cont=$true; $cont; $cont=$false) {
        if ($_ % 7 -ne 0 ) { continue; }
        Write-Host "$($_) is a multiple of 7"
    }
}
zdan
fonte
4
Francamente, isso é feio :) E não apenas um hack porque, em vez do objeto foreach, você também poderia ter usado um loop foreach.
manojlds
1
@manojlds: 1..100 é apenas para ilustração. do {} while ($ False) funciona tão bem quanto para loop e um pouco mais intuitivo.
Harry Martyrossian
2

Uma elsedeclaração simples faz com que funcione como em:

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

Ou em um único pipeline:

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

Mas uma solução mais elegante é inverter seu teste e gerar resultados apenas para seus sucessos

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