Exceções, códigos de erro e uniões discriminadas

80

Recentemente, iniciei um trabalho de programação em C #, mas tenho bastante experiência em Haskell.

Mas eu entendo que C # é uma linguagem orientada a objetos, não quero forçar um pino redondo em um buraco quadrado.

Eu li o artigo Lançamento de exceção da Microsoft, que afirma:

NÃO retorne códigos de erro.

Mas, como estou acostumado a Haskell, tenho usado o tipo de dados C # OneOf, retornando o resultado como o valor "certo" ou o erro (geralmente uma Enumeração) como o valor "esquerdo".

Isso é muito parecido com a convenção de EitherHaskell.

Para mim, isso parece mais seguro do que exceções. No C #, ignorar exceções não produz um erro de compilação e, se elas não forem detectadas, elas simplesmente explodem e travam o programa. Talvez isso seja melhor do que ignorar um código de erro e produzir um comportamento indefinido, mas travar o software do seu cliente ainda não é uma coisa boa, principalmente quando ele executa muitas outras tarefas comerciais importantes em segundo plano.

Com OneOf, é preciso ser bem explícito sobre como descompactar e manipular o valor de retorno e os códigos de erro. E se alguém não sabe como lidar com isso nesse estágio na pilha de chamadas, ele precisa ser colocado no valor de retorno da função atual, para que os chamadores saibam que um erro pode resultar.

Mas essa não parece ser a abordagem sugerida pela Microsoft.

O uso em OneOfvez de exceções para lidar com exceções "comuns" (como Arquivo Não Encontrado, etc.) é uma abordagem razoável ou é uma prática terrível?


Vale a pena notar que ouvi dizer que as exceções como fluxo de controle são consideradas um antipadrão sério ; portanto, se a "exceção" é algo que você normalmente trataria sem encerrar o programa, não é esse "fluxo de controle" de alguma maneira? Eu entendo que há um pouco de uma área cinzenta aqui.

Observe que não estou usando OneOfpara coisas como "Memória insuficiente", condições das quais não espero me recuperar ainda geram exceções. Mas sinto-me como questões razoáveis, como a entrada do usuário que não analisa são essencialmente "fluxo de controle" e provavelmente não devem gerar exceções.


Pensamentos subsequentes:

Nesta discussão, o que estou retirando atualmente é o seguinte:

  1. Se você espera que o chamador imediato catchlide com a exceção na maior parte do tempo e continue seu trabalho, talvez por outro caminho, ele provavelmente deve fazer parte do tipo de retorno. Optionalou OneOfpode ser útil aqui.
  2. Se você espera que o chamador imediato não capte a exceção na maioria das vezes, lance uma exceção, para economizar a bobagem de passá-la manualmente para a pilha.
  3. Se você não tiver certeza do que o chamador imediato fará, talvez forneça os dois, como Parsee TryParse.
Clinton
fonte
13
Use F # Result;-)
Astrinus
8
Um pouco fora do tópico, mas observe que a abordagem que você está vendo tem a vulnerabilidade geral de que não há como garantir que o desenvolvedor tenha tratado o erro corretamente. Claro, o sistema de digitação pode funcionar como um lembrete, mas você perde o padrão de explodir o programa por não conseguir lidar com um erro, o que torna mais provável que você acabe em um estado inesperado. Se o lembrete vale a pena perder esse padrão, é um debate aberto. Estou inclinado a pensar que é melhor escrever todo o seu código, supondo que uma exceção a encerre em algum momento; então o lembrete tem menos valor.
precisa saber é o seguinte
9
Artigo relacionado: Eric Lippert sobre os quatro "baldes" de exceções . O exemplo que ele dá sobre idéias exógenas mostra que existem cenários em que você apenas precisa lidar com exceções, ie FileNotFoundException.
Søren D. Ptæus
7
Posso estar sozinho nisso, mas acho que bater é bom . Não há evidências melhores de que haja um problema no seu software do que um log de falha com o rastreamento adequado. Se você tem uma equipe capaz de corrigir e implantar um patch em 30 minutos ou menos, permitir que esses amigos feios apareçam pode não ser uma coisa ruim.
T. Sar - Restabelece Monica
9
Como é que nenhuma das respostas esclarece que a Eithermônada não é um código de erro e nem é OneOf? Eles são fundamentalmente diferentes, e a pergunta, consequentemente, parece se basear em um mal-entendido. (Embora em uma forma modificada ainda é uma pergunta válida.)
Konrad Rudolph

Respostas:

131

mas travar o software do seu cliente ainda não é uma coisa boa

Certamente é uma coisa boa.

Você deseja que qualquer coisa que deixe o sistema em um estado indefinido pare o sistema, porque um sistema indefinido pode fazer coisas desagradáveis, como dados corrompidos, formatar o disco rígido e enviar e-mails ameaçadores ao presidente. Se você não conseguir recuperar e colocar o sistema novamente em um estado definido, a falha é a coisa responsável a fazer. É exatamente por isso que criamos sistemas que falham, em vez de se separarem silenciosamente. Agora, com certeza, todos nós queremos um sistema estável que nunca trava, mas só queremos isso quando o sistema permanece em um estado seguro previsível definido.

Ouvi dizer que exceções como fluxo de controle são consideradas um sério antipadrão

Isso é absolutamente verdade, mas geralmente é mal compreendido. Quando eles inventaram o sistema de exceção, tinham medo de interromper a programação estruturada. Programação estruturada é por isso que temos for, while, until, break, e continuequando tudo o que precisamos, para fazer tudo isso, é goto.

Dijkstra nos ensinou que usar o goto informalmente (ou seja, pular onde você quiser) faz da leitura de código um pesadelo. Quando eles nos deram o sistema de exceção, eles temiam que estavam se reinventando. Então eles nos disseram para não "usá-lo para controle de fluxo", esperando que entendêssemos. Infelizmente, muitos de nós não.

Estranhamente, não costumamos abusar de exceções para criar código espaguete, como costumávamos fazer com o goto. O conselho em si parece ter causado mais problemas.

Fundamentalmente, as exceções são sobre rejeitar uma suposição. Quando você solicita que um arquivo seja salvo, assume que o arquivo pode e será salvo. A exceção que você recebe quando não pode pode ser o nome ser ilegal, o HD estar cheio ou o rato roer seu cabo de dados. Você pode lidar com todos esses erros de maneira diferente, pode lidar com todos da mesma maneira ou pode deixar que eles interrompam o sistema. Há um caminho feliz no seu código em que suas suposições devem ser verdadeiras. De uma maneira ou de outra, as exceções o afastam desse caminho feliz. Estritamente falando, sim, isso é uma espécie de "controle de fluxo", mas não é sobre isso que eles estavam avisando. Eles estavam falando sobre bobagens como esta :

insira a descrição da imagem aqui

"Exceções devem ser excepcionais". Essa pequena tautologia nasceu porque os projetistas de sistemas de exceção precisam de tempo para criar rastreamentos de pilha. Comparado a pular, isso é lento. Consome tempo de CPU. Mas se você estiver prestes a registrar e interromper o sistema ou pelo menos interromper o processamento intensivo de tempo atual antes de iniciar o próximo, você terá algum tempo para matar. Se as pessoas começarem a usar exceções "para controle de fluxo", essas suposições sobre o tempo sairão pela janela. Portanto, "Exceções devem ser excepcionais" foi realmente dada a nós como uma consideração de desempenho.

Muito mais importante do que isso não está nos confundindo. Quanto tempo você levou para identificar o loop infinito no código acima?

NÃO retorne códigos de erro.

... é um bom conselho quando você está em uma base de código que normalmente não usa códigos de erro. Por quê? Porque ninguém vai se lembrar de salvar o valor de retorno e verificar seus códigos de erro. Ainda é uma boa convenção quando você está em C.

OneOf

Você está usando outra convenção. Tudo bem, desde que você esteja definindo a convenção e não apenas lutando contra outra. É confuso ter duas convenções de erro na mesma base de código. Se, de alguma forma, você se livrou de todo o código que usa a outra convenção, vá em frente.

Eu também gosto da convenção. Uma das melhores explicações que encontrei aqui * :

insira a descrição da imagem aqui

Mas, por mais que eu goste, ainda não vou misturá-lo com as outras convenções. Escolha um e fique com ele. 1 1

1: Com o que quero dizer, não me faça pensar em mais de uma convenção ao mesmo tempo.


Pensamentos subsequentes:

Nesta discussão, o que estou retirando atualmente é o seguinte:

  1. Se você espera que o chamador imediato capte e lide com a exceção na maioria das vezes e continue seu trabalho, talvez por outro caminho, provavelmente deve fazer parte do tipo de retorno. Opcional ou OneOf pode ser útil aqui.
  2. Se você espera que o chamador imediato não capte a exceção na maioria das vezes, lance uma exceção, para economizar a bobagem de passá-la manualmente para a pilha.
  3. Se você não tiver certeza do que o chamador imediato fará, talvez forneça os dois, como Parse e TryParse.

Realmente não é assim tão simples. Uma das coisas fundamentais que você precisa entender é o que é um zero.

Quantos dias restam em maio? 0 (porque não é maio. Já é junho).

Exceções são uma maneira de rejeitar uma suposição, mas não são a única. Se você usar exceções para rejeitar a suposição, deixa o caminho feliz. Mas se você escolher valores para enviar o caminho feliz que sinaliza que as coisas não são tão simples como foi assumido, você poderá permanecer nesse caminho enquanto puder lidar com esses valores. Às vezes, 0 já é usado para significar algo, então você precisa encontrar outro valor para mapear sua suposição de rejeitar a ideia. Você pode reconhecer essa idéia pelo seu uso em boa e velha álgebra . Mônadas podem ajudar com isso, mas nem sempre precisa ser uma mônada.

Por exemplo 2 :

IList<int> ParseAllTheInts(String s) { ... }

Você pode pensar em alguma boa razão para isso ter que ser planejado para que deliberadamente jogue alguma coisa? Adivinha o que você ganha quando nenhum int pode ser analisado? Eu nem preciso te contar.

Isso é um sinal de um bom nome. Desculpe, mas TryParse não é minha idéia de um bom nome.

Muitas vezes evitamos lançar uma exceção ao receber nada quando a resposta pode ser mais do que uma coisa ao mesmo tempo, mas por algum motivo, se a resposta for uma coisa ou nada, ficamos obcecados em insistir que ela nos dê uma coisa ou jogue:

IList<Point> Intersection(Line a, Line b) { ... }

As linhas paralelas realmente precisam causar uma exceção aqui? É realmente tão ruim se essa lista nunca conter mais de um ponto?

Talvez semanticamente você não consiga aceitar isso. Se assim for, é uma pena. Mas talvez as mônadas, que não têm um tamanho arbitrário como o Listfazem, farão com que você se sinta melhor.

Maybe<Point> Intersection(Line a, Line b) { ... }

As Mônadas são pequenas coleções de finalidades especiais que devem ser usadas de maneiras específicas que evitam a necessidade de testá-las. Devemos encontrar maneiras de lidar com eles, independentemente do que eles contêm. Dessa forma, o caminho feliz permanece simples. Se você abrir e testar todas as mônadas que tocar, estará usando errado.

Eu sei, é estranho. Mas é uma nova ferramenta (bem, para nós). Então, dê um tempo. Os martelos fazem mais sentido quando você para de usá-los nos parafusos.


Se você me quiser, gostaria de abordar este comentário:

Como é que nenhuma das respostas esclarece que a mônada Either não é um código de erro e o OneOf também não é? Eles são fundamentalmente diferentes, e a pergunta, consequentemente, parece se basear em um mal-entendido. (Embora de forma modificada ainda seja uma pergunta válida.) Konrad Rudolph 4 de junho de 18 às 14:08

Isto é absolutamente verdade. As mônadas estão muito mais próximas das coleções do que as exceções, sinalizadores ou códigos de erro. Eles fazem bons recipientes para essas coisas quando usados ​​com sabedoria.

candied_orange
fonte
9
Não seria mais apropriado se a faixa vermelha estivesse abaixo das caixas, não passando por elas?
Flater
4
Logicamente, você está correto. No entanto, cada caixa neste diagrama específico representa uma operação de ligação monádica. bind inclui (talvez crie!) a faixa vermelha. Eu recomendo assistir a apresentação completa. É interessante e Scott é um ótimo orador.
Gusdor
7
Penso exceções mais como um COMEFROM instrução em vez de um GOTO, uma vez que throwna verdade não sabe / dizer onde vamos saltar para;)
Warbo
3
Não há nada errado com as convenções de mixagem, se você pode estabelecer limites, e é disso que se trata o encapsulamento.
Robert Harvey
4
A primeira seção inteira desta resposta é forte como se você parasse de ler a pergunta após a frase citada. A pergunta reconhece que travar o programa é superior a passar para um comportamento indefinido: eles contrastam travamentos em tempo de execução, não com execução contínua e indefinida, mas com erros em tempo de compilação que o impedem de criar o programa sem considerar o erro em potencial e lidar com isso (que pode acabar sendo um travamento se não houver mais nada a ser feito, mas será um travamento porque você deseja que seja, não porque você se esqueceu).
KRyan
35

C # não é Haskell e você deve seguir o consenso de especialistas da comunidade C #. Se você tentar seguir as práticas de Haskell em um projeto C #, alienará todos os outros membros da equipe e, eventualmente, provavelmente descobrirá os motivos pelos quais a comunidade C # faz as coisas de maneira diferente. Um grande motivo é que o C # não suporta convenientemente uniões discriminadas.

Ouvi dizer que exceções como fluxo de controle são consideradas um antipadrão sério,

Esta não é uma verdade universalmente aceita. A escolha em todos os idiomas que suportam exceções é lançar uma exceção (que o chamador é livre para não manipular) ou retornar algum valor composto (que o chamador DEVE manipular).

Propagar condições de erro para cima através da pilha de chamadas requer uma condicional em todos os níveis, dobrando a complexidade ciclomática desses métodos e, consequentemente, dobrando o número de casos de teste de unidade. Em aplicativos de negócios típicos, muitas exceções estão além da recuperação e podem ser propagadas para o nível mais alto (por exemplo, o ponto de entrada do serviço de um aplicativo da web).

Kevin Cline
fonte
9
Propagar mônadas não requer nada.
Basilevs
23
@Basilevs Requer suporte para propagação de mônadas, mantendo o código legível, o que o C # não possui. Você recebe instruções repetidas se retornar ou cadeias de lambdas.
Sebastian Redl
4
As lambdas @SebastianRedl parecem OK em C #.
Basilevs
@Basilevs De uma visão de sintaxe, sim, mas suspeito que, do ponto de vista do desempenho, eles possam ser menos atraentes.
Pharap
5
No C # moderno, você provavelmente pode usar parcialmente as funções locais para recuperar a velocidade. De qualquer forma, a maioria dos softwares comerciais é retardada de forma muito mais significativa pelo IO do que por outros.
Turksarama 4/06
26

Você chegou ao C # em um momento interessante. Até relativamente recentemente, a linguagem permanecia firme no imperativo espaço de programação. Usar exceções para comunicar erros era definitivamente a norma. O uso de códigos de retorno sofreu com a falta de suporte ao idioma (por exemplo, em torno de uniões discriminadas e correspondência de padrões). Muito razoavelmente, a orientação oficial da Microsoft era evitar códigos de retorno e usar exceções.

Sempre houve exceções a isso, na forma de TryXXXmétodos, que retornariam um booleanresultado de sucesso e forneceriam um segundo resultado de valor por meio de um outparâmetro. Eles são muito semelhantes aos padrões "try" na programação funcional, exceto que o resultado é obtido através de um parâmetro out, e não através de um Maybe<T>valor de retorno.

Mas as coisas estão mudando. A programação funcional está se tornando ainda mais popular e linguagens como C # estão respondendo. O C # 7.0 introduziu alguns recursos básicos de correspondência de padrões no idioma; O C # 8 apresentará muito mais, incluindo uma expressão de alternância, padrões recursivos etc. Junto com isso, houve um crescimento de "bibliotecas funcionais" para C #, como minha própria biblioteca Succinc <T> , que também fornece suporte para uniões discriminadas .

Essas duas coisas combinadas significam que códigos como o seguinte estão crescendo lentamente em popularidade.

static Maybe<int> TryParseInt(string source) =>
    int.TryParse(source, out var result) ? new Some<int>(result) : none; 

public int GetNumberFromUser()
{
    Console.WriteLine("Please enter a number");
    while (true)
    {
        var userInput = Console.ReadLine();
        if (TryParseInt(userInput) is int value)
        {
            return value;
        }
        Console.WriteLine("That's not a valid number. Please try again");
    }
}

Ainda é bastante nicho no momento, embora, mesmo nos últimos dois anos, tenha notado uma mudança acentuada da hostilidade geral entre desenvolvedores de C # para esse código, para um interesse crescente em usar essas técnicas.

Ainda não dizemos " NÃO retorne códigos de erro". é antiquado e precisa se aposentar. Mas a marcha em direção a esse objetivo está bem encaminhada. Então faça a sua escolha: fique com as velhas formas de lançar exceções com abandono gay, se é assim que você gosta de fazer as coisas; ou comece a explorar um novo mundo em que convenções de linguagens funcionais estão se tornando mais populares em C #.

David Arno
fonte
22
É por isso que eu odeio C # :) Eles continuam adicionando maneiras de esfolar um gato e, é claro, os velhos modos nunca desaparecem. No final, não há convenções para nada, um programador perfeccionista pode ficar sentado por horas decidindo qual método é "melhor", e um novo programador precisa aprender tudo antes de poder ler o código de outros.
Aleksandr Dubinsky
24
@AleksandrDubinsky, você encontrou um problema central com linguagens de programação em geral, não apenas com C #. Novos idiomas são criados, enxutos, frescos e cheios de idéias modernas. E muito poucas pessoas as usam. Com o tempo, eles ganham usuários e novos recursos, aparafusados ​​a recursos antigos que não podem ser removidos porque quebrariam o código existente. O inchaço cresce. Assim popular criar novas linguagens que são magra, fresco e cheio de ideias modernas ...
David Arno
7
@AleksandrDubinsky, você não odeia quando eles adicionam agora recursos da década de 1960.
Ctrl-alt-delor
6
@AleksandrDubinsky Yeah. Como o Java tentou adicionar uma coisa uma vez (genéricos), eles falharam, agora temos que conviver com a bagunça horrível que resultou para que eles aprendam a lição e parem de adicionar qualquer coisa
:)
3
@ Agent_L: Você sabe que o Java adicionou java.util.Stream(um IEnumerable parecido) e lambdas agora, certo? :)
Chao
5

Eu acho que é perfeitamente razoável usar um DU para retornar erros, mas eu diria isso como escrevi o OneOf :) Mas eu diria assim mesmo, pois é uma prática comum em F # etc (por exemplo, o tipo de resultado, como mencionado por outros )

Não penso nos erros que normalmente retornam como situações excepcionais, mas como normais, mas espero que raramente encontrem situações que podem ou não precisar ser tratadas, mas devem ser modeladas.

Aqui está o exemplo da página do projeto.

public OneOf<User, InvalidName, NameTaken> CreateUser(string username)
{
    if (!IsValid(username)) return new InvalidName();
    var user = _repo.FindByUsername(username);
    if(user != null) return new NameTaken();
    var user = new User(username);
    _repo.Save(user);
    return user;
}

Lançar exceções para esses erros parece um exagero - eles têm uma sobrecarga de desempenho, além das desvantagens de não serem explícitos na assinatura do método ou de fornecer uma correspondência exaustiva.

Até que ponto o OneOf é idiomático em c #, essa é outra questão. A sintaxe é válida e razoavelmente intuitiva. DUs e programação orientada a trilhos são conceitos bem conhecidos.

Eu salvaria exceções para coisas que indicam que algo está quebrado, sempre que possível.

mcintyre321
fonte
O que você está dizendo é que OneOftrata de tornar o valor de retorno mais rico, como retornar um Nullable. Ele substitui exceções para situações que não são excepcionais, para começar.
Aleksandr Dubinsky
Sim - e fornece uma maneira fácil de fazer uma correspondência exaustiva no chamador, o que não é possível usando uma hierarquia de resultados baseada em herança.
precisa
4

OneOfé equivalente a exceções verificadas em Java. Existem algumas diferenças:

  • OneOfnão funciona com métodos que produzem métodos chamados por seus efeitos colaterais (como podem ser chamados significativamente e o resultado simplesmente ignorado). Obviamente, todos nós devemos tentar usar funções puras, mas isso nem sempre é possível.

  • OneOfnão fornece informações sobre onde o problema ocorreu. Isso é bom, pois economiza desempenho (as exceções lançadas em Java são caras devido ao preenchimento do rastreamento de pilha e em C # será o mesmo) e obriga a fornecer informações suficientes no próprio membro de erro OneOf. Também é ruim, pois essas informações podem ser insuficientes e é difícil encontrar a origem do problema.

OneOf e a exceção verificada têm um "recurso" importante em comum:

  • ambos forçam você a lidar com eles em todos os lugares da pilha de chamadas

Isso evita o seu medo

No C #, ignorar exceções não produz um erro de compilação e, se elas não forem detectadas, elas simplesmente explodem e travam o programa.

mas como já foi dito, esse medo é irracional. Basicamente, geralmente há um único local no seu programa, onde você precisa capturar tudo (é provável que você já use uma estrutura). Como você e sua equipe normalmente passam semanas ou anos trabalhando no programa, você não vai esquecer, não é?

A grande vantagem das exceções ignoráveis é que elas podem ser tratadas onde você quiser manipulá-las, e não em qualquer lugar no rastreamento da pilha. Isso elimina toneladas de clichê (basta olhar para algum código Java declarando "arremessos ...." ou quebrando exceções) e também torna seu código menos problemático: como qualquer método pode ser usado, você precisa estar ciente disso. Felizmente, a ação correta geralmente não está fazendo nada , ou seja, deixando-a borbulhar em um local onde possa ser manuseada de maneira razoável.

maaartinus
fonte
+1, mas por que o OneOf não funciona com efeitos colaterais? (Eu acho que é porque passará uma verificação se você não atribuir o valor de retorno, mas como eu não conheço o OneOf, nem muito C # não tenho certeza - o esclarecimento melhoraria sua resposta.)
dcorking
1
Você pode retornar informações de erro com o OneOf, fazendo com que o objeto de erro retornado contenha informações úteis, por exemplo, OneOf<SomeSuccess, SomeErrorInfo>onde SomeErrorInfofornece detalhes do erro.
precisa
@dcorking Editado. Claro, se você ignorou o valor de retorno, não sabe nada sobre o que aconteceu. Se você fizer isso com uma função pura, tudo bem (você acabou de perder tempo).
Maaartinus 5/06
2
@ mcintyre321 Claro, você pode adicionar informações, mas precisa fazê-las manualmente e está sujeita a preguiça e esquecimento. Eu acho que, em casos mais complicados, um rastreamento de pilha é mais útil.
Maaartinus
4

Usar o OneOf em vez de exceções para manipular exceções "comuns" (como Arquivo Não Encontrado etc) é uma abordagem razoável ou é uma prática terrível?

Vale a pena notar que ouvi dizer que as exceções como fluxo de controle são consideradas um antipadrão sério; portanto, se a "exceção" é algo que você normalmente trataria sem encerrar o programa, não é esse "fluxo de controle" de alguma maneira?

Os erros têm várias dimensões para eles. Identifico três dimensões: elas podem ser evitadas, com que frequência ocorrem e se podem ser recuperadas. Enquanto isso, a sinalização de erro tem principalmente uma dimensão: decidir em que medida bater na cabeça do chamador para forçá-lo a lidar com o erro ou deixar o erro "silenciosamente" borbulhar como uma exceção.

Os erros não evitáveis, frequentes e recuperáveis ​​são os que realmente precisam ser tratados no local da chamada. Eles melhor justificam forçar o interlocutor a confrontá-los. Em Java, eu as faço exceções verificadas, e um efeito semelhante é alcançado criando Try*métodos ou retornando uma união discriminada. Uniões discriminadas são especialmente agradáveis ​​quando uma função tem um valor de retorno. Eles são uma versão muito mais refinada do retorno null. A sintaxe dos try/catchblocos não é ótima (muitos colchetes), tornando as alternativas melhores. Além disso, as exceções são um pouco lentas porque registram rastreamentos de pilha.

Os erros evitáveis ​​(que não foram evitados devido a erro / negligência do programador) e os erros não recuperáveis ​​funcionam muito bem como exceções comuns. Erros pouco frequentes também funcionam melhor como exceções comuns, pois um programador pode frequentemente pesar, não vale o esforço de antecipar (depende do objetivo do programa).

Um ponto importante é que geralmente é o site de uso que determina como os modos de erro de um método se encaixam nessas dimensões, que deve ser lembrado ao considerar o seguinte.

Na minha experiência, forçar o chamador a enfrentar condições de erro é bom quando os erros não são evitáveis, frequentes e recuperáveis, mas fica muito irritante quando o chamador é forçado a pular aros quando não precisa ou deseja . Isso pode ocorrer porque o chamador sabe que o erro não ocorrerá ou porque ainda não deseja tornar o código resiliente. Em Java, isso explica a angústia do uso frequente de exceções verificadas e do retorno do Opcional (que é semelhante ao Maybe e foi recentemente introduzido em meio a muita controvérsia). Em caso de dúvida, lance exceções e deixe que o chamador decida como deseja lidar com elas.

Por fim, lembre-se de que a coisa mais importante sobre condições de erro, muito acima de qualquer outra consideração, como a forma como elas são sinalizadas, é documentá-las completamente .

Aleksandr Dubinsky
fonte
2

As outras respostas discutiram exceções versus códigos de erro com detalhes suficientes, então gostaria de adicionar outra perspectiva específica à pergunta:

O OneOf não é um código de erro, é mais como uma mônada

Da maneira como é usado, OneOf<Value, Error1, Error2>é um contêiner que representa o resultado real ou algum estado de erro. É como um Optional, exceto que, quando nenhum valor está presente, ele pode fornecer mais detalhes sobre o motivo.

O principal problema com os códigos de erro é que você se esquece de verificá-los. Mas aqui, você literalmente não pode acessar o resultado sem verificar se há erros.

O único problema é quando você não se importa com o resultado. O exemplo da documentação do OneOf fornece:

public OneOf<User, InvalidName, NameTaken> CreateUser(string username) { ... }

... e eles criam respostas HTTP diferentes para cada resultado, o que é muito melhor do que usar exceções. Mas se você chamar o método assim.

public void CreateUserButtonClick(String username) {
    UserManager.CreateUser(string username)
}

então você não tem um problema e exceções seria a melhor escolha. Compare de Rail savevs save!.

Cefalópode
fonte
1

Uma coisa que você precisa considerar é que o código em C # geralmente não é puro. Em uma base de código cheia de efeitos colaterais e com um compilador que não se importa com o que você faz com um valor de retorno de alguma função, sua abordagem pode causar um sofrimento considerável. Por exemplo, se você possui um método que exclui um arquivo, deseja garantir que o aplicativo observe quando o método falhou, mesmo que não haja motivo para verificar qualquer código de erro "no caminho feliz". Essa é uma diferença considerável das linguagens funcionais que exigem que você ignore explicitamente os valores de retorno que não está usando. Em C #, os valores de retorno sempre são implicitamente ignorados (a única exceção são os getters de propriedade IIRC).

A razão pela qual o MSDN diz para "não retornar códigos de erro" é exatamente isso - ninguém o força a ler o código de erro. O compilador não ajuda em nada. No entanto, se sua função é livre de efeitos colaterais, você pode usar com segurança algo como Either- o ponto é que, mesmo que ignore o resultado do erro (se houver), você só poderá fazer isso se não usar o "resultado adequado" ou. Existem algumas maneiras de fazer isso - por exemplo, você só pode permitir "ler" o valor passando um delegado para lidar com os casos de sucesso e erro, e pode combiná-lo facilmente com abordagens como a Programação Orientada a Ferrovias (isso sim funciona muito bem em c #).

int.TryParseestá em algum lugar no meio. Isso é puro. Ele define quais são os valores dos dois resultados o tempo todo - para que você saiba que, se o valor de retorno for false, o parâmetro de saída será definido como 0. Ele ainda não impede que você use o parâmetro de saída sem verificar o valor de retorno, mas pelo menos você tem a garantia de qual é o resultado, mesmo que a função falhe.

Mas uma coisa absolutamente crucial aqui é consistência . O compilador não vai te salvar se alguém mudar essa função para ter efeitos colaterais. Ou para lançar exceções. Portanto, embora essa abordagem seja perfeitamente adequada em C # (e eu a uso), você precisa garantir que todos na sua equipe entendam e usem . Muitos dos invariantes necessários para que a programação funcional funcione muito bem não são impostos pelo compilador C # e você precisa garantir que eles estejam sendo seguidos por você. Você deve manter-se ciente das coisas que quebram se não seguir as regras que Haskell impõe por padrão - e isso é algo que você deve ter em mente para quaisquer paradigmas funcionais que introduzir no seu código.

Luaan
fonte
0

A instrução correta seria Não use códigos de erro para exceções! E não use exceções para não exceções.

Se algo acontecer que seu código não pode recuperar (ou seja, fazê-lo funcionar), isso é uma exceção. Não há nada que seu código imediato faria com um código de erro, exceto entregá-lo para registrar ou talvez fornecer o código ao usuário de alguma forma. No entanto, é exatamente para isso que são as exceções - elas lidam com toda a entrega e ajudam a analisar o que realmente aconteceu.

Se, no entanto, algo acontecer, como entrada inválida que você pode manipular, reformatando-a automaticamente ou pedindo ao usuário que corrija os dados (e você pensou nesse caso), isso não é realmente uma exceção em primeiro lugar - como você espere isso. Você pode lidar com esses casos com códigos de erro ou qualquer outra arquitetura. Apenas não exceções.

(Observe que eu não tenho muita experiência em C #, mas minha resposta deve ser válida no caso geral, com base no nível conceitual de quais são as exceções.)

Frank Hopkins
fonte
7
Discordo fundamentalmente da premissa desta resposta. Se os designers de idiomas não pretendessem que exceções fossem usadas para erros recuperáveis, a catchpalavra - chave não existiria. E como estamos conversando em um contexto de C #, vale a pena notar que a filosofia adotada aqui não é seguida pela estrutura .NET; talvez o exemplo mais simples possível de "entrada inválida que você possa manipular" seja o usuário digitando um não número em um campo em que um número é esperado ... e int.Parsegera uma exceção.
Mark Amery
@MarkAmery Para int.Parse, não é nada com o qual possa se recuperar nem tudo o que seria de esperar. A cláusula catch é normalmente usada para evitar falhas completas de uma exibição externa, não solicita novas credenciais ao banco de dados ou similares, evita o travamento do programa. É uma falha técnica, não um fluxo de controle de aplicativos.
21418 Frank Frankkins
3
A questão de saber se é melhor para uma função gerar uma exceção quando ocorre uma condição depende se o chamador imediato da função pode lidar com essa condição de maneira útil. Nos casos em que é possível, o lançamento de uma exceção que o chamador imediato deve capturar representa um trabalho extra para o autor do código de chamada e um trabalho extra para a máquina que o processa. Se um chamador não estiver preparado para lidar com uma condição, no entanto, as exceções aliviarão a necessidade de que o chamador o codifique explicitamente.
precisa
@ Darkwing "Você pode lidar com esses casos com códigos de erro ou qualquer outra arquitetura". Sim, você é livre para fazer isso. No entanto, você não recebe nenhum suporte do .NET para fazer isso, pois o .NET CL em si usa exceções em vez de códigos de erro. Portanto, em muitos casos, você não apenas é forçado a capturar as exceções do .NET e retornar seu código de erro correspondente, em vez de deixar a exceção aparecer, mas também está forçando outras pessoas da sua equipe a adotarem outro conceito de tratamento de erros que eles precisam usar paralelamente às exceções, o conceito padrão de tratamento de erros.
Aleksander
-2

É uma "idéia terrível".

É terrível porque, pelo menos em .net, você não tem uma lista exaustiva de possíveis exceções. Qualquer código pode gerar qualquer exceção. Especialmente se você estiver fazendo OOP e puder chamar métodos substituídos em um subtipo, em vez do tipo declarado

Então você teria que mudar todos os métodos e funções para OneOf<Exception, ReturnType>, então, se quisesse lidar com diferentes tipos de exceção, teria que examinar o tipo If(exception is FileNotFoundException)etc

try catch é o equivalente a OneOf<Exception, ReturnType>

Editar ----

Estou ciente de que você propõe retornar, OneOf<ErrorEnum, ReturnType>mas sinto que isso é uma distinção sem diferença.

Você está combinando códigos de retorno, exceções esperadas e ramificação por exceção. Mas o efeito geral é o mesmo.

Ewan
fonte
2
Exceções "normais" também são uma péssima idéia. a pista está no nome
Ewan
4
Não estou propondo retornar Exceptions.
Clinton
você está propondo que a alternativa a um dos é controle de fluxo via exceções
Ewan