Loop foreach com interrupção / retorno vs. loop while com invariante explícito e pós-condição


17

Esta é a maneira mais popular (me parece) de verificar se um valor está em uma matriz:

for (int x : array)
{
    if (x == value)
        return true;
}
return false;        

No entanto, em um livro que li há muitos anos por, provavelmente, Wirth ou Dijkstra, foi dito que esse estilo é melhor (quando comparado a um loop while com uma saída interna):

int i = 0;
while (i < array.length && array[i] != value)
    i++;
return i < array.length;

Dessa forma, a condição de saída adicional se torna uma parte explícita do invariante do loop, não há condições ocultas e sai dentro do loop, tudo é mais óbvio e mais de uma maneira de programação estruturada. Eu geralmente preferia esse último padrão sempre que possível e usei o forloop para iterar apenas de apara b.

E, no entanto, não posso dizer que a primeira versão seja menos clara. Talvez seja ainda mais claro e fácil de entender, pelo menos para iniciantes. Então, eu ainda estou me perguntando qual é o melhor?

Talvez alguém possa dar uma boa justificativa em favor de um dos métodos?

Atualização: Não se trata de pontos de retorno de múltiplas funções, lambdas ou localização de um elemento em uma matriz em si. É sobre como escrever loops com invariantes mais complexos do que uma única desigualdade.

Atualização: OK, eu vejo o ponto das pessoas que respondem e comentam: eu misturei o loop foreach aqui, que já é muito mais claro e legível do que um loop while. Eu não deveria ter feito isso. Mas essa também é uma pergunta interessante, então vamos deixar como está: loop foreach e uma condição extra por dentro, ou um loop while com um loop explícito invariável e uma pós-condição depois. Parece que o loop foreach com uma condição e uma saída / interrupção está vencendo. Vou criar uma pergunta adicional sem o loop foreach (para uma lista vinculada).


2
Os exemplos de código citados aqui misturam vários problemas diferentes. Retornos iniciais e múltiplos (que para mim vão para o tamanho do método (não mostrado)), pesquisa de matriz (que implora por uma discussão envolvendo lambdas), foreach x indexação direta ... Essa questão seria mais clara e fácil de resolver. responda se se concentrou em apenas um desses problemas de cada vez.
Erik Eidt


1
Eu sei que são exemplos, mas existem idiomas que possuem APIs para lidar exatamente com esse caso de uso. Ou sejacollection.contains(foo)
Berin Loritsch

2
Você pode encontrar o livro e relê-lo agora para ver o que ele realmente disse.
Thorbjørn Ravn Andersen

1
"Melhor" é uma palavra altamente subjetiva. Dito isto, pode-se dizer de relance o que a primeira versão está fazendo. Que a segunda versão faça exatamente a mesma coisa requer algum exame.
David Hammen

Respostas:


19

Eu acho que para loops simples, como esses, a primeira sintaxe padrão é muito mais clara. Algumas pessoas consideram vários retornos confusos ou com cheiro de código, mas para um pedaço de código tão pequeno, não acredito que seja um problema real.

Fica um pouco mais discutível para loops mais complexos. Se o conteúdo do loop não puder caber na tela e apresentar vários retornos, é possível argumentar que os vários pontos de saída podem dificultar a manutenção do código. Por exemplo, se você tivesse que garantir que algum método de manutenção de estado fosse executado antes de sair da função, seria fácil deixar de adicioná-lo a uma das instruções de retorno e você causaria um erro. Se todas as condições finais puderem ser verificadas em um loop while, você terá apenas um ponto de saída e poderá adicionar esse código depois dele.

Dito isto, especialmente com loops, é bom tentar colocar o máximo de lógica possível em métodos separados. Isso evita muitos casos em que o segundo método teria vantagens. Loops enxutos com lógica claramente separada importarão mais do que qual desses estilos você usa. Além disso, se a maior parte da base de código do seu aplicativo estiver usando um estilo, você deve seguir esse estilo.


56

Isso é facil.

Quase nada importa mais do que clareza para o leitor. A primeira variante que achei incrivelmente simples e clara.

Na segunda versão 'aprimorada', tive que ler várias vezes e ter certeza de que todas as condições da borda estavam corretas.

Existe o ZERO DOUBT, que é o melhor estilo de codificação (o primeiro é muito melhor).

Agora - o que é CLARO para as pessoas pode variar de pessoa para pessoa. Não tenho certeza de que existem padrões objetivos para isso (embora postar em um fórum como esse e obter uma variedade de contribuições das pessoas possa ajudar).

Nesse caso em particular, no entanto, posso dizer por que o primeiro algoritmo é mais claro: sei como é a iteração C ++ na sintaxe de um contêiner. Eu o internalizei. Alguém UNFAMILIAR (sua nova sintaxe) com essa sintaxe pode preferir a segunda variação.

Mas uma vez que você conhece e entende essa nova sintaxe, é um conceito básico que você pode usar. Com a abordagem de iteração de loop (segunda), é necessário verificar cuidadosamente se o usuário está verificando CORRETAMENTE todas as condições de borda para fazer loop em toda a matriz (por exemplo, menos do que em vez do mesmo índice menor ou igual, usado para teste e para indexação etc).


4
O novo é relativo, como já estava no padrão de 2011. Além disso, a segunda demonstração obviamente não é C ++.
Deduplicator

Uma solução alternativa se você quiser usar um único ponto de saída seria a criação de uma bandeira longerLength = true, então return longerLength.
Cullub

@Duplicator Por que a segunda demonstração C ++ não é? Não vejo por que não ou estou perdendo algo óbvio?
Rakete1111

2
@ Rakete1111 As matrizes brutas não têm propriedades como length. Se realmente fosse declarado como uma matriz e não um ponteiro, eles poderiam usar sizeofou, se fosse uma std::array, a função de membro correta é size(), não há uma lengthpropriedade.
precisa saber é o seguinte

@ IllusiveBrian: sizeofestaria em bytes ... O mais genérico desde C ++ 17 é std::size().
Deduplicator

9
int i = 0;
while (i < array.length && array[i] != value)
    i++;
return i < array.length;

[...] tudo é mais óbvio e mais de uma maneira de programação estruturada.

Nem tanto. A variável iexiste fora do loop while aqui e, portanto, faz parte do escopo externo, enquanto (trocadilho intencional) xdo forloop existe apenas dentro do escopo do loop. O escopo é uma maneira muito importante de introduzir estrutura na programação.



1
@ruakh Não sei ao certo o que tirar do seu comentário. Parece um pouco passivo-agressivo, como se minha resposta se opusesse ao que está escrito na página da wiki. Por favor elabore.
Nulo

"Programação estruturada" é um termo de arte com um significado específico, e o OP está objetivamente correto de que a versão nº 2 esteja em conformidade com as regras da programação estruturada, enquanto a versão nº 1 não. Pela sua resposta, parecia que você não estava familiarizado com o termo da arte e estava interpretando o termo literalmente. Não sei por que meu comentário parece passivo-agressivo; Eu quis dizer isso simplesmente como informativo.
Ruakh 14/08/19

@ruakh Não concordo que a versão 2 esteja em conformidade com as regras mais em todos os aspectos e expliquei isso na minha resposta.
Nulo

Você diz "eu discordo" como se fosse uma coisa subjetiva, mas não é. Retornar de dentro de um loop é uma violação categórica das regras da programação estruturada. Tenho certeza de que muitos entusiastas da programação estruturada são fãs de variáveis ​​de escopo mínimo, mas se você reduz o escopo de uma variável afastando-se da programação estruturada, você sai da programação estruturada, ponto e reduzir o escopo da variável não desfaz naquela.
Ruakh 14/08/19

2

Os dois loops têm semânticas diferentes:

  • O primeiro loop simplesmente responde a uma pergunta simples de sim / não: "A matriz contém o objeto que estou procurando?" Fá-lo da maneira mais breve possível.

  • O segundo loop responde à pergunta: "Se a matriz contiver o objeto que estou procurando, qual é o índice da primeira correspondência?" Mais uma vez, faz isso da maneira mais breve possível.

Como a resposta para a segunda pergunta fornece estritamente mais informações do que a resposta para a primeira, você pode optar por responder à segunda e depois derivar a resposta da primeira. É o que a linha return i < array.length;faz, de qualquer maneira.

Acredito que geralmente é melhor usar apenas a ferramenta adequada ao objetivo, a menos que você possa reutilizar uma ferramenta já existente e mais flexível. Ou seja:

  • Usar a primeira variante do loop é bom.
  • Alterar a primeira variante para definir apenas uma boolvariável e quebra também é bom. (Evita a segunda returndeclaração, a resposta está disponível em uma variável em vez de em um retorno de função.)
  • O uso std::findé bom (reutilização de código!).
  • No entanto, codificar explicitamente uma descoberta e reduzir a resposta a boolnão são.

Seria bom se os downvoters deixassem um comentário ...
cmaster - reinstate monica

2

Vou sugerir uma terceira opção completamente:

return array.find(value);

Existem várias razões para iterar sobre uma matriz: verifique se existe um valor específico, transforme a matriz em outra matriz, calcule um valor agregado, filtre alguns valores da matriz ... Se você usar um loop for simples, não está claro rapidamente, especificamente como o loop for está sendo usado. No entanto, a maioria das linguagens modernas possui APIs ricas em suas estruturas de dados de matriz que tornam essas diferentes intenções muito explícitas.

Compare a transformação de uma matriz em outra com um loop for:

int[] doubledArray = new int[array.length];
for (int i = 0; i < array.length; i++) {
  doubledArray[i] = array[i] * 2;
}

e usando uma mapfunção no estilo JavaScript :

array.map((value) => value * 2);

Ou somando uma matriz:

int sum = 0;
for (int i = 0; i < array.length; i++) {
  sum += array[i];
}

versus:

array.reduce(
  (sum, nextValue) => sum + nextValue,
  0
);

Quanto tempo você leva para entender o que isso faz?

int[] newArray = new int[array.length];
int numValuesAdded = 0;

for (int i = 0; i < array.length; i++) {
  if (array[i] >= 0) {
    newArray[numValuesAdded] = array[i];
    numValuesAdded++;
  }
}

versus

array.filter((value) => (value >= 0));

Nos três casos, enquanto o loop for certamente é legível, você precisa dedicar alguns momentos para descobrir como o loop for está sendo usado e verificar se todos os contadores e condições de saída estão corretos. As funções modernas no estilo lambda tornam os objetivos dos loops extremamente explícitos e você sabe ao certo que as funções da API que estão sendo chamadas foram implementadas corretamente.

A maioria das linguagens modernas, incluindo JavaScript , Ruby , C # e Java , usa esse estilo de interação funcional com matrizes e coleções semelhantes.

Em geral, embora eu não ache que o uso de loops seja necessariamente errado e seja uma questão de gosto pessoal, tenho me sentido bastante favorável ao uso desse estilo de trabalhar com matrizes. Isto é especificamente devido à maior clareza na determinação do que cada loop está fazendo. Se seu idioma possui recursos ou ferramentas semelhantes em suas bibliotecas padrão, sugiro que você considere adotar esse estilo também!


2
A recomendação array.findsugere a pergunta, pois precisamos discutir a melhor maneira de implementar array.find. A menos que você esteja usando hardware com uma findoperação integrada , precisamos escrever um loop lá.
Barmar

2
@ Barmar eu discordo. Como indiquei na minha resposta, muitas linguagens muito usadas fornecem funções como findem suas bibliotecas padrão. Sem dúvida, essas bibliotecas implementam finde seus parentes usam loops, mas é isso que uma boa função faz: abstrai os detalhes técnicos do consumidor da função, permitindo que o programador não precise pensar nesses detalhes. Portanto, mesmo que findseja provavelmente implementado com um loop for, ele ainda ajuda a tornar o código mais legível e, como é frequente na biblioteca padrão, o uso dele não adiciona sobrecarga ou risco significativos.
Kevin - Restabelece Monica

4
Mas um engenheiro de software precisa implementar essas bibliotecas. Os mesmos princípios de engenharia de software não se aplicam aos autores de bibliotecas que os programadores de aplicativos? A questão é sobre loops de escrita em geral, não é a melhor maneira de procurar um elemento de matriz em um determinado idioma
Barmar

4
Em outras palavras, procurar um elemento da matriz é apenas um exemplo simples que ele usou para demonstrar as diferentes técnicas de loop.
Barmar

-2

Tudo se resume exatamente ao que se entende por "melhor". Para programadores práticos, geralmente significa eficiente - ou seja, neste caso, sair diretamente do loop evita uma comparação extra e retornar uma constante booleana evita uma comparação duplicada; isso economiza ciclos. Dijkstra está mais preocupado em criar código que seja mais fácil de provar correto . [pareceu-me que a educação em CS na Europa leva o 'teste de correção de código' muito mais a sério do que a educação em CS nos EUA, onde as forças econômicas tendem a dominar a prática de codificação]


3
Em geral, os dois loops em termos de desempenho são praticamente equivalentes - ambos têm duas comparações.
Danila Piatov 10/08/19

1
Se alguém realmente se importasse com o desempenho, usaria um algoritmo mais rápido. por exemplo, classifique o array e faça uma pesquisa binária ou use um Hashtable.
user949300

Danila - você não sabe qual estrutura de dados está por trás disso. Um iterador é sempre rápido. O acesso indexado pode ter um tempo linear, e o comprimento nem precisa existir.
gnasher729
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.