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 Either
Haskell.
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 OneOf
vez 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 OneOf
para 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:
- Se você espera que o chamador imediato
catch
lide 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.Optional
ouOneOf
pode ser útil aqui. - 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.
- Se você não tiver certeza do que o chamador imediato fará, talvez forneça os dois, como
Parse
eTryParse
.
fonte
Result
;-)FileNotFoundException
.Either
mô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.)Respostas:
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.
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
, econtinue
quando 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 :
"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?
... é 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.
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 * :
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.
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 :
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:
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
List
fazem, farão com que você se sinta melhor.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:
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.
fonte
throw
na verdade não sabe / dizer onde vamos saltar para;)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.
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).
fonte
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
TryXXX
métodos, que retornariam umboolean
resultado de sucesso e forneceriam um segundo resultado de valor por meio de umout
parâ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 umMaybe<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.
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 #.
fonte
java.util.Stream
(um IEnumerable parecido) e lambdas agora, certo? :)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.
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.
fonte
OneOf
trata 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.OneOf
é equivalente a exceções verificadas em Java. Existem algumas diferenças:OneOf
nã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.OneOf
nã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 erroOneOf
. 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:Isso evita o seu medo
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.
fonte
OneOf<SomeSuccess, SomeErrorInfo>
ondeSomeErrorInfo
fornece detalhes do erro.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 retornonull
. A sintaxe dostry/catch
blocos 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 .
fonte
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 umOptional
, 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:
... 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.
então você não tem um problema e exceções seria a melhor escolha. Compare de Rail
save
vssave!
.fonte
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.TryParse
está 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 forfalse
, o parâmetro de saída será definido como0
. 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.
fonte
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.)
fonte
catch
palavra - 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 ... eint.Parse
gera uma exceção.É 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 tipoIf(exception is FileNotFoundException)
etctry catch
é o equivalente aOneOf<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.
fonte
Exception
s.