Por que projetar uma linguagem moderna sem um mecanismo de tratamento de exceções?


47

Muitas linguagens modernas fornecem recursos avançados de tratamento de exceções , mas a linguagem de programação Swift da Apple não fornece um mecanismo de tratamento de exceções .

Imerso em exceções como eu sou, estou tendo problemas para entender o que isso significa. Swift tem afirmações e, é claro, retorna valores; mas estou tendo problemas para imaginar como minha maneira de pensar baseada em exceções é mapeada para um mundo sem exceções (e, nesse caso, por que esse mundo é desejável ). Existem coisas que não posso fazer em um idioma como o Swift que eu poderia fazer com exceções? Ganho algo perdendo exceções?

Como, por exemplo, posso expressar melhor algo como

try:
    operation_that_can_throw_ioerror()
except IOError:
    handle_the_exception_somehow()
else:
     # we don't want to catch the IOError if it's raised
    another_operation_that_can_throw_ioerror()
finally:
    something_we_always_need_to_do()

em um idioma (Swift, por exemplo) que não possui tratamento de exceção?


11
Você pode adicionar Ir à lista , se simplesmente ignorarmos o panicque não é exatamente o mesmo. Além do que é dito lá, uma exceção não é muito mais do que uma maneira sofisticada (mas confortável) de executar uma GOTO, embora ninguém chame dessa maneira, por razões óbvias.
JensG

3
A resposta ordenada para sua pergunta é que você precisa de suporte ao idioma para exceções para escrevê-las. O suporte ao idioma geralmente inclui gerenciamento de memória; como uma exceção pode ser lançada em qualquer lugar e capturada em qualquer lugar, é preciso haver uma maneira de descartar objetos que não dependem do fluxo de controle.
Robert Harvey

1
Não estou te seguindo muito, @Robert. O C ++ consegue oferecer suporte a exceções sem coleta de lixo.
Karl Bielefeldt

3
@KarlBielefeldt: Com grandes despesas, pelo que entendi. Então, novamente, há algo que é realizado em C ++ sem grandes despesas, pelo menos em esforço e conhecimento de domínio necessário?
Robert Harvey

2
@RobertHarvey: Bom ponto. Sou uma dessas pessoas que não pensa o suficiente sobre essas coisas. Fui enganado ao pensar que o ARC é GC, mas, é claro, não é. Então, basicamente (se eu estiver entendendo, grosso modo), as exceções seriam uma coisa bagunçada (apesar de C ++) em uma linguagem em que o descarte de objetos dependia do fluxo de controle?
orome

Respostas:


33

Na programação incorporada, tradicionalmente, as exceções não eram permitidas, porque a sobrecarga da pilha que você precisava fazer era considerada uma variabilidade inaceitável ao tentar manter o desempenho em tempo real. Embora os smartphones possam tecnicamente ser considerados plataformas em tempo real, eles são poderosos o suficiente agora, onde as antigas limitações dos sistemas embarcados não se aplicam mais. Eu apenas trago isso por uma questão de rigor.

As exceções geralmente são suportadas em linguagens de programação funcionais, mas tão raramente são usadas que podem não ser. Um dos motivos é a avaliação preguiçosa, que é feita ocasionalmente, mesmo em idiomas que não são preguiçosos por padrão. Ter uma função que é executada com uma pilha diferente do local em que foi colocado na fila para executar dificulta a determinação de onde colocar seu manipulador de exceções.

O outro motivo é que as funções de primeira classe permitem construções como opções e futuros que fornecem os benefícios sintáticos das exceções com mais flexibilidade. Em outras palavras, o restante do idioma é expressivo o suficiente para que as exceções não comprem nada.

Não estou familiarizado com o Swift, mas o pouco que li sobre o tratamento de erros sugere que eles pretendem que o tratamento de erros siga padrões de estilo funcional. Eu já vi exemplos de código successe failureblocos que se parecem muito com futuros.

Aqui está um exemplo usando um Futurede este tutorial Scala :

val f: Future[List[String]] = future {
  session.getRecentPosts
}
f onFailure {
  case t => println("An error has occured: " + t.getMessage)
}
f onSuccess {
  case posts => for (post <- posts) println(post)
}

Você pode ver que ele tem aproximadamente a mesma estrutura do seu exemplo usando exceções. O futurebloco é como a try. O onFailurebloco é como um manipulador de exceções. No Scala, como na maioria das linguagens funcionais, Futureé implementado completamente usando a própria linguagem. Não requer nenhuma sintaxe especial, como as exceções. Isso significa que você pode definir suas próprias construções semelhantes. Talvez adicione um timeoutbloco, por exemplo, ou funcionalidade de log.

Além disso, você pode repassar o futuro, retorná-lo da função, armazená-lo em uma estrutura de dados ou qualquer outra coisa. É um valor de primeira classe. Você não está limitado como exceções que devem ser propagadas diretamente para a pilha.

As opções resolvem o problema de manipulação de erros de uma maneira um pouco diferente, o que funciona melhor em alguns casos de uso. Você não está preso apenas a um método.

Esses são os tipos de coisas que você "ganha ao perder exceções".


1
Portanto, Futureé essencialmente uma maneira de examinar o valor retornado de uma função, sem parar para esperar por ela. Como o Swift, é baseado em valor de retorno, mas, diferentemente do Swift, a resposta ao valor de retorno pode ocorrer posteriormente (um pouco como as exceções). Direito?
Orome # 4/14

1
Você entende Futurecorretamente, mas acho que pode estar descaracterizando o Swift. Veja a primeira parte desta resposta do stackoverflow , por exemplo.
Karl Bielefeldt

Hmm, eu sou novo no Swift, então essa resposta é um pouco difícil para eu analisar. Mas se não me engano: isso passa essencialmente por um manipulador que pode ser invocado posteriormente; direito?
Orome 4/10

Sim. Você está basicamente criando um retorno de chamada quando ocorre um erro.
Karl Bielefeldt

Eitherseria um exemplo melhor IMHO
Paweł Prażak

19

Exceções podem dificultar o raciocínio do código. Embora não sejam tão poderosos quanto os gotos, eles podem causar muitos dos mesmos problemas devido à sua natureza não local. Por exemplo, digamos que você tenha um código imperativo como este:

cleanMug();
brewCoffee();
pourCoffee();
drinkCoffee();

Você não pode dizer rapidamente se algum desses procedimentos pode gerar uma exceção. Você precisa ler a documentação de cada um desses procedimentos para descobrir isso. (Alguns idiomas tornam isso um pouco mais fácil, aumentando a assinatura de tipo com essas informações.) O código acima será compilado muito bem, independentemente de algum dos procedimentos ser lançado, tornando muito fácil esquecer o tratamento de uma exceção.

Além disso, mesmo que a intenção seja propagar a exceção de volta para o chamador, muitas vezes é necessário adicionar código adicional para impedir que as coisas sejam deixadas em um estado inconsistente (por exemplo, se sua cafeteira quebrar, você ainda precisará limpar a bagunça e retornar a caneca!). Portanto, em muitos casos, o código que usa exceções pareceria tão complexo quanto o código que não foi devido à limpeza extra necessária.

Exceções podem ser emuladas com um sistema de tipos suficientemente poderoso. Muitos dos idiomas que evitam exceções usam valores de retorno para obter o mesmo comportamento. É semelhante à maneira como é feito em C, mas os sistemas de tipos modernos geralmente tornam mais elegante e também mais difícil de esquecer de lidar com a condição de erro. Eles também podem fornecer açúcar sintático para tornar as coisas menos complicadas, às vezes quase tão limpas quanto seriam, com exceções.

Em particular, ao incorporar o tratamento de erros no sistema de tipos, em vez de implementá-lo como um recurso separado, permite que "exceções" sejam usadas para outras coisas que nem estão relacionadas a erros. (É sabido que o tratamento de exceções é na verdade uma propriedade de mônadas.)


É correto que tipo de sistema de tipos que Swift possui, incluindo opcionais, seja o tipo de "sistema de tipos poderoso" que consegue isso?
Orome # 4/14

2
Sim, os tipos opcionais e, geralmente, de soma (referidos como "enum" em Swift / Rust) podem conseguir isso. No entanto, é necessário um trabalho extra para torná-los agradáveis ​​de usar: no Swift, isso é alcançado com a sintaxe de encadeamento opcional; em Haskell, isso é alcançado com a doação monádica.
Rufflewind 4/04

Um "suficientemente poderoso sistema de tipo" pode dar um rastreamento de pilha, se não é bastante inútil
Paweł Prazak

1
+1 por apontar que as exceções obscurecem o fluxo de controle. Apenas como uma observação auxiliar: É lógico que as exceções não sejam realmente mais más do que goto: A gotoé restrita a uma única função, que é um intervalo muito pequeno, desde que a função seja realmente pequena, a exceção age mais como um cruzamento de gotoe come from(consulte en.wikipedia.org/wiki/INTERCAL ;-)). Ele pode conectar praticamente qualquer dois pedaços de código, possivelmente ignorando algumas terceira funções do código. A única coisa que não pode fazer, o que gotopode, é voltar atrás.
C28 de

2
@ PawełPrażak Ao trabalhar com muitas funções de ordem superior, os rastreamentos de pilha não são tão valiosos. Fortes garantias sobre entradas e saídas e prevenção de efeitos colaterais são o que impede esse indireto de causar bugs confusos.
Jack

11

Há ótimas respostas aqui, mas acho que um motivo importante não foi enfatizado o suficiente: Quando ocorrem exceções, os objetos podem ser deixados em estados inválidos. Se você pode "capturar" uma exceção, seu código manipulador de exceções poderá acessar e trabalhar com esses objetos inválidos. Isso vai ser terrivelmente errado, a menos que o código para esses objetos tenha sido escrito perfeitamente, o que é muito, muito difícil de fazer.

Por exemplo, imagine implementar Vector. Se alguém instancia seu vetor com um conjunto de objetos, mas ocorre uma exceção durante a inicialização (talvez, digamos, ao copiar seus objetos na memória recém-alocada), é muito difícil codificar corretamente a implementação do vetor de maneira que não a memória vazou. Este pequeno artigo de Stroustroup aborda o exemplo vetorial .

E isso é apenas a ponta do iceberg. E se, por exemplo, você tivesse copiado alguns dos elementos, mas não todos? Para implementar corretamente um contêiner como o Vector, você quase precisa fazer com que todas as ações executadas sejam reversíveis, para que toda a operação seja atômica (como uma transação de banco de dados). Isso é complicado e a maioria dos aplicativos erra. E mesmo quando feito corretamente, complica bastante o processo de implementação do contêiner.

Portanto, algumas línguas modernas decidiram que não vale a pena. Rust, por exemplo, tem exceções, mas elas não podem ser "capturadas"; portanto, não há como o código interagir com objetos em um estado inválido.


1
o objetivo da captura é tornar um objeto consistente (ou encerrar algo, se não for possível) após a ocorrência de um erro.
JoulinRouge

@JoulinRouge eu sei. Mas alguns idiomas decidiram não lhe dar essa oportunidade, mas travar todo o processo. Esses designers de idiomas sabem o tipo de limpeza que você gostaria de fazer, mas concluíram que é muito complicado fornecer isso, e as vantagens e desvantagens envolvidas nesse processo não valeriam a pena. Sei que você pode não concordar com a escolha deles ... mas é importante saber que eles fizeram a escolha conscientemente por esses motivos específicos.
Charlie Flowers

6

Uma coisa que me surpreendeu inicialmente sobre a linguagem Rust é que ela não suporta exceções de captura. Você pode lançar exceções, mas somente o tempo de execução pode capturá-las quando uma tarefa (pense em um encadeamento, mas nem sempre um encadeamento separado do SO) morre; se você mesmo iniciar uma tarefa, poderá perguntar se ela saiu normalmente ou se foi fail!()concluída.

Como tal, não é idiomático com failmuita frequência. Os poucos casos em que isso ocorre são, por exemplo, no equipamento de teste (que não sabe como é o código do usuário), como o nível superior de um compilador (a maioria dos compiladores bifurca-se) ou ao chamar um retorno de chamada na entrada do usuário.

Em vez disso, o idioma comum é usar o Resultmodelo para transmitir explicitamente os erros que devem ser tratados. Esta é feita significativamente mais fácil pela try!macro , que é pode ser enrolado em torno de qualquer expressão que produz um resultado e produz o braço de sucesso, se houver um, ou caso contrário retorna cedo a partir da função.

use std::io::IoResult;
use std::io::File;

fn load_file(name: &Path) -> IoResult<String>
{
    let mut file = try!(File::open(name));
    let s = try!(file.read_to_string());
    return Ok(s);
}

fn main()
{
    print!("{}", load_file(&Path::new("/tmp/hello")).unwrap());
}

1
Então, é justo dizer que isso também (como a abordagem de Go) é semelhante ao Swift, que tem assert, mas não catch?
Orome # 4/14

1
Em Swift, tente! significa: Sim, eu sei que isso pode falhar, mas tenho certeza que não, por isso não estou lidando com isso e, se falhar, meu código está errado, por isso, falhe nesse caso.
precisa saber é o seguinte

6

Na minha opinião, as exceções são uma ferramenta essencial para detectar erros de código em tempo de execução. Tanto em testes quanto em produção. Torne suas mensagens detalhadas o suficiente para que, em combinação com um rastreamento de pilha, você possa descobrir o que aconteceu em um log.

As exceções são principalmente uma ferramenta de desenvolvimento e uma maneira de obter relatórios razoáveis ​​de erros da produção em casos inesperados.

Além da separação de preocupações (caminho feliz com apenas erros esperados vs. falhas até chegar a um manipulador genérico para erros inesperados), é uma coisa boa, tornando seu código mais legível e sustentável, é impossível preparar seu código para todos os possíveis casos inesperados, mesmo inchando-o com o código de tratamento de erros para concluir a ilegibilidade.

Esse é realmente o significado de "inesperado".

Aliás, o que é esperado e o que não é uma decisão que só pode ser tomada no local da chamada. É por isso que as exceções verificadas em Java não deram certo - a decisão é tomada no momento do desenvolvimento de uma API, quando não está claro o que é esperado ou inesperado.

Exemplo simples: a API de um mapa de hash pode ter dois métodos:

Value get(Key)

e

Option<Value> getOption(key)

o primeiro lançando uma exceção, se não encontrado, o último fornecendo um valor opcional. Em alguns casos, o último faz mais sentido, mas em outros, seu código simplesmente deve esperar que exista um valor para uma determinada chave; portanto, se não houver uma, é um erro que esse código não pode corrigir porque um código básico suposição falhou. Nesse caso, é realmente o comportamento desejado cair fora do caminho do código e cair em algum manipulador genérico, caso a chamada falhe.

O código nunca deve tentar lidar com suposições básicas com falha.

Exceto verificando-os e lançando exceções bem legíveis, é claro.

Lançar exceções não é mau, mas capturá-las pode ser. Não tente corrigir erros inesperados. Capture exceções em alguns lugares em que deseja continuar com algum loop ou operação, registre-as, talvez relate um erro desconhecido, e é isso.

Blocos de captura em todo o lugar são uma péssima idéia.

Crie suas APIs de maneira a facilitar a expressão de sua intenção, ou seja, declarando se você espera um determinado caso, como chave não encontrada ou não. Os usuários da sua API podem escolher a chamada de lançamento apenas para casos realmente inesperados.

Eu acho que o motivo pelo qual as pessoas se ressentem de exceções e vão longe demais ao omitir essa ferramenta crucial para automação do tratamento de erros e melhor separação de preocupações de novos idiomas são experiências ruins.

Isso e alguns mal-entendidos sobre o que eles realmente são bons.

Simulá-los, fazendo TUDO através de ligação monádica torna seu código menos legível e de fácil manutenção, e você acaba sem um rastreamento de pilha, o que torna essa abordagem MUITO pior.

O tratamento de erros de estilo funcional é excelente para os casos de erro esperados.

Deixe o tratamento de exceções cuidar automaticamente de todo o resto, é para isso que serve :)


3

O Swift usa os mesmos princípios aqui do Objective-C, apenas mais consequentemente. No Objective-C, as exceções indicam erros de programação. Eles não são manipulados, exceto pelas ferramentas de relatório de falhas. "Tratamento de exceção" é feito fixando o código. (Existem algumas exceções. Por exemplo, nas comunicações entre processos. Mas isso é muito raro e muitas pessoas nunca se deparam com isso. E o Objective-C realmente tem try / catch / finalmente / throw, mas você raramente as usa). Swift acabou de remover a possibilidade de capturar exceções.

O Swift possui um recurso que se parece com o tratamento de exceções, mas é apenas o tratamento de erros forçado. Historicamente, o Objective-C tinha um padrão de manipulação de erros bastante difundido: um método retornaria uma BOOL (YES para êxito) ou uma referência de objeto (nada para falha, nada para sucesso) e teria o parâmetro "ponteiro para NSError *" que seria usado para armazenar uma referência NSError. O Swift converte automaticamente chamadas para esse método em algo parecido com o tratamento de exceções.

Em geral, as funções Swift podem retornar facilmente alternativas, como um resultado se uma função funcionou bem e um erro se falhou; isso facilita muito o tratamento de erros. Mas a resposta para a pergunta original: os designers do Swift obviamente sentiram que criar uma linguagem segura e escrever código bem-sucedido nessa linguagem é mais fácil se o idioma não tiver exceções.


Para Swift, esta é a resposta correta, IMO. O Swift precisa permanecer compatível com as estruturas existentes do sistema Objective-C, para que não ocorram exceções tradicionais. Eu escrevi um post de blog há um tempo atrás sobre como o ObjC funciona no tratamento de erros: orangejuiceliberationfront.com/…
uliwitness

2
 int result;
 if((result = operation_that_can_throw_ioerror()) == IOError)
 {
  handle_the_exception_somehow();
 }
 else
 {
   # we don't want to catch the IOError if it's raised
   result = another_operation_that_can_throw_ioerror();
 }
 result |= something_we_always_need_to_do();
 return result;

Em C, você terminaria com algo como o acima.

Existem coisas que não posso fazer no Swift que eu poderia fazer com exceções?

Não, não há nada. Você acaba manipulando códigos de resultados em vez de exceções.
As exceções permitem reorganizar seu código para que a manipulação de erros seja separada do código do caminho feliz, mas é isso.


E, da mesma forma, essas chamadas para ...throw_ioerror()retornar erros em vez de lançar exceções?
orome

1
@raxacoricofallapatorius Se estamos alegando que não existem exceções, suponho que o programa siga o padrão usual de retornar códigos de erro em caso de falha e 0 em caso de sucesso.
Stonemetal

1
@stonemetal Alguns idiomas, como Rust e Haskell, usam o sistema de tipos para retornar algo mais significativo que um código de erro, sem adicionar pontos de saída ocultos, como as exceções. Uma função Rust pode, por exemplo, retornar uma Result<T, E>enumeração, que pode ser um Ok<T>, ou um Err<E>, com To tipo que você deseja, se houver, e o Etipo que representa um erro. A correspondência de padrões e alguns métodos específicos simplificam o tratamento de sucessos e falhas. Em resumo, não assuma que a falta de exceções signifique automaticamente códigos de erro.
bitbit

1

Além da resposta de Charlie:

Esses exemplos de manipulação de exceção declarada que você vê em muitos manuais e livros, parecem muito inteligentes apenas em exemplos muito pequenos.

Mesmo se você deixar de lado o argumento sobre o estado inválido do objeto, eles sempre causam uma enorme dor ao lidar com um aplicativo grande.

Por exemplo, quando você precisa lidar com E / S, usando alguma criptografia, pode ter 20 tipos de exceções que podem ser lançadas em 50 métodos. Imagine a quantidade de código de manipulação de exceção que você precisará. O tratamento de exceções levará várias vezes mais código que o próprio código.

Na realidade, você sabe quando a exceção não pode aparecer e você nunca precisa escrever tanta manipulação de exceção; portanto, basta usar algumas soluções alternativas para ignorar as exceções declaradas. Na minha prática, apenas cerca de 5% das exceções declaradas precisam ser tratadas no código para ter um aplicativo confiável.


Bem, na realidade, essas exceções geralmente podem ser tratadas em apenas um lugar. Por exemplo, em uma função "baixar dados de atualização" se o SSL falhar ou o DNS não puder ser resolvido ou o servidor da Web retornar um 404, não importa, pegue-o na parte superior e relate o erro ao usuário.
Zan Lynx
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.