Por que as instruções “if elif else” virtualmente nunca estão no formato de tabela?

73
if   i>0 : return sqrt(i)  
elif i==0: return 0  
else     : return 1j * sqrt(-i)

VS

if i>0:  
   return sqrt(i)  
elif i==0:  
   return 0  
else:  
   return 1j * sqrt(-i)  

Dados os exemplos acima, não entendo porque virtualmente nunca vejo o primeiro estilo em bases de código. Para mim, você transforma o código em um formato tabular que mostra claramente o que você deseja. A primeira coluna pode ser virtualmente ignorada. A segunda coluna identifica a condição e a terceira coluna fornece a saída desejada. Parece, pelo menos para mim, direto e fácil de ler. No entanto, eu sempre vejo esse tipo simples de situação de caso / switch sair no formato estendido, guia recuado. Por que é que? As pessoas acham o segundo formato mais legível?

O único caso em que isso poderia ser problemático é se o código muda e fica mais longo. Nesse caso, acho perfeitamente razoável refatorar o código no formato longo e indentado. Todos fazem da segunda maneira simplesmente porque é sempre assim? Sendo um advogado do diabo, acho que outra razão pode ser porque as pessoas acham que dois formatos diferentes, dependendo da complexidade das declarações if / else, são confusos? Qualquer insight seria apreciado.

    
por horta 26.07.2016 / 21:31
fonte

10 respostas

93

Um motivo pode ser que você não esteja usando idiomas populares.

Alguns contra-exemplos:

Haskell com guardas e com padrões:

sign x |  x >  0        =   1
       |  x == 0        =   0
       |  x <  0        =  -1

take  0     _           =  []
take  _     []          =  []
take  n     (x:xs)      =  x : take (n-1) xs

Erlang com padrões:

insert(X,Set) ->
    case lists:member(X,Set) of
        true  -> Set;
        false -> [X|Set]
    end.

Emacs lisp:

(pcase (get-return-code x)
  ('success       (message "Done!"))
  ('would-block   (message "Sorry, can't do it now"))
  ('read-only     (message "The shmliblick is read-only"))
  ('access-denied (message "You do not have the needed rights"))
  (code           (message "Unknown return code %S" code)))

Geralmente, vejo que o formato da tabela é bastante popular com linguagens funcionais (e em geral baseadas em expressões), enquanto que quebrar as linhas é mais popular em outras (principalmente baseadas em declarações).

    
por 27.07.2016 / 01:47
fonte
134

É mais legível. Algumas razões pelas quais:

  • Quase todas as linguagens usam essa sintaxe (não todas, mais - seu exemplo parece ser o Python, embora)
  • isanae apontou em um comentário que a maioria dos depuradores é baseada em linha (não baseada em declarações)
  • Ele começa a parecer ainda mais feio se você precisar usar ponto-e-vírgulas ou chaves in-line
  • Ele lê de cima para baixo mais suavemente
  • Ele parece terrivelmente ilegível se você tiver algo além de declarações de retorno triviais
    • Qualquer sintaxe significativa de recuo é perdida quando você navega código, pois o código condicional não é mais separado visualmente (de Dan Neely )
    • Isso será particularmente ruim se você continuar a corrigir / adicionar itens nas instruções if de 1 linha
  • Só é legível se todas as suas verificações se tiverem o mesmo tamanho
  • Isso significa que você não pode formatar instruções if complicadas em instruções multilinhas, elas terão como oneliners
  • Tenho muito mais chances de perceber bugs / logicflow ao ler verticalmente linha por linha, sem tentar analisar várias linhas juntas
  • Nosso cérebro lê texto mais estreito e mais alto MUITO mais rápido do que o texto horizontal longo

No minuto em que você tentar fazer isso, você acabará reescrevendo-o em instruções multilinhas. O que significa que você acabou de perder tempo!

Além disso, as pessoas inevitavelmente adicionam algo como:

if i>0:  
   print('foobar')
   return sqrt(i)  
elif i==0:  
   return 0  
else:  
   return 1j * sqrt(-i)  

Não é preciso fazer isso com muita frequência antes de decidir que esse formato é muito melhor que sua alternativa. Ah, mas você poderia inserir tudo em uma linha! enderland morre no interior .

Ou isto:

if   i>0 : return sqrt(i)  
elif i==0 and bar==0: return 0  
else     : return 1j * sqrt(-i)

O que é realmente muito chato. Ninguém gosta de formatar coisas como esta.

E por último, você vai começar a guerra santa de "quantos espaços para abas" problema. O que renderiza perfeitamente em sua tela como um formato de tabela pode não renderizar no meu dependendo das configurações.

A legibilidade não deve depender das configurações do IDE.

    
por 26.07.2016 / 21:52
fonte
55

Acredito firmemente que "o código é lido muitas vezes, poucas escritas - por isso a legibilidade é muito importante".

Uma coisa chave que me ajuda quando leio o código de outras pessoas é que segue os padrões "normais" que meus olhos são treinados para reconhecer. Consigo ler a forma recuada com mais facilidade porque já a vi tantas vezes que ela é registrada quase que automaticamente (com pouco esforço cognitivo da minha parte). Não é porque é mais bonito - é porque segue as convenções que eu estou acostumado. Convenção bate "melhor" ...

    
por 26.07.2016 / 22:31
fonte
16

Além das outras desvantagens já mencionadas, o layout tabular aumenta as chances de conflitos de mesclagem de controle de versão que exigem intervenção manual.

Quando um bloco de código tabularmente alinhado precisa ser realinhado, o sistema de controle de versão tratará cada uma dessas linhas como tendo sido modificada:

diff --git a/foo.rb b/foo.rb
index 40f7833..694d8fe 100644
--- a/foo.rb
+++ b/foo.rb
@@ -1,8 +1,8 @@
 class Foo

   def initialize(options)
-    @cached_metadata = options[:metadata]
-    @logger          = options[:logger]
+    @metadata = options[:metadata]
+    @logger   = options[:logger]
   end

 end

Agora, suponha que, nesse meio tempo, em outro ramo, um programador tenha adicionado uma nova linha ao bloco de código alinhado:

diff --git a/foo.rb b/foo.rb
index 40f7833..86648cb 100644
--- a/foo.rb
+++ b/foo.rb
@@ -3,6 +3,7 @@ class Foo
   def initialize(options)
     @cached_metadata = options[:metadata]
     @logger          = options[:logger]
+    @kittens         = options[:kittens]
   end

 end

A fusão desse ramo falhará:

[email protected]:/tmp/foo$ git merge add_kittens
Auto-merging foo.rb
CONFLICT (content): Merge conflict in foo.rb
Automatic merge failed; fix conflicts and then commit the result.

Se o código que está sendo modificado não tivesse usado alinhamento tabular, a mesclagem teria sido bem-sucedida automaticamente.

(Esta resposta foi "plagiada" do meu próprio artigo desencorajando o alinhamento tabular no código) .

    
por 28.07.2016 / 23:57
fonte
8

Os formatos tabulares podem ser muito bons se as coisas sempre couberem na largura atribuída. Se algo excede a largura atribuída, no entanto, muitas vezes é necessário ter parte da tabela que não está alinhada com o resto, ou então ajustar o layout de todo o resto da tabela para ajustá-la ao item longo .

Se os arquivos de origem forem editados usando programas projetados para trabalhar com dados em formato de tabela e puderem manipular itens excessivamente longos usando um tamanho de fonte menor, dividindo-os em duas linhas na mesma célula, etc. para usar formatos tabulares com mais freqüência, mas a maioria dos compiladores deseja arquivos de origem livres dos tipos de marcação que esses editores precisariam armazenar para manter a formatação. Usar linhas com quantidades variáveis de recuo, mas nenhum outro layout não é tão bom quanto a formatação tabular, no melhor dos casos, mas não causa quase tantos problemas no pior dos casos.

    
por 27.07.2016 / 01:38
fonte
6

Existe a declaração 'switch' que fornece esse tipo de coisa para casos especiais, mas acho que não é isso que você está perguntando.

Já vi instruções if em formato de tabela, mas tem que haver um grande número de condições para fazer valer a pena. 3 se as declarações são mostradas melhor no formato tradicional, mas se você tiver 20, é muito mais fácil exibi-las em um bloco grande que esteja formatado para torná-lo mais claro.

E aí está o ponto: clareza. Se facilitar a visualização (e seu primeiro exemplo não for fácil de ver onde está o delimitador:), formate-o para se adequar à situação. Caso contrário, fique com o que as pessoas esperam, pois isso é sempre mais fácil de reconhecer.

    
por 27.07.2016 / 09:35
fonte
1

Se sua expressão é realmente fácil, a maioria das linguagens de programação oferece o operador?: branching:

return  ( i > 0  ) ? sqrt( i)
      : ( i == 0 ) ? 0
        /* else */ : 1j * sqrt( -i )

Este é um formato tabular de leitura curta. Mas a parte importante é: vejo de relance qual é a ação "principal". Esta é uma declaração de retorno! E o valor é decidido por certas condições.

Se, por outro lado, você tiver ramificações que executam código diferente, acho muito mais legível recuar nesses blocos. Porque agora existem ações "principais" diferentes dependendo da declaração if. Em um caso nós jogamos, em um caso nós registramos e retornamos ou simplesmente retornamos. Existe um fluxo de programa diferente dependendo da lógica, então os blocos de código encapsulam os diferentes ramos e os tornam mais proeminentes para o desenvolvedor (por exemplo, leitura de velocidade de uma função para captar o fluxo do programa)

if ( i > 0 )
{
    throw new InvalidInputException(...);
}
else if ( i == 0 )
{
    return 0;
}
else
{
    log( "Calculating sqrt" );
    return sqrt( -i );
}
    
por 27.07.2016 / 13:00
fonte
1

Como o enderland já disse, você está assumindo que você só tem um "retorno" como ação e que pode marcar esse "retorno" no final da condição. Eu gostaria de dar mais detalhes sobre por que isso não será bem sucedido.

Não sei quais são seus idiomas preferidos, mas há muito tempo estou codificando em C. Há vários padrões de codificação em torno dos quais o objetivo é evitar alguns erros de codificação padrão, não permitindo construções de código que sejam propensas a erros, seja na codificação inicial ou durante a manutenção posterior. Eu estou mais familiarizado com MISRA-C, mas há outros, e geralmente todos eles têm regras semelhantes, porque eles estão resolvendo os mesmos problemas no mesmo idioma.

Um erro popular que os padrões de codificação geralmente abordam é este pequeno problema: -

if (x == 10)
    do_something();
    do_something_else();

Isso não faz o que você acha que faz. No que diz respeito a C, se x é 10, você chama do_something() , mas do_something_else() é chamado independentemente do valor de x . Somente a ação imediatamente após a instrução "if" é condicional. Isso pode ser o que o codificador pretendia, em cujo caso há uma armadilha potencial para os mantenedores; ou pode não ser o que o programador pretendia, caso em que há um erro. É uma pergunta popular sobre entrevista.

A solução nos padrões de codificação é obrigar chaves em torno de todas as ações condicionais, mesmo que sejam de linha única. Nós agora conseguimos

if (x == 10)
{
    do_something();
    do_something_else();
}

ou

if (x == 10)
{
    do_something();
}
do_something_else();

e agora funciona corretamente e é claro para os mantenedores.

Você notará que isso é totalmente incompatível com seu formato de estilo de tabela.

Algumas outras linguagens (por exemplo, Python) analisaram esse problema e decidiram que, como os codificadores estavam usando espaço em branco para tornar o layout claro, seria uma boa ideia usar espaços em branco em vez de chaves. Então, em Python,

if x == 10:
    do_something()
    do_something_else()

faz as chamadas para do_something() e do_something_else() condicionais em x == 10, enquanto

if x == 10:
    do_something()
do_something_else()

significa que apenas do_something() é condicional em x e do_something_else() é sempre chamado.

É um conceito válido e você encontrará alguns idiomas para usá-lo. (Eu vi pela primeira vez no Occam2, muito tempo atrás.) Novamente, você pode ver facilmente que o formato de estilo de tabela é incompatível com o idioma.

    
por 27.07.2016 / 13:53
fonte
1

O layout tabular pode ser útil em alguns casos limitados, mas há poucas ocasiões em que é útil com if.

Em casos simples: pode ser uma escolha melhor. Em casos médios, um switch geralmente é mais adequado (se o seu idioma tiver um). Em casos complicados, você pode achar que as tabelas de chamadas são mais adequadas.

Houve muitas vezes em que o código de refatoração que eu reorganizei é tabular para torná-lo óbvio. Raramente é assim que eu deixo assim, na maioria dos casos, existe uma maneira melhor de resolver o problema quando você o entende. Ocasionalmente, uma prática de codificação ou padrão de layout a proíbe e, nesse caso, um comentário é útil.

Houve algumas perguntas sobre ?: . Sim, é o operador ternário (ou como gosto de pensar no valor se). no primeiro blush este exemplo é um pouco complicado para?: (e uso excessivo?: não ajuda a legibilidade, mas dói), mas com algum pensamento O exemplo pode ser reorganizado como abaixo, Mas acho que neste caso um switch é o mais legível solução.

if i==0: return 0
return i>0?sqrt(i):(1j*sqrt(-i))
    
por 27.07.2016 / 02:40
fonte
-3

Não vejo nada de errado com o formato da tabela. Preferência pessoal, mas eu usaria um ternário assim:

return i>0  ? sqrt(i)       :
       i==0 ? 0             :
              1j * sqrt(-i)

Não é necessário repetir return todas as vezes:)

    
por 27.07.2016 / 17:33
fonte