A que tipos de erros as instruções "goto" levam? Existem exemplos historicamente significativos?


103

Entendo que, exceto por romper loops aninhados em loops; a gotodeclaração é evadida e difamada como um estilo de programação propenso a erros, para nunca ser usado.

XKCD Texto alternativo: "Neal Stephenson acha fofo nomear seus rótulos como 'dengo'"
Veja a história em quadrinhos original em: http://xkcd.com/292/

Porque eu aprendi isso cedo; Eu realmente não tenho nenhuma percepção ou experiência sobre quais tipos de bugs gotorealmente levam. Então, do que estamos falando aqui:

  • Instabilidade?
  • Código inalterável ou ilegível?
  • Vulnerabilidades de segurança?
  • Algo completamente diferente?

A que tipos de erros as instruções "goto" realmente levam? Existem exemplos historicamente significativos?


33
Você leu o breve artigo original de Dijkstra sobre esse assunto? (Essa é uma pergunta sim / não, e qualquer resposta que não seja um instante inequívoco "sim" é um "não".)
John R. Strohm

2
Presumo que uma saída de alarme ocorre quando algo falha catastrófica acontece. de onde você nunca retornará. Como falta de energia, dispositivos ou arquivos que desaparecem. Alguns tipos de "em caso de erro" ... Mas eu acho que a maioria dos erros "quase catastrópicos" podem ser tratados por construções "try - catch" hoje em dia.
Lenne

13
Ninguém mencionou o GOTO calculado ainda. Quando a Terra era jovem, o BASIC tinha números de linha. Você pode escrever o seguinte: 100 N = A * 100; GOTO N. Tentar depuração que :)
mcottle

7
@ JohnR.Strohm Eu li o jornal de Dijkstra. Foi escrito em 1968 e sugere que a necessidade de gotos pode ser reduzida em idiomas que incluem recursos avançados como instruções e funções de chave. Foi escrito em resposta a idiomas nos quais o goto era o principal método de controle de fluxo e poderia ser usado para pular para qualquer ponto do programa. Enquanto nas linguagens desenvolvidas após a redação deste documento, por exemplo, C, o goto só pode pular para lugares no mesmo quadro de pilha e geralmente é usado apenas quando outras opções não funcionam bem. (continuação ...)
Ray

10
(... continuação) Seus pontos eram válidos na época, mas não são mais relevantes, independentemente de ir ou não ser uma boa idéia. Se queremos argumentar que, de um jeito ou de outro, devem ser apresentados argumentos relacionados às linguagens modernas. A propósito, você leu a "Programação Estruturada com Instruções Goto de Knuth"?
Ray

Respostas:


2

Aqui está uma maneira de ver isso que ainda não vi nas outras respostas.

É sobre escopo. Um dos principais pilares das boas práticas de programação é manter seu escopo restrito. Você precisa de um escopo restrito porque não possui a capacidade mental de supervisionar e entender mais do que apenas algumas etapas lógicas. Então você cria pequenos blocos (cada qual se torna "uma coisa") e depois constrói com eles para criar blocos maiores que se tornam uma coisa, e assim por diante. Isso mantém as coisas gerenciáveis ​​e compreensíveis.

Um goto efetivamente infla o escopo de sua lógica para todo o programa. Isso certamente derrotará seu cérebro em qualquer um dos programas menores, abrangendo apenas algumas linhas.

Portanto, você não será capaz de dizer se cometeu mais um erro, porque há muito para absorver e verificar sua pobre cabecinha. Este é o verdadeiro problema, os erros são apenas um resultado provável.


Goto não local?
Deduplicator

thebrain.mcgill.ca/flash/capsules/experience_jaune03.html << A mente humana pode acompanhar uma média de 7 coisas ao mesmo tempo. Sua lógica entra nessa limitação, pois a expansão do escopo adicionará muito mais coisas que precisam ser monitoradas. Assim, os tipos de erros que serão introduzidos provavelmente são de omissão e esquecimento. Você também oferece uma estratégia atenuante e boas práticas em geral, de "manter seu escopo restrito". Como tal, aceito esta resposta. Bom trabalho Martin.
Akiva

1
Quão legal é isso?! Entre os mais de 200 votos expressos, vencendo com apenas 1!
Martin Maat

68

Não é que isso gotoseja ruim por si só. (Afinal, toda instrução de salto em um computador é um pulo.) O problema é que existe um estilo humano de programação que antecede a programação estruturada, o que poderia ser chamado de programação de "fluxograma".

Na programação de fluxogramas (que as pessoas da minha geração aprenderam e foram usadas para o programa lunar Apollo), você faz um diagrama com blocos para execuções de instruções e diamantes para decisões, e pode conectá-los com linhas que se espalham por todo o lugar. (Chamado "código de espaguete").

O problema com o código espaguete é que você, o programador, pode "saber" que estava certo, mas como você pode provar isso para si mesmo ou para outras pessoas? De fato, ele pode realmente ter um mau comportamento em potencial, e seu conhecimento de que ele está sempre correto pode estar errado.

Junto veio a programação estruturada com blocos de início e fim, para, enquanto, se mais, e assim por diante. Eles tinham a vantagem de você ainda poder fazer qualquer coisa neles, mas se você fosse cuidadoso, poderia ter certeza de que seu código estava correto.

Obviamente, as pessoas ainda podem escrever código espaguete mesmo sem goto. O método comum é escrever a while(...) switch( iState ){..., onde casos diferentes definem iStatevalores diferentes. De fato, em C ou C ++, pode-se escrever uma macro para fazer isso e nomeá-la GOTO, portanto, dizer que você não está usando gotoé uma distinção sem diferença.

Como um exemplo de como a prova de código pode impedir irrestritamente goto, há muito tempo deparei com uma estrutura de controle útil para alterar dinamicamente as interfaces de usuário. Eu chamei de execução diferencial . É totalmente Turing universal, mas sua prova de correcção depende de programação estruturada pura - sem goto, return, continue, break, ou exceções.

insira a descrição da imagem aqui


49
A correção não pode ser comprovada, exceto nos programas mais triviais, mesmo quando a programação estruturada é usada para escrevê-los. Você só pode aumentar seu nível de confiança; programação estruturada faz isso.
Robert Harvey

23
@ MikeDunlavey: Related: "Cuidado com os bugs no código acima; eu apenas provei que está correto, não tentei". staff.fnwi.uva.nl/p.vanemdeboas/knuthnote.pdf
Mooing Duck

26
@RobertHarvey Não é bem verdade que provas de correção são práticas apenas para programas triviais. Porém, são necessárias ferramentas mais especializadas que a programação estruturada.
Mike Haskel

18
@MSalters: É um projeto de pesquisa. O objetivo de um projeto de pesquisa é mostrar que eles poderiam escrever um compilador verificado se tivessem os recursos. Se você observar o trabalho atual, verá que eles simplesmente não estão interessados ​​em dar suporte a versões posteriores do C, mas sim em expandir as propriedades de correção que podem provar. E, para isso, é simplesmente completamente irrelevante se eles suportam C90, C99, C11, Pascal, Fortran, Algol ou Plankalkül.
Jörg W Mittag

8
@martineau: Eu desconfio dessas "frases do boson", em que os programadores meio que concordam que isso é bom ou ruim, porque eles ficam sentados se não o fizerem. Tem que haver razões mais básicas para as coisas.
Mike Dunlavey

63

Por que o goto é perigoso?

  • gotonão causa instabilidade por si só. Apesar de cerca de 100.000 gotos, o kernel do Linux ainda é um modelo de estabilidade.
  • gotopor si só não deve causar vulnerabilidades de segurança. No entanto, em alguns idiomas, misturá-lo com os blocos de gerenciamento de try/ catchexceção pode levar a vulnerabilidades, conforme explicado nesta recomendação do CERT . Os principais compiladores C ++ sinalizam e evitam esses erros, mas infelizmente os compiladores mais antigos ou mais exóticos não.
  • gotocausa código ilegível e impossível de manter. Isso também é chamado de código de espaguete , porque, como em uma placa de espaguete, é muito difícil acompanhar o fluxo de controle quando há muitos gotos.

Mesmo se você conseguir evitar o código de espaguete e usar apenas alguns gotos, eles ainda facilitarão erros como o vazamento de recursos:

  • É fácil seguir códigos usando programação de estrutura, com blocos e loops ou switches aninhados claros; seu fluxo de controle é muito previsível. Portanto, é mais fácil garantir que os invariantes sejam respeitados.
  • Com uma gotodeclaração, você quebra esse fluxo direto e quebra as expectativas. Por exemplo, você pode não perceber que ainda precisa liberar recursos.
  • Muitos gotoem lugares diferentes podem enviar você para um único alvo. Portanto, não é óbvio saber com certeza o estado em que você se encontra ao chegar a esse lugar. O risco de fazer suposições erradas / infundadas é, portanto, bastante grande.

Informações e citações adicionais:

C fornece a gotodeclaração e os rótulos infinitamente abusivos aos quais ramificar. Formalmente, isso gotonunca é necessário e, na prática, quase sempre é fácil escrever código sem ele. (...)
No entanto, sugerimos algumas situações em que o goto pode encontrar um lugar. O uso mais comum é abandonar o processamento em algumas estruturas profundamente aninhadas, como interromper dois loops ao mesmo tempo. (...)
Embora não sejamos dogmáticos sobre o assunto, parece que as declarações goto devem ser usadas com moderação, se houver .

  • James Gosling e Henry McGilton escreveram em seu white paper sobre o ambiente da linguagem Java de 1995 :

    Não há mais
    instruções Goto Java não tem nenhuma instrução goto. Os estudos ilustraram que o goto é (mis) usado com mais frequência do que simplesmente "porque está lá". A eliminação do goto levou a uma simplificação do idioma (...) Estudos em aproximadamente 100.000 linhas de código C determinaram que aproximadamente 90% das instruções goto foram usadas apenas para obter o efeito de romper os loops aninhados. Como mencionado acima, a quebra em vários níveis e continua removendo a maior parte da necessidade de instruções goto.

  • Bjarne Stroustrup define goto em seu glossário nestes termos convidativos:

    goto - o infame goto. Principalmente útil no código C ++ gerado por máquina.

Quando o goto pode ser usado?

Como K&R, eu não sou dogmático sobre gotos. Eu admito que há situações em que ir poderia facilitar a vida de alguém.

Normalmente, em C, o goto permite saída de loop multinível ou tratamento de erros que exigem atingir um ponto de saída apropriado que libere / desbloqueie todos os recursos que foram alocados até agora (alocação múltipla em sequência significa vários rótulos). Este artigo quantifica os diferentes usos do goto no kernel do Linux.

Pessoalmente, prefiro evitá-lo e, em 10 anos de C, usei no máximo 10 gotos. Prefiro usar ifs aninhados , que acho mais legíveis. Quando isso levaria a um aninhamento muito profundo, eu optaria por decompor minha função em partes menores ou usar um indicador booleano em cascata. Os compiladores de otimização de hoje são inteligentes o suficiente para gerar quase o mesmo código que o mesmo código goto.

O uso de goto depende muito do idioma:

  • No C ++, o uso adequado do RAII faz com que o compilador destrua automaticamente objetos que ficam fora do escopo, de modo que os recursos / bloqueio sejam limpos de qualquer maneira e sem necessidade real de ir mais longe.

  • Em Java, não há necessidade de Goto (ver citação autor de Java acima e este excelente resposta Stack Overflow ): o coletor de lixo que limpa a bagunça, break, continue, e try/ catcho tratamento de exceção cobrir todo o caso em que gotopoderia ser útil, mas em um mais seguro e melhor maneira. A popularidade de Java prova que a declaração goto pode ser evitada em uma linguagem moderna.

Dê um zoom na famosa vulnerabilidade SSL para falhar

Isenção de responsabilidade importante: em vista da discussão acirrada nos comentários, quero esclarecer que não pretendo que a declaração goto seja a única causa desse bug. Eu não finjo que sem ir, não haveria bug. Eu só quero mostrar que um goto pode estar envolvido em um bug sério.

Não sei a quantos bugs sérios estão relacionados gotona história da programação: os detalhes geralmente não são comunicados. No entanto, houve um famoso bug do SSL da Apple que enfraqueceu a segurança do iOS. A afirmação que levou a esse bug foi uma gotoafirmação errada .

Alguns argumentam que a causa raiz do bug não foi a declaração goto em si, mas uma cópia / pasta incorreta, uma indentação enganosa, faltando chaves no bloco condicional ou talvez os hábitos de trabalho do desenvolvedor. Não posso confirmar nenhum deles: todos esses argumentos são prováveis ​​hipóteses e interpretação. Ninguém realmente sabe. ( enquanto isso, a hipótese de uma mesclagem que deu errado, como alguém sugeriu nos comentários, parece ser um candidato muito bom em vista de outras inconsistências de indentação na mesma função ).

O único fato objetivo é que um duplicado gotolevou a sair prematuramente da função. Observando o código, a única outra declaração única que poderia ter causado o mesmo efeito teria sido um retorno.

O erro está em função SSLEncodeSignedServerKeyExchange()de esse arquivo :

    if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) != 0)
        goto fail;
    if ((err =...) !=0)
        goto fail;
    if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
        goto fail;
        goto fail;  // <====OUCH: INDENTATION MISLEADS: THIS IS UNCONDITIONDAL!!
    if (...)
        goto fail;

    ... // Do some cryptographic operations here

fail:
    ... // Free resources to process error

De fato, chaves em volta do bloco condicional poderiam ter evitado o erro:
isso levaria a um erro de sintaxe na compilação (e, portanto, a uma correção) ou a um goto redundante e inofensivo. A propósito, o GCC 6 poderá detectar esses erros graças ao aviso opcional para detectar recuo inconsistente.

Mas, em primeiro lugar, todos esses gotos poderiam ter sido evitados com um código mais estruturado. Portanto, o goto é pelo menos indiretamente uma causa desse bug. Existem pelo menos duas maneiras diferentes de evitá-lo:

Abordagem 1: se cláusula ou ifs aninhados

Em vez de testar sequencialmente muitas condições de erro e sempre que enviar para um failrótulo em caso de problema, poderia-se optar por executar as operações criptográficas em uma ifdeclaração que o faria apenas se não houvesse uma condição prévia errada:

    if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0 &&
        (err = ...) == 0 ) &&
        (err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0) &&
        ...
        (err = ...) == 0 ) )
    {
         ... // Do some cryptographic operations here
    }
    ... // Free resources

Abordagem 2: use um acumulador de erros

Essa abordagem é baseada no fato de que quase todas as instruções aqui chamam alguma função para definir um errcódigo de erro e executam o restante do código apenas se errfor 0 (ou seja, função executada sem erro). Uma boa alternativa segura e legível é:

bool ok = true;
ok =  ok && (err = ReadyHash(&SSLHashSHA1, &hashCtx))) == 0;
ok =  ok && (err = NextFunction(...)) == 0;
...
ok =  ok && (err = ...) == 0;
... // Free resources

Aqui, não há um único passo: não há risco de pular rapidamente para o ponto de saída da falha. E visualmente seria fácil identificar uma linha desalinhada ou uma esquecida ok &&.

Essa construção é mais compacta. É baseado no fato de que em C, a segunda parte de uma lógica e ( &&) é avaliada apenas se a primeira parte for verdadeira. De fato, o assembler gerado por um compilador de otimização é quase equivalente ao código original com gotos: O otimizador detecta muito bem a cadeia de condições e gera código, que no primeiro valor de retorno não nulo salta até o fim ( prova online ).

Você pode até prever uma verificação de consistência no final da função que durante a fase de teste identifique incompatibilidades entre o sinalizador ok e o código de erro.

assert( (ok==false && err!=0) || (ok==true && err==0) );

Erros como um erro ==0inadvertidamente substituído por um !=0ou conector lógico seriam facilmente detectados durante a fase de depuração.

Como dito: não finjo que construções alternativas teriam evitado qualquer bug. Eu só quero dizer que eles poderiam ter tornado o bug mais difícil de ocorrer.


34
Não, o bug de falha na Apple foi causado por um programador inteligente que decidiu não incluir o aparelho depois de uma declaração if. :-) Então, quando o próximo programador de manutenção (ou o mesmo, a mesma diferença) apareceu e adicionou outra linha de código que deveria ter sido executada apenas quando a instrução if era avaliada como verdadeira, a instrução era executada sempre. Bam, o erro "goto fail", que foi um grande erro de segurança. Mas isso não tinha nada a ver com o fato de uma declaração goto ter sido usada.
Craig

47
O bug "goto fail" da Apple foi causado diretamente pela duplicação de uma linha de código. O efeito específico desse bug foi causado por essa linha estar gotodentro de uma ifinstrução sem chave. Mas se você está sugerindo que evitar gototornará seu código imune aos efeitos de linhas duplicadas aleatoriamente, tenho más notícias para você.
user253751

12
Você está usando o comportamento do operador booleano de atalho para executar uma declaração por efeito colateral e alegando que isso é mais claro do que uma ifdeclaração? Tempos divertidos. :)
Craig

38
Se, em vez de "ir para o fracasso", houvesse uma declaração "falhado = verdadeiro", a mesma coisa teria acontecido.
gnasher729

15
Esse bug foi causado por um desenvolvedor desleixado que não prestou atenção ao que estava fazendo e não leu suas próprias alterações de código. Nada mais nada menos. Ok, talvez jogue algumas falhas de teste lá também.
Lightness Races em órbita

34

O famoso artigo da Dijkstra foi escrito em um momento em que algumas linguagens de programação eram realmente capazes de criar sub-rotinas com vários pontos de entrada e saída. Em outras palavras, você poderia literalmente pular no meio de uma função e pular em qualquer lugar dentro da função, sem realmente chamar essa função ou retornar dela da maneira convencional. Isso ainda é verdade na linguagem assembly. Ninguém nunca argumenta que essa abordagem é superior ao método estruturado de escrever software que usamos agora.

Na maioria das linguagens de programação modernas, as funções são definidas especificamente com uma entrada e um ponto de saída. O ponto de entrada é onde você especifica os parâmetros para a função e a chama, e o ponto de saída é onde você retorna o valor resultante e continua a execução na instrução após a chamada da função original.

Dentro dessa função, você deve ser capaz de fazer o que quiser, dentro da razão. Se colocar um goto ou dois na função torna mais claro ou melhora sua velocidade, por que não? O objetivo de uma função é seqüestrar um pouco da funcionalidade claramente definida, para que você não precise mais pensar em como ela funciona internamente. Depois de escrito, basta usá-lo.

E sim, você pode ter várias instruções de retorno dentro de uma função; ainda há sempre um lugar em uma função adequada a partir da qual você retorna (a parte traseira da função, basicamente). Isso não é o mesmo que saltar de uma função com um goto antes que ela tenha a chance de retornar corretamente.

insira a descrição da imagem aqui

Portanto, não se trata realmente de usar goto's. É sobre evitar o abuso deles. Todos concordam que você pode criar um programa terrivelmente complicado usando gotos, mas também pode fazer o mesmo abusando de funções (é muito mais fácil abusar dos gotos).

Pelo que vale a pena, desde que me formei nos programas BASIC no estilo de número de linha para programação estruturada usando as linguagens Pascal e chaves, nunca tive que usar um goto. A única vez em que me senti tentado a usar um é fazer uma saída antecipada de um loop aninhado (em idiomas que não suportam saídas antecipadas de vários níveis a partir de loops), mas geralmente consigo encontrar outra maneira mais limpa.


2
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
maple_shaft

11

A que tipos de erros as instruções "goto" levam? Existem exemplos historicamente significativos?

Eu costumava usar gotodeclarações ao escrever programas BASIC quando criança como uma maneira simples de obter loops for e while (o Commodore 64 BASIC não tinha loops while, e eu era imaturo demais para aprender a sintaxe e o uso adequados dos loops for ) Meu código era frequentemente trivial, mas qualquer erro de loop podia ser imediatamente atribuído ao meu uso do goto.

Agora eu uso principalmente o Python, uma linguagem de programação de alto nível que determinou que não precisa ir.

Quando Edsger Dijkstra declarou "Goto considerado prejudicial" em 1968, ele não deu um punhado de exemplos em que erros relacionados poderiam ser atribuídos ao goto, mas declarou que gotoera desnecessário para idiomas de nível superior e que deveria ser evitado em favor do que hoje consideramos o fluxo de controle normal: loops e condicionais. Suas palavras:

O uso desenfreado do go to tem uma consequência imediata de que se torna terrivelmente difícil encontrar um conjunto significativo de coordenadas para descrever o progresso do processo.
[...]
A declaração ir para como está é primitiva demais; é um convite demais para atrapalhar o programa de alguém.

Ele provavelmente tinha montanhas de exemplos de bugs toda vez que ele depurava códigos gotonele. Mas seu artigo era uma declaração de posição generalizada, apoiada em provas gotodesnecessárias para idiomas de nível superior. O erro generalizado é que você pode não ter capacidade de analisar estaticamente o código em questão.

O código é muito mais difícil de analisar estaticamente com gotoinstruções, especialmente se você voltar ao seu fluxo de controle (o que eu costumava fazer) ou a alguma seção não relacionada do código. O código pode e foi escrito dessa maneira. Foi feito para otimizar altamente recursos de computação muito escassos e, portanto, as arquiteturas das máquinas.

Um exemplo apócrifo

Havia um programa de blackjack que um mantenedor achou bastante otimizado de maneira elegante, mas também impossível de "consertar" devido à natureza do código. Foi programado em código de máquina que depende muito de gotos - então eu acho essa história bastante relevante. Este é o melhor exemplo canônico em que consigo pensar.

Um contra-exemplo

No entanto, a fonte C do CPython (a implementação mais comum e de referência do Python) usa gotodeclarações com grande efeito. Eles são usados ​​para ignorar o fluxo de controle irrelevante dentro das funções para chegar ao fim das funções, tornando as funções mais eficientes sem perder a legibilidade. Isso respeita um dos ideais da programação estruturada - ter um único ponto de saída para funções.

Para constar, acho esse contra-exemplo bastante fácil de analisar estaticamente.


5
A "História de Mel" que encontrei estava em uma arquitetura estranha, onde (1) todas as instruções de linguagem de máquina fazem um goto após sua operação e (2) era terrivelmente ineficiente colocar uma sequência de instruções em locais consecutivos de memória. E o programa foi escrito em assembly otimizado à mão, não em uma linguagem compilada.

@MilesRout Acho que você pode estar correto aqui, mas a página da wikipedia sobre programação estruturada diz: "O desvio mais comum, encontrado em muitos idiomas, é o uso de uma declaração de retorno para saída antecipada de uma sub-rotina. Isso resulta em vários pontos de saída , em vez do ponto de saída único exigido pela programação estruturada ". - você está dizendo que isso está incorreto? Você tem uma fonte para citar?
Aaron Hall

@AaronHall Esse não é o significado original.
Route de milhas 27/10/16

11

Quando a tenda era a arte arquitetônica atual, seus construtores poderiam, sem dúvida, dar conselhos práticos sobre a construção de tendas, como deixar a fumaça escapar e assim por diante. Felizmente, os construtores de hoje provavelmente podem esquecer a maioria desses conselhos.

Quando a diligência era uma arte de transporte atual, seus motoristas poderiam, sem dúvida, dar conselhos práticos sobre cavalos de diligência, como se defender de ladrões de estrada e assim por diante. Felizmente, os motoristas de hoje também podem esquecer a maioria desses conselhos.

Quando os cartões perfurados eram arte de programação atual, seus praticantes também davam conselhos práticos sobre a organização dos cartões, como numerar declarações e assim por diante. Não tenho certeza de que esse conselho seja muito relevante hoje.

Você tem idade suficiente para conhecer a frase "to number numbers" ? Você não precisa saber, mas, se não, não está familiarizado com o contexto histórico no qual o aviso contra gotoera principalmente relevante.

O aviso contra gotohoje não é muito relevante. Com o treinamento básico em enquanto / para loops e chamadas de função, você nem pensa em emitir isso com gotomuita frequência. Quando você pensa sobre isso, provavelmente tem um motivo, então vá em frente.

Mas a gotodeclaração não pode ser abusada?

Resposta: claro, pode ser abusado, mas seu abuso é um problema minúsculo na engenharia de software, comparado a erros muito mais comuns, como o uso de uma variável em que uma constante servirá ou a programação de recortar e colar (caso contrário, negligência em refatorar). Duvido que você esteja em muito perigo. A menos que você esteja usando longjmpou transferindo o controle para um código distante, se você pensa em usar um gotoou gostaria de experimentá-lo por diversão, vá em frente. Você vai ficar bem.

Você pode notar a falta de histórias de horror recentes nas quais gotointerpreta o vilão. A maioria ou todas essas histórias parecem ter 30 ou 40 anos. Você se mantém firme se considera essas histórias principalmente obsoletas.


1
Vejo que atraí pelo menos um voto negativo. Bem, isso é de se esperar. Pode haver mais votos negativos. Não vou dar a resposta convencional, no entanto, quando décadas de experiência em programação me ensinaram que, nesse caso, a resposta convencional está errada. Esta é a minha opinião, de qualquer forma. Eu dei minhas razões. O leitor pode decidir.
Th

1
Sua postagem está muito ruim. Não posso deixar comentários negativos.
Pieter B

7

Para adicionar uma coisa às outras excelentes respostas, com as gotodeclarações, pode ser difícil dizer exatamente como você chegou a um determinado local do programa. Você pode saber que ocorreu uma exceção em alguma linha específica, mas se houver gotono código, não há como saber quais instruções executadas resultam nessa exceção causando estado sem pesquisar o programa inteiro. Não há pilha de chamadas nem fluxo visual. É possível que exista uma declaração a 1000 linhas que o coloque em um estado incorreto e execute gotoa linha que gerou uma exceção.


1
Visual Basic 6 e VBA têm tratamento de erros doloroso, mas se você usar On Error Goto errh, poderá Resumetentar novamente, Resume Nextignorar a linha incorreta e Erlsaber qual linha era (se você usar números de linha).
fácil fazer o

@ CeesTimmerman Fiz muito pouco com o Visual Basic, não sabia disso. Vou adicionar um comentário sobre isso.
Qfwfq 24/10

2
@ CeesTimmerman: A semântica do VB6 de "resume" é horrível se ocorrer um erro em uma sub-rotina que não possui seu próprio bloco de erro. Resumeexecutará novamente essa função desde o início e Resume Nextignorará tudo na função que ainda não tenha sido.
Supercat 24/10

2
@ supercat Como eu disse, é doloroso. Se você precisar saber a linha exata, deve numerar todas elas ou desativar temporariamente o tratamento de erros com a On Error Goto 0Depuração manualmente. Resumedeve ser usado após a verificação Err.Numbere os ajustes necessários.
Cees Timmerman

1
Devo observar que, em uma linguagem moderna, gotosó funciona com linhas rotuladas, que parecem ter sido substituídas por loops rotulados .
Cees Timmerman

7

Goto é mais difícil para os seres humanos raciocinarem do que outras formas de controle de fluxo.

Programar o código correto é difícil. Escrever programas corretos é difícil, determinar se os programas estão corretos é difícil, provar que os programas corretos são difíceis.

Conseguir código para fazer vagamente o que você quer é fácil comparado a tudo o mais sobre programação.

goto resolve alguns programas com a obtenção de código para fazer o que você deseja. Isso não ajuda a facilitar a verificação da correção, enquanto suas alternativas costumam ajudar.

Existem estilos de programação em que o goto é a solução apropriada. Existem até estilos de programação em que seu irmão gêmeo do mal, comefrom, é a solução apropriada. Em ambos os casos, é preciso tomar muito cuidado para garantir que você o esteja usando de acordo com um padrão compreendido e muita verificação manual de que você não está fazendo algo difícil para garantir a correção.

Como exemplo, há um recurso de alguns idiomas chamado coroutines. Corotinas são threads sem thread; estado de execução sem um encadeamento para executá-lo. Você pode solicitar que eles executem, e eles podem executar parte de si mesmos e depois se suspender, devolvendo o controle de fluxo.

As rotinas "rasas" em idiomas sem suporte a rotinas (como C ++ pré-C ++ 20 e C) são possíveis usando uma mistura de gotos e gerenciamento manual de estados. As rotinas "profundas" podem ser feitas usando as funções setjmpe longjmpde C.

Há casos em que as corotinas são tão úteis que vale a pena escrevê-las manualmente e com cuidado.

No caso do C ++, eles são considerados úteis o suficiente para estender a linguagem para suportá-los. O gotogerenciamento manual e de estado está sendo oculto por trás de uma camada de abstração de custo zero, permitindo que os programadores os escrevam sem a dificuldade de provar que sua bagunça de goto, void**s, construção / destruição manual de estado etc. está correta.

O goto fica escondido atrás de uma abstração de nível superior, como whileou forou ifou switch. Essas abstrações de nível superior são mais fáceis de provar e corrigir.

Se o idioma estava faltando alguns deles (como algumas linguagens modernas estão faltando corotinas), usar um padrão que não se encaixa no problema ou usar goto torna-se suas alternativas.

Conseguir que um computador faça vagamente o que você quer fazer em casos comuns é fácil . Escrever código robusto e confiável é difícil . Ir para o primeiro ajuda muito mais do que para o segundo; portanto, "foi considerado prejudicial", pois é um sinal de código "superficialmente superficial", com erros profundos e difíceis de rastrear. Com esforço suficiente, você ainda pode criar código com "goto" confiável e robusto; portanto, manter a regra como absoluta está errada; mas como regra geral, é uma boa.


1
longjmpé legal apenas em C para recuperar a pilha de volta para a setjmpem uma função pai. (Como romper com vários níveis de aninhamento, mas para chamadas de função em vez de loops). longjjmppara um contexto de um setjmpfeito em uma função que retornou desde então não é legal (e suspeito que não funcione na prática na maioria dos casos). Não estou familiarizado com as co-rotinas, mas, a partir da descrição de uma co-rotina semelhante a um encadeamento de espaço do usuário, acho que ele precisaria de sua própria pilha e de um contexto de registro salvo, além de longjmpfornecer apenas o registro -saving, não uma pilha separada.
22816 Peter Cordes

1
Ah, eu deveria ter pesquisado no google : fanf.livejournal.com/105413.html descreve a criação de espaço na pilha para a corotina ser executada. (Que, como eu suspeitava, é necessário além de usar setjmp / longjmp).
22616 Peter Cordes

2
@ PeterCordes: as implementações não são necessárias para fornecer qualquer meio pelo qual o código possa criar um par jmp_buff adequado para uso como uma rotina. Algumas implementações especificam um meio pelo qual o código pode criar um, mesmo que o Padrão não exija que eles forneçam essa capacidade. Eu não descreveria código que se baseia em técnicas como "padrão C", pois em muitos casos um jmp_buff será uma estrutura opaca que não garante o comportamento consistente entre diferentes compiladores.
Supercat

6

Dê uma olhada neste código, em http://www-personal.umich.edu/~axe/research/Software/CC/CC2/TourExec1.1.f.html, que na verdade fazia parte da simulação de um grande dilema do prisioneiro. Se você já viu códigos FORTRAN ou BASIC antigos, perceberá que não é tão incomum.

C  Not nice rules in second round of tour (cut and pasted 7/15/93)
   FUNCTION K75R(J,M,K,L,R,JA)
C  BY P D HARRINGTON
C  TYPED BY JM 3/20/79
   DIMENSION HIST(4,2),ROW(4),COL(2),ID(2)
   K75R=JA       ! Added 7/32/93 to report own old value
   IF (M .EQ. 2) GOTO 25
   IF (M .GT. 1) GOTO 10
   DO 5 IA = 1,4
     DO 5 IB = 1,2
5  HIST(IA,IB) = 0

   IBURN = 0
   ID(1) = 0
   ID(2) = 0
   IDEF = 0
   ITWIN = 0
   ISTRNG = 0
   ICOOP = 0
   ITRY = 0
   IRDCHK = 0
   IRAND = 0
   IPARTY = 1
   IND = 0
   MY = 0
   INDEF = 5
   IOPP = 0
   PROB = .2
   K75R = 0
   RETURN

10 IF (IRAND .EQ. 1) GOTO 70
   IOPP = IOPP + J
   HIST(IND,J+1) = HIST(IND,J+1) + 1
   IF (M .EQ. 15 .OR. MOD(M,15) .NE. 0 .OR. IRAND .EQ. 2) GOTO 25
   IF (HIST(1,1) / (M - 2) .GE. .8) GOTO 25
   IF (IOPP * 4 .LT. M - 2 .OR. IOPP * 4 .GT. 3 * M - 6) GOTO 25
   DO 12 IA = 1,4
12 ROW(IA) = HIST(IA,1) + HIST(IA,2)

   DO 14 IB = 1,2
     SUM = .0
     DO 13 IA = 1,4
13   SUM = SUM + HIST(IA,IB)
14 COL(IB) = SUM

   SUM = .0
   DO 16 IA = 1,4
     DO 16 IB = 1,2
       EX = ROW(IA) * COL(IB) / (M - 2)
       IF (EX .LE. 1.) GOTO 16
       SUM = SUM + ((HIST(IA,IB) - EX) ** 2) / EX
16 CONTINUE

   IF (SUM .GT. 3) GOTO 25
   IRAND = 1
   K75R = 1
   RETURN

25 IF (ITRY .EQ. 1 .AND. J .EQ. 1) IBURN = 1
   IF (M .LE. 37 .AND. J .EQ. 0) ITWIN = ITWIN + 1
   IF (M .EQ. 38 .AND. J .EQ. 1) ITWIN = ITWIN + 1
   IF (M .GE. 39 .AND. ITWIN .EQ. 37 .AND. J .EQ. 1) ITWIN = 0
   IF (ITWIN .EQ. 37) GOTO 80
   IDEF = IDEF * J + J
   IF (IDEF .GE. 20) GOTO 90
   IPARTY = 3 - IPARTY
   ID(IPARTY) = ID(IPARTY) * J + J
   IF (ID(IPARTY) .GE. INDEF) GOTO 78
   IF (ICOOP .GE. 1) GOTO 80
   IF (M .LT. 37 .OR. IBURN .EQ. 1) GOTO 34
   IF (M .EQ. 37) GOTO 32
   IF (R .GT. PROB) GOTO 34
32 ITRY = 2
   ICOOP = 2
   PROB = PROB + .05
   GOTO 92

34 IF (J .EQ. 0) GOTO 80
   GOTO 90

70 IRDCHK = IRDCHK + J * 4 - 3
   IF (IRDCHK .GE. 11) GOTO 75
   K75R = 1
   RETURN

75 IRAND = 2
   ICOOP = 2
   K75R = 0
   RETURN

78 ID(IPARTY) = 0
   ISTRNG = ISTRNG + 1
   IF (ISTRNG .EQ. 8) INDEF = 3
80 K75R = 0
   ITRY = ITRY - 1
   ICOOP = ICOOP - 1
   GOTO 95

90 ID(IPARTY) = ID(IPARTY) + 1
92 K75R = 1
95 IND = 2 * MY + J + 1
   MY = K75R
   RETURN
   END

Há muitos problemas aqui que vão muito além da declaração do GOTO aqui; Sinceramente, acho que a declaração do GOTO foi um bode expiatório. Mas o fluxo de controle não é absolutamente claro aqui, e o código é misturado de maneiras que tornam muito claro o que está acontecendo. Mesmo sem adicionar comentários ou usar nomes de variáveis ​​melhores, alterar isso para uma estrutura de blocos sem GOTOs tornaria muito mais fácil ler e seguir.


4
tão verdadeiro :-) Vale a pena mencionar: legado FORTAN e BASIC não tinham estruturas de blocos, de modo que, se uma cláusula THEN não pudesse conter uma linha, o GOTO seria a única alternativa.
Christophe

Rótulos de texto em vez de números, ou pelo menos comentários nas linhas numeradas, ajudariam muito.
Cees Timmerman

De volta aos meus dias no FORTRAN-IV: restringi meu uso do GOTO para implementar blocos IF / ELSE IF / ELSE, enquanto os loops e BREAK e NEXT nos loops. Alguém me mostrou os males do código do espaguete e por que você deve estruturá-lo para evitá-lo, mesmo que seja necessário implementar suas estruturas de bloco com o GOTO. Mais tarde, comecei a usar um pré-processador (RATFOR, IIRC). Ir não é intrinsecamente mau, é apenas uma construção de baixo nível excessivamente poderosa que mapeia para uma instrução de ramificação do assembler. Se você precisar usá-lo porque seu idioma é deficiente, use-o para criar ou aumentar as estruturas de blocos apropriadas.
nigel222

@ CeesTimmerman: Esse é o código FORTRAN IV da variedade de jardins. Os rótulos de texto não são suportados no FORTRAN IV. Não sei se as versões mais recentes do idioma as suportam. Comentários na mesma linha do código não eram suportados no FORTRAN IV padrão, embora possa ter havido extensões específicas do fornecedor para suportá-los. Não sei o que diz o padrão FORTRAN "atual": abandonei o FORTRAN há muito tempo e não sinto falta dele. (A mais recente código Fortran que eu vi tem uma forte semelhança com o início da década de 1970 PASCAL.)
John R. Strohm

1
@ CeesTimmerman: extensão específica do fornecedor. O código em geral é REALMENTE tímido nos comentários. Além disso, há uma convenção que se tornou comum em alguns lugares, de colocar números de linhas apenas nas instruções CONTINUE e FORMAT, para facilitar a adição de linhas posteriormente. Este código não segue essa convenção.
John R. Strohm

0

Um dos princípios da programação sustentável é o encapsulamento . O ponto é que você faz interface com um módulo / rotina / sub-rotina / componente / objeto usando uma interface definida, e somente essa interface, e os resultados serão previsíveis (assumindo que a unidade foi efetivamente testada).

Dentro de uma única unidade de código, o mesmo princípio se aplica. Se você aplicar consistentemente a programação estruturada ou os princípios de programação orientada a objetos, não irá:

  1. crie caminhos inesperados através do código
  2. chegam em seções de código com valores variáveis ​​indefinidos ou não permitidos
  3. sair de um caminho do código com valores variáveis ​​indefinidos ou não permitidos
  4. falha ao concluir uma unidade de transação
  5. deixe porções de código ou dados na memória efetivamente inoperantes, mas inelegíveis para limpeza e realocação de lixo, porque você não sinalizou seu lançamento usando uma construção explícita que chama a liberação deles quando o caminho de controle de código sai da unidade

Alguns dos sintomas mais comuns desses erros de processamento incluem vazamentos de memória, acumulação de memória, estouros de ponteiros, falhas, registros de dados incompletos, exceções de adição / alteração de registro / exclusão de registro, falhas de página de memória e assim por diante.

As manifestações observáveis ​​pelo usuário desses problemas incluem bloqueio da interface do usuário, desempenho gradualmente reduzido, registros de dados incompletos, incapacidade de iniciar ou concluir transações, dados corrompidos, interrupções na rede, interrupções de energia, falhas nos sistemas incorporados (da perda do controle de mísseis à perda da capacidade de rastreamento e controle nos sistemas de controle de tráfego aéreo), falhas no temporizador e assim por diante. O catálogo é muito extenso.


3
Você respondeu sem mencionar gotonem uma vez. :)
Robert Harvey

0

Ir é perigoso porque geralmente é usado onde não é necessário. Usar qualquer coisa que não seja necessária é perigoso, mas especialmente. Se você pesquisar no Google, encontrará muitos erros causados ​​pelo goto, isso não é um motivo para não usá-lo (os erros sempre acontecem quando você usa recursos de linguagem porque isso é intrínseco à programação), mas alguns deles são claramente altamente relacionados para ir ao uso.


Razões para usar / não usar goto :

  • Se você precisar de um loop, precisará usar whileou for.

  • Se você precisar fazer um salto condicional, use if/then/else

  • Se você precisar de um procedimento, chame uma função / método.

  • Se você precisar sair de uma função, apenas return.

Posso contar com os dedos locais onde vi gotousados ​​e usados ​​corretamente.

  • CPython
  • libKTX
  • provavelmente mais alguns

No libKTX, existe uma função que possui o seguinte código

if(something)
    goto cleanup;

if(bla)
    goto cleanup;

cleanup:
    delete [] array;

Agora neste local gotoé útil porque o idioma é C:

  • nós estamos dentro de uma função
  • não podemos escrever uma função de limpeza (porque entramos em outro escopo e tornar o estado da função de chamador acessível é mais oneroso)

Esse caso de uso é útil, pois em C não temos classes, portanto, a maneira mais simples de limpar é usando a goto.

Se tivéssemos o mesmo código em C ++, não há mais necessidade de ir para:

class MyClass{
    public:

    void Function(){
        if(something)
            return Cleanup(); // invalid syntax in C#, but valid in C++
        if(bla)
            return Cleanup(); // invalid syntax in C#, but valid in C++
    }

    // access same members, no need to pass state (compiler do it for us).
    void Cleanup(){

    }



}

Que tipo de erros ele pode levar? Qualquer coisa. Loops infinitos, ordem de execução incorreta, parafuso de pilha ..

Um caso documentado é uma vulnerabilidade no SSL que permitiu ataques Man in the middle causados ​​pelo uso incorreto do goto: aqui está o artigo

Esse é um erro de digitação, mas passou despercebido por um tempo, se o código fosse estruturado de outra maneira, testado corretamente, esse erro poderia não ter sido possível.


2
Há muitos comentários em uma resposta que explicam muito claramente que o uso de "goto" era totalmente irrelevante para o bug do SSL - o bug foi causado pela duplicação descuidada de uma linha de código, por isso foi executada condicionalmente e incondicionalmente.
gnasher729

Sim, estou aguardando um exemplo histórico melhor de um erro de goto antes de aceitar. Como dito; Realmente não importava o que havia nessa linha de código; A falta de chaves seria executada e causaria um erro independentemente. Estou curioso, porém; O que é um "Stack Screw"?
Akiva

1
Um exemplo do código em que trabalhei anteriormente no PL / 1 CICS. O programa exibiu uma tela que consiste em mapas de linha única. Quando a tela foi retornada, encontrou o conjunto de mapas que havia enviado. Usou os elementos para encontrar o índice em uma matriz e, em seguida, usou esse índice em um índice de variáveis ​​da matriz para executar um GOTO, para que ele pudesse processar esse mapa individual. Se a matriz de mapas enviados estiver incorreta (ou tiver sido corrompida), poderá tentar executar um GOTO no rótulo errado ou piorar, porque acessou um elemento após o final da matriz de variáveis ​​de rótulo. Reescrito para usar a sintaxe do tipo de caso.
Kickstart
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.