O que é tão ruim com goto quando é usado para esses casos óbvios e relevantes?

36

Eu sempre soube que goto é algo ruim, trancado em um porão em algum lugar para nunca ser visto para sempre, mas eu me deparei com um exemplo de código hoje que faz muito sentido usar goto .

Eu tenho um IP onde preciso verificar se está dentro de uma lista de IPs e, em seguida, procedo com o código, caso contrário, lançar uma exceção.

<?php

$ip = '192.168.1.5';
$ips = [
    '192.168.1.3',
    '192.168.1.4',
    '192.168.1.5',
];

foreach ($ips as $i) {
    if ($ip === $i) {
        goto allowed;
    }
}

throw new Exception('Not allowed');

allowed:

...

Se eu não usar goto , então eu tenho que usar alguma variável como

$allowed = false;

foreach ($ips as $i) {
    if ($ip === $i) {
        $allowed = true;
        break;
    }
}

if (!$allowed) {
    throw new Exception('Not allowed');
}

Minha pergunta é o que há de tão ruim com goto quando usado para casos óbvios e relevantes?

    
por php_nub_qq 25.08.2016 / 12:33
fonte

10 respostas

117

O GOTO em si não é um problema imediato, são as máquinas de estado implícitas que as pessoas tendem a implementar com ele. No seu caso, você quer um código que verifique se o endereço IP está na lista de endereços permitidos, portanto

if (!contains($ips, $ip)) throw new Exception('Not allowed');

para que seu código queira verificar uma condição. O algoritmo para implementar esta verificação não deve ser uma preocupação aqui, no espaço mental do seu programa principal a verificação é atômica. É assim que deve ser.

Mas se você colocar o código que faz o check no seu programa principal, você perde isso. Você introduz o estado mutável, seja explicitamente:

$list_contains_ip = undef;        # STATE: we don't know yet

foreach ($ips as $i) {
  if ($ip === $i) {
      $list_contains_ip = true;   # STATE: positive
      break;
  }
                                  # STATE: we still don't know yet, huh?                                                          
                                  # Well, then...
  $list_contains_ip = false;      # STATE: negative
}

if (!$list_contains_ip) {
  throw new Exception('Not allowed');
}

onde $list_contains_ip é sua única variável de estado ou implicitamente:

                             # STATE: unknown
foreach ($ips as $i) {       # What are we checking here anyway?
  if ($ip === $i) {
    goto allowed;            # STATE: positive
  }
                             # STATE: unknown
}
                             # guess this means STATE: negative
throw new Exception('Not allowed');

allowed:                     # Guess we jumped over the trap door

Como você pode ver, existe uma variável de estado não declarada na construção GOTO. Isso não é um problema em si, mas essas variáveis de estado são como seixos: carregar uma não é difícil, carregar uma sacola cheia delas fará você suar. Seu código não permanecerá o mesmo: no próximo mês, você será solicitado a diferenciar os endereços privados e públicos. No mês seguinte, seu código precisará suportar intervalos de IP. No próximo ano, alguém pedirá que você ofereça suporte a endereços IPv6. Em nenhum momento, seu código ficará assim:

if ($ip =~ /:/) goto IP_V6;
if ($ip =~ /\//) goto IP_RANGE;
if ($ip =~ /^10\./) goto IP_IS_PRIVATE;

foreach ($ips as $i) { ... }

IP_IS_PRIVATE:
   foreach ($ip_priv as $i) { ... }

IP_V6:
   foreach ($ipv6 as $i) { ... }

IP_RANGE:
   # i don't even want to know how you'd implement that

ALLOWED:
   # Wait, is this code even correct?
   # There seems to be a bug in here.

E quem tiver que depurar esse código irá amaldiçoar você e seus filhos.

Dijkstra diz o seguinte:

The unbridled use of the go to statement has as an immediate consequence that it becomes terribly hard to find a meaningful set of coordinates in which to describe the process progress.

E é por isso que o GOTO é considerado prejudicial.

    
por 25.08.2016 / 19:07
fonte
41

Existem alguns casos de uso legítimo para GOTO . Por exemplo, para tratamento e limpeza de erros em C ou para implementar algumas formas de máquinas de estado. Mas este não é um desses casos. O segundo exemplo é mais legível IMHO, mas ainda mais legível seria extrair o loop para uma função separada e, em seguida, retornar quando você encontrar uma correspondência. Melhor ainda seria (no pseudocódigo, não sei a sintaxe exata):

if (!in_array($ip, $ips)) throw new Exception('Not allowed');

Então, o que há de tão ruim em GOTO ? A programação estruturada usa funções e estruturas de controle para organizar o código, de modo que a estrutura sintática reflete a estrutura lógica. Se algo for executado apenas condicionalmente, ele aparecerá em um bloco de instrução condicional. Se algo for executado em um loop, ele aparecerá em um bloco de loop. GOTO permite contornar a estrutura sintática pulando arbitrariamente, tornando o código muito mais difícil de seguir.

Claro que se você não tem outra escolha, você usa GOTO , mas se o mesmo efeito pode ser alcançado com funções e estruturas de controle, é preferível.

    
por 25.08.2016 / 14:18
fonte
17

Como outros já disseram, o problema não é com o goto em si; O problema está em como as pessoas usam o goto e como ele pode tornar o código mais difícil de entender e manter.

Assuma o seguinte trecho de código:

       i = 4;
label: printf( "%d\n", i );

Qual valor é impresso para i ? Quando é impresso? Até você considerar todas as instâncias de goto label em sua função, você não pode saber. A simples presença desse rótulo destrói sua capacidade de depurar código por simples inspeção. Para pequenas funções com um ou dois ramos, não há muito problema. Para funções não pequenas ...

No início dos anos 90, recebemos uma pilha de códigos em C que exibiam uma tela gráfica em 3D e informavam para que ela fosse executada mais rapidamente. Foram apenas cerca de 5000 linhas de código, mas todas eram em main , e o autor usava cerca de 15% degoto s em ambas as direções. Este era um código ruim para começar, mas a presença daqueles goto s tornou tudo muito pior. Demorou meu colega de trabalho cerca de 2 semanas para decifrar o fluxo de controle. Melhor ainda, aqueles goto s resultaram em código tão strongmente acoplado a si mesmo que nós não pudemos fazer quaisquer alterações sem quebrar algo.

Nós tentamos compilar com otimização de nível 1, e o compilador consumiu toda a RAM disponível, depois todas as swap disponíveis, e então entrou em pânico no sistema (o que provavelmente não teve nada a ver com o goto s, mas eu gosto de jogar isso anedota lá fora).

No final, demos ao cliente duas opções - vamos reescrever tudo do zero ou comprar um hardware mais rápido.

Eles compraram hardware mais rápido.

Regras do Bode para usar goto :

  1. Apenas encaminhamento de ramificação;
  2. Não ignore as estruturas de controle (ou seja, não se ramifique no corpo de uma instrução if ou for ou while );
  3. Não use goto no lugar de uma estrutura de controle

Há casos em que goto é a resposta certa, mas eles são raros (romper um loop profundamente aninhado é o único lugar em que eu o utilizo).

EDITAR

Ao expandir a última declaração, aqui está um dos poucos casos de uso válidos para goto . Suponha que tenhamos a seguinte função:

T ***myalloc( size_t N, size_t M, size_t P )
{
  size_t i, j, k;

  T ***arr = malloc( sizeof *arr * N );
  for ( i = 0; i < N; i ++ )
  {
    arr[i] = malloc( sizeof *arr[i] * M );
    for ( j = 0; j < M; j++ )
    {
      arr[i][j] = malloc( sizeof *arr[i][j] * P );
      for ( k = 0; k < P; k++ )
        arr[i][j][k] = initial_value();
    }
  }
  return arr;
}

Agora, temos um problema - e se uma das chamadas malloc falhar no meio? Por mais improvável que um evento possa ser, não queremos retornar uma matriz parcialmente alocada, nem queremos apenas sair da função com um erro; queremos limpar depois de nós mesmos e desalocar qualquer memória parcialmente alocada. Em uma linguagem que lança uma exceção em uma má alocação, isso é bastante simples - você apenas escreve um manipulador de exceções para liberar o que já foi alocado.

Em C, você não tem tratamento estruturado de exceções; você precisa verificar o valor de retorno de cada chamada de malloc e tomar a ação apropriada.

T ***myalloc( size_t N, size_t M, size_t P )
{
  size_t i, j, k;

  T ***arr = malloc( sizeof *arr * N );
  if ( arr )
  {
    for ( i = 0; i < N; i ++ )
    {
      if ( !(arr[i] = malloc( sizeof *arr[i] * M )) )
        goto cleanup_1;

      for ( j = 0; j < M; j++ )
      {
        if ( !(arr[i][j] = malloc( sizeof *arr[i][j] * P )) )
          goto cleanup_2;

        for ( k = 0; k < P; k++ )
          arr[i][j][k] = initial_value();
      }
    }
  }
  goto done;

  cleanup_2:
    // We failed while allocating arr[i][j]; clean up the previously allocated arr[i][j]
    while ( j-- )
      free( arr[i][j] );
    free( arr[i] );
    // fall through

  cleanup_1:
    // We failed while allocating arr[i]; free up all previously allocated arr[i][j]
    while ( i-- )
    {
      for ( j = 0; j < M; j++ )
        free( arr[i][j] );
      free( arr[i] );
    }

    free( arr );
    arr = NULL;

  done:
    return arr;
}

Podemos fazer isso sem usar goto ? Claro que podemos - requer apenas uma pequena contabilidade extra (e, na prática, esse é o caminho que eu tomaria). Mas, se você estiver procurando por lugares em que usar um goto não seja imediatamente um sinal de má prática ou design, esse é um dos poucos.

    
por 25.08.2016 / 23:37
fonte
12

return , break , continue e throw / catch são todos essencialmente gotos - todos eles transferem o controle para outro trecho de código e todos poderiam ser implementados com as gotos - na verdade, eu fiz então, uma vez em um projeto escolar, um instrutor PASCAL estava dizendo quanto melhor Pascal era do que básico por causa da estrutura ... então eu tive que ser contrário ...

A coisa mais importante sobre Engenharia de Software (eu vou usar este termo sobre codificação para se referir a uma situação em que você está sendo pago por alguém para criar uma base de código junto com outros engenheiros que exigem melhoria e manutenção) está fazendo legível em código - fazê-lo fazer algo é quase secundário. Seu código será escrito apenas uma vez, mas, na maioria dos casos, as pessoas passarão dias e semanas revisitando / reaprendendo, melhorando e consertando - e toda vez que elas (ou você) tiverem que começar do zero e tentar lembrar / descobrir seu código.

A maioria dos recursos que foram adicionados aos idiomas ao longo dos anos é para tornar o software mais fácil de manter e não de escrever (embora alguns idiomas sigam nessa direção - eles geralmente causam problemas a longo prazo ...).

Em comparação com declarações de controle de fluxo semelhantes, os GOTOs podem ser quase tão fáceis de seguir no seu melhor (um único goto usado em um caso como você sugere) e um pesadelo quando abusado - e são muito facilmente abusados ...

Então, depois de lidar com pesadelos de espaguete por alguns anos, nós apenas dissemos "Não", como uma comunidade, não vamos aceitar isso - muitas pessoas estragam tudo se derem uma pequena margem de manobra - esse é realmente o único problema com eles. Você pode usá-los ... mas mesmo que seja o caso perfeito, o próximo cara vai assumir que você é um péssimo programador porque não entende a história da comunidade.

Muitas outras estruturas foram desenvolvidas apenas para tornar seu código mais compreensível: Funções, Objetos, Escopo, Encapsulamento, Comentários (!) ... bem como os padrões / processos mais importantes como "DRY" (prevenção de duplicação) e "YAGNI" (Reduzindo a generalização excessiva / complicação do código) - tudo realmente importa apenas para o próximo jogador ler o seu código (Quem provavelmente será você - depois de ter esquecido a maior parte do que você fez em primeiro lugar! )

    
por 25.08.2016 / 21:42
fonte
7

GOTO é uma ferramenta. Pode ser usado para o bem ou para o mal.

Nos velhos e maus dias, com FORTRAN e BASIC, era quase a única ferramenta .

Ao analisar o código desses dias, quando você vir um GOTO , precisará descobrir por que ele está lá. Pode ser parte de um idioma padrão que você pode entender rapidamente ... ou pode ser parte de alguma estrutura de controle de pesadelos que nunca deveria ter sido. Você não sabe até que tenha olhado, e é fácil estar enganado.

As pessoas queriam algo melhor, e estruturas de controle mais avançadas foram inventadas. Estes cobriam a maioria dos casos de uso, e as pessoas que foram queimadas pelo mau GOTO s queriam bani-las completamente.

Ironicamente, GOTO não é tão ruim quando é raro. Quando você vê um, você sabe que há algo especial acontecendo, e é fácil encontrar o rótulo correspondente, já que é o único rótulo próximo.

Avance para hoje. Você é um professor ensinando programação. Você poderia dizer "Na maioria dos casos, você deve usar as novas construções avançadas, mas em alguns casos, um simples GOTO pode ser mais legível". Os alunos não vão entender isso. Eles abusarão de GOTO para criar um código ilegível.

Em vez disso, você diz " GOTO bad. GOTO evil. GOTO falha no exame." Os alunos entenderão que !

    
por 26.08.2016 / 10:43
fonte
4

Com exceção de goto , todas as construções de fluxo no PHP (e na maioria das linguagens) têm o escopo hierarquicamente.

Imagine algum código examinado com olhos apertados:

a;
foo {
    b;
}
c;

Independentemente da construção de controle foo ( if , while , etc.), existem apenas alguns pedidos permitidos para a , b e c .

Você pode ter a - b - c ou a - c ou mesmo a - b - b - b - c . Mas você poderia nunca ter b - c ou a - b - a - c .

... a menos que você tenha goto .

$a = 1;
first:
echo 'a';
if ($a === 1) {
    echo 'b';
    $a = 2;
    goto first;
}
echo 'c'; 

goto (em particular para trás goto ) pode ser problemático o suficiente para que seja melhor deixá-lo sozinho e usar construções de fluxo hierárquicas e bloqueadas.

goto s tem um lugar, mas principalmente como micro-otimizações em linguagens de baixo nível. IMO, não há lugar bom para isso em PHP.

FYI, o código de exemplo pode ser escrito ainda melhor do que qualquer uma das suas sugestões.

if(!in_array($ip, $ips, true)) {
    throw new Exception('Not allowed');
}
    
por 25.08.2016 / 23:05
fonte
2

Em linguagens de baixo nível, GOTO é inevitável. Mas, em alto nível, deve ser evitado (no caso de a linguagem suportar) porque torna os programas mais difíceis de ler .

Tudo se resume a tornar o código mais difícil de ler. Linguagens de alto nível são supostas para tornar o código mais fácil de ler do que linguagens de baixo nível como, por exemplo, assembler ou C.

GOTO não causa aquecimento global nem causa pobreza no terceiro mundo. Apenas torna o código mais difícil de ler.

A maioria das linguagens modernas possui estruturas de controle que tornam o GOTO desnecessário. Alguns como o Java nem sequer têm isso.

Na verdade, o termo código spaguetti vem de uma estrutura complicada, difícil de seguir por causa de estruturas de ramificação não estruturadas.

    
por 25.08.2016 / 21:35
fonte
1

Nada de errado com as declarações goto . Os erros estão com algumas das pessoas que usam inadequadamente a declaração.

Além do que JacquesB disse (tratamento de erros em C), você está usando goto para sair de um loop não aninhado, algo que pode ser feito usando break . Nesse caso, é melhor você usar break .

Mas se você tivesse um cenário de loop aninhado, usar goto seria mais elegante / mais simples. Por exemplo:

$allowed = false;

foreach ($ips as $i) {
    foreach ($ips2 as $i2) {
        if ($ip === $i && $ip === $i2) {
            $allowed = true;
            goto out;
        }
    }
}

out:

if (!$allowed) {
    throw new Exception('Not allowed');
}

O exemplo acima não faz sentido em seu problema específico, porque você não precisa de um loop aninhado. Mas espero que você veja apenas a parte do loop aninhado.

Ponto de bonificação: se sua lista de IPs for pequena, seu método está correto. Mas se a lista crescer, saiba que sua abordagem tem uma complexidade assintótica de pior tempo de execução de O (n) . À medida que sua lista cresce, você pode querer usar um método diferente que atinja O (log n) (como uma estrutura de árvore) ou O (1) (uma tabela de hash sem colisões).

    
por 25.08.2016 / 15:23
fonte
0

With goto I can write faster code!

Verdadeiro. Não se importe.

Goto exists in assembly! They just call it jmp.

Verdadeiro. Não se importe.

Goto solves problems more simply.

Verdadeiro. Não se importe.

In the hands of a disciplined developer code that uses goto can be easier to read.

Verdadeiro. No entanto, tenho sido aquele codificador disciplinado. Eu vi o que acontece com o código ao longo do tempo. Goto começa bem. Em seguida, o desejo de reutilizar os conjuntos de códigos. Muito em breve eu me encontro em um ponto de interrupção sem ter a menor idéia do que está acontecendo, mesmo depois de olhar para o estado do programa. Goto dificulta o raciocínio sobre o código. Nós trabalhamos muito para criar while , do while , for , for each switch , subroutines , functions e muito mais, porque fazer essas coisas com if e goto é duro com o cérebro.

Então não. Nós não queremos olhar para goto . Claro que está vivo e bem no binário, mas não precisamos ver isso na fonte. Na verdade, if está começando a parecer um pouco instável.

    
por 25.08.2016 / 22:15
fonte
-1

Os idiomas de montagem normalmente têm apenas saltos condicionais / incondicionais (o equivalente a GOTO. Implementações mais antigas de FORTRAN e BASIC não tinham instruções de bloco de controle além de uma iteração contada (o loop DO), deixando todos os outros fluxos de controle para IFs e GOTOs. O loop DO nessas linguagens foi finalizado por uma instrução numericamente rotulada Como resultado, o código escrito para essas linguagens poderia ser, e frequentemente era, difícil de seguir e propenso a erros.

Para sublinhar o ponto, há a declaração " VENHA DE " cuidadosamente inventada.

Não há praticamente necessidade de usar o GOTO em linguagens como C, C ++, C #, PASCAL, Java, etc .; construções alternativas podem ser usadas, o que quase certamente será tão eficiente e muito mais sustentável. É verdade que um GOTO em um arquivo de origem não será um problema. O problema é que não é preciso muita coisa para tornar uma unidade de código difícil de ser seguida e propensa a erros. É por isso que a sabedoria aceita é evitar o GOTO sempre que possível.

Este artigo da wikipedia sobre a declaração goto pode ser útil

    
por 27.08.2016 / 04:30
fonte

Tags