As atribuições na condição fazem parte de condicionais uma má prática?

35

Vamos supor que eu queira escrever uma função que concatene duas seqüências de caracteres em C. A maneira como eu a escreveria é:

void concat(char s[], char t[]){
    int i = 0;
    int j = 0;

    while (s[i] != '\0'){
        i++;
    }

    while (t[j] != '\0'){
        s[i] = t[j];
        i++;
        j++;
    }

    s[i] = '\0';

}

No entanto, a K&R em seu livro a implementou de maneira diferente, particularmente incluindo o máximo possível na parte de condição do loop while:

void concat(char s[], char t[]){
    int i, j;
    i = j = 0;
    while (s[i] != '\0') i++;

    while ((s[i++]=t[j++]) != '\0');

}

Qual o caminho preferido? É encorajado ou desencorajado a escrever código da maneira que a K&R faz? Acredito que minha versão seria mais fácil de ser lida por outras pessoas.

Richard Smith
fonte
38
Não se esqueça, a K&R foi publicada pela primeira vez em 1978. Houve algumas pequenas mudanças na forma como codificamos desde então.
corsiKa
28
A legibilidade era muito diferente nos dias de tele-impressoras e editores orientados a linhas. Mushing tudo isso em uma única linha costumava ser mais legível.
User2357112 suporta Monica
15
Estou chocado que eles tenham índices e comparações com '\ 0' em vez de algo como while (*s++ = *t++); (meu C está muito enferrujado, preciso de parênteses para precedência do operador?) A K&R lançou uma nova versão do livro? O livro original deles tinha um código extremamente conciso e idiomático.
user949300
4
A legibilidade é uma coisa muito pessoal - mesmo nos dias dos teletipos. Pessoas diferentes preferem estilos diferentes. Muitas das instruções foram relacionadas à geração de código. Naqueles dias, alguns conjuntos de instruções (por exemplo, Dados Gerais) poderiam amontoar várias operações em uma instrução. Além disso, no início dos anos 80, havia um mito de que o uso de parênteses gerava mais instruções. Eu tive que gerar o assembler para provar ao revisor de código que era um mito.
copa
10
Observe que os dois blocos de código não são equivalentes. O primeiro bloco de código não copiará a terminação '\0'de t(as whilesaídas primeiro). Isso deixará a ssequência resultante sem uma finalização '\0'(a menos que o local da memória já esteja zerado). O segundo bloco de código fará a cópia da terminação '\0'antes de sair do whileloop.
Makyen 30/10/16

Respostas:

80

Sempre prefira clareza a esperteza. Nos últimos anos, o melhor programador era aquele cujo código ninguém podia entender. "Não consigo entender o código dele, ele deve ser um gênio" , disseram eles. Atualmente, o melhor programador é aquele cujo código qualquer um pode entender. O tempo do computador é mais barato agora do que o tempo do programador.

Qualquer tolo pode escrever código que um computador possa entender. Bons programadores escrevem código que os humanos podem entender. (M. Fowler)

Portanto, sem dúvida, eu optaria pela opção A. E essa é a minha resposta definitiva.

Tulains Córdova
fonte
8
Ideologia muito concisa, mas permanece o fato de que não há nada de errado com a atribuição em condicionais. É muito preferível sair antecipadamente de um loop ou duplicar o código antes e dentro de um loop.
Route de milhas
26
@MilesRout Existe. Há algo errado com qualquer código que tenha um efeito colateral no qual você não espera, por exemplo, passando argumentos de função ou avaliando condicionais. Nem mesmo mencionar que if (a=b)pode ser facilmente confundido if (a==b).
Arthur Havlicek
12
@ Lucas: "Meu IDE pode limpar o X, portanto não é um problema" não é convincente. Se não houver problema, por que o IDE facilita a "correção"?
30716 Kevin
6
@ArthurHavlicek Concordo com o seu ponto geral, mas código com efeitos colaterais em condicionais não é realmente tão incomum: while ((c = fgetc(file)) != EOF)como o primeiro que me vem à cabeça.
Daniel Jour
3
+1 "Considerando que a depuração é duas vezes mais difícil do que escrever um programa, se você é o mais inteligente possível ao escrevê-lo, como será que ele será depurado?" BWKernighan
Christophe
32

A regra de ouro, igual à da resposta de Tulains Córdova, é garantir que o código seja inteligível. Mas eu não concordo com a conclusão. Essa regra de ouro significa escrever um código que o programador típico que acabará mantendo o seu código possa entender. E você é o melhor juiz de quem é o programador típico que acabará mantendo o seu código.

Para os programadores que não começaram com C, a primeira versão é provavelmente mais fácil de entender, por razões que você já conhece.

Para aqueles que cresceram com esse estilo de C, a segunda versão pode ser mais fácil de entender: para eles, é igualmente compreensível o que o código faz, para eles, deixa menos perguntas por que está escrito do jeito que está e para eles. , menos espaço vertical significa que mais contexto pode ser exibido na tela.

Você terá que confiar no seu próprio bom senso. Para qual público você deseja tornar seu código mais fácil de entender? Esse código foi escrito para uma empresa? Então o público-alvo provavelmente são os outros programadores dessa empresa. Esse é um projeto de hobby pessoal em que ninguém trabalhará além de você? Então você é seu próprio público-alvo. Esse código você deseja compartilhar com outras pessoas? Então esses outros são o seu público-alvo. Escolha a versão que corresponde a esse público. Infelizmente, não existe uma maneira preferida única de incentivar.

hvd
fonte
14

EDIT: A linha s[i] = '\0';foi adicionada à primeira versão, corrigindo-a como descrito na variante 1 abaixo, portanto, isso não se aplica mais à versão atual do código da pergunta.

A segunda versão tem a vantagem distinta de estar correta , enquanto a primeira não está - ela não encerra a seqüência de destino corretamente.

A "atribuição em condição" permite expressar o conceito de "copiar todos os caracteres antes de verificar o caractere nulo" de maneira muito concisa e de maneira a facilitar um pouco a otimização para o compilador, embora muitos engenheiros de software hoje em dia achem esse estilo de código menos legível . Se você insistir em usar a primeira versão, terá que

  1. adicione a terminação nula após o final do segundo loop (adicionando mais código, mas você pode argumentar que a legibilidade vale a pena) ou
  2. altere o corpo do loop para "primeiro atribua, verifique ou salve o caractere atribuído e depois incremente os índices". Verificar a condição no meio do loop significa sair do loop (reduzindo a clareza, desaprovada pela maioria dos puristas). Salvar o caractere atribuído significaria introduzir uma variável temporária (reduzindo a clareza e a eficiência). Ambos aniquilariam a vantagem na minha opinião.
hjhill
fonte
Correto é melhor que legível e conciso.
user949300
5

As respostas de Tulains Córdova e hvd cobrem muito bem os aspectos de clareza / legibilidade. Deixe-me mencionar o escopo como outra razão a favor de atribuições em condições. Uma variável declarada na condição está disponível apenas no escopo dessa instrução. Você não pode usar essa variável posteriormente por acidente. O para circuito vem fazendo isso há muito tempo. E é importante o suficiente que o próximo C ++ 17 introduza uma sintaxe semelhante para if e switch :

if (int foo = bar(); foo > 42) {
    do_stuff();
}

foo = 23;   // compiler error: foo is not in scope
besc
fonte
3

Não. É um estilo C muito padrão e normal. Seu exemplo é ruim, porque deveria ser apenas um loop for, mas em geral não há nada errado com

if ((a = f()) != NULL)
    ...

por exemplo (ou com while).

Rota das milhas
fonte
7
Há algo errado com isso; `! = NULL` e seus parentes em C condicional são mais naturais, apenas para aplacar os desenvolvedores que não estão confortáveis ​​com o conceito de um valor verdadeiro ou falso (ou vice-versa).
Jonathan fundido
11
@jcast Não, é mais explícito incluir != NULL.
Route de milhas 30/10/16
11
Não, é mais explícito dizer (x != NULL) != 0. Afinal, é isso que C está realmente verificando, certo?
Jonathan fundido
@jcast Não, não é. Verificar se algo é desigual ou falso não é como você escreve condicionais em qualquer idioma.
Route de milhas 30/10/16
"Verificar se algo é desigual e falso não é como você escreve condicionais em qualquer idioma." Exatamente.
Jonathan fundido
2

Nos dias de K&R

  • 'C' era código de montagem portátil
  • Foi usado por programadores que pensavam em código de montagem
  • O compilador não fez muita otimização
  • A maioria dos computadores tinha "conjuntos de instruções complexos", por exemplo while ((s[i++]=t[j++]) != '\0'), mapearia para uma instrução na maioria das CPUs (espero que o VAC de dezembro)

Há dias

  • A maioria das pessoas que lê o código C não é programadora de código de montagem
  • Os compiladores C fazem muita otimização; portanto, o código mais simples de ler provavelmente será traduzido no mesmo código de máquina.

(Uma observação sobre o uso sempre de chaves - o primeiro conjunto de códigos ocupa mais espaço devido a algumas informações "desnecessárias" {}. Na minha experiência, isso geralmente impede o código mal mesclado do compilador e permite que erros com posicionamentos ";" incorretos sejam detectado por ferramentas.)

No entanto, nos velhos tempos, a segunda versão do código teria lido. (Se eu entendi direito!)

concat(char* s, char *t){      
    while (*s++);
    --s;
    while (*s++=*t++);
}
Ian
fonte
2

Mesmo sendo capaz de fazer isso é uma péssima idéia. É coloquialmente conhecido como "O último erro do mundo", assim:

if (alert = CODE_RED)
{
   launch_nukes();
}

Embora não seja provável que você cometa um erro tão grave, é muito fácil errar acidentalmente e causar um erro difícil de encontrar na sua base de código. A maioria dos compiladores modernos inserirá um aviso para atribuições dentro de uma condicional. Eles estão lá por uma razão, e você faria bem em atendê-los e apenas evitar essa construção.

Mason Wheeler
fonte
Antes desses avisos, escrevíamos CODE_RED = alertpara que desse um erro no compilador.
31416 Ian
4
@Ian Yoda Condicionais que é chamado. Difícil de ler eles são. Infelizmente a necessidade para eles é.
Mason Wheeler
Após um breve período introdutório de "se acostumando", as condições Yoda não são mais difíceis de ler do que as normais. Às vezes, eles são mais legíveis . Por exemplo, se você tiver uma sequência de ifs / elseifs, ter a condição sendo testada à esquerda para maior ênfase é uma pequena melhoria na IMO.
user949300
2
@ user949300 Duas palavras: Síndrome de Estocolmo: P
Mason Wheeler
2

Ambos os estilos são bem formados, corretos e apropriados. Qual é o mais apropriado depende em grande parte das diretrizes de estilo da sua empresa. Os IDEs modernos facilitarão o uso de ambos os estilos através do uso de fiapos de sintaxe ao vivo que destacam explicitamente áreas que de outra forma poderiam se tornar uma fonte de confusão.

Por exemplo, a seguinte expressão é destacada pelo Netbeans :

if($a = someFunction())

com base em "atribuição acidental".

insira a descrição da imagem aqui

Para dizer explicitamente ao Netbeans que "sim, eu realmente pretendia fazer isso ...", a expressão pode ser colocada em um conjunto de parênteses.

if(($a = someFunction()))

insira a descrição da imagem aqui

No final do dia, tudo se resume às diretrizes de estilo da empresa e à disponibilidade de ferramentas modernas para facilitar o processo de desenvolvimento.

Luke A. Leber
fonte