Às vezes, acabo tendo que escrever um método ou propriedade para uma biblioteca de classes para a qual não é excepcional não ter uma resposta real, mas uma falha. Algo não pode ser determinado, não está disponível, não foi encontrado, não é possível no momento ou não há mais dados disponíveis.
Eu acho que existem três soluções possíveis para uma situação relativamente não excepcional que indique falha no C # 4:
- retornar um valor mágico que não tem significado de outra forma (como
null
e-1
); - lançar uma exceção (por exemplo
KeyNotFoundException
); - return
false
e forneça o valor de retorno real em umout
parâmetro (comoDictionary<,>.TryGetValue
).
Portanto, as perguntas são: em que situação não excepcional devo lançar uma exceção? E se eu não deveria jogar: quando está retornando um valor mágico, perferido acima da implementação de um Try*
método com um out
parâmetro ? (Para mim, o out
parâmetro parece sujo e é mais trabalhoso usá-lo adequadamente.)
Estou procurando respostas factuais, como respostas que envolvam diretrizes de design (não conheço nenhum Try*
método), usabilidade (como solicito uma biblioteca de classes), consistência com a BCL e legibilidade.
Na biblioteca de classes base do .NET Framework, todos os três métodos são usados:
- retorne um valor mágico que não tem significado de outra forma:
Collection<T>.IndexOf
retorna -1,StreamReader.Read
retorna -1,Math.Sqrt
retorna NaN,Hashtable.Item
retorna nulo;
- lançar uma exceção:
Dictionary<,>.Item
lança KeyNotFoundException,Double.Parse
lança FormatException; ou
- return
false
e forneça o valor de retorno real em umout
parâmetro:
Observe que, como Hashtable
foi criado no momento em que não havia genéricos em C #, ele usa object
e pode, portanto, retornar null
como um valor mágico. Mas com os genéricos, as exceções são usadas Dictionary<,>
e, inicialmente, não existiam TryGetValue
. Aparentemente, as idéias mudam.
Obviamente, o Item
- TryGetValue
e Parse
- TryParse
dualidade está lá por uma razão, assim que eu supor que lançar exceções para falhas não-excepcionais está em C # 4 não feito . No entanto, os Try*
métodos nem sempre existiam, mesmo quando Dictionary<,>.Item
existiam.
fonte
Respostas:
Não acho que seus exemplos sejam realmente equivalentes. Existem três grupos distintos, cada um com sua própria justificativa para seu comportamento.
StreamReader.Read
ou quando existe um valor simples de usar que nunca será uma resposta válida (-1 paraIndexOf
).Os exemplos que você fornece são perfeitamente claros para os casos 2 e 3. Para os valores mágicos, pode-se argumentar se essa é uma boa decisão de design ou não em todos os casos.
O
NaN
retornado porMath.Sqrt
é um caso especial - segue o padrão de ponto flutuante.fonte
Either<TLeft, TRight>
mônada?Você está tentando comunicar ao usuário da API o que ele precisa fazer. Se você lançar uma exceção, não há nada que os force a capturá-la, e somente a leitura da documentação permitirá que eles saibam quais são todas as possibilidades. Pessoalmente, acho lento e tedioso cavar a documentação para encontrar todas as exceções que um determinado método pode lançar (mesmo que seja no sentido sensato, ainda preciso copiá-las manualmente).
Um valor mágico ainda requer que você leia a documentação e, possivelmente, faça referência a alguma
const
tabela para decodificar o valor. Pelo menos, não possui a sobrecarga de uma exceção para o que você chama de ocorrência não excepcional.É por isso que, embora os
out
parâmetros às vezes sejam mal vistos, eu prefiro esse método, com aTry...
sintaxe. É canônico .NET e sintaxe C #. Você está comunicando ao usuário da API que ele precisa verificar o valor de retorno antes de usar o resultado. Você também pode incluir um segundoout
parâmetro com uma mensagem de erro útil, que novamente ajuda na depuração. É por isso que voto na soluçãoTry...
without
parameter.Outra opção é retornar um objeto "resultado" especial, embora eu ache isso muito mais entediante:
Então sua função parece correta, pois possui apenas parâmetros de entrada e retorna apenas uma coisa. É que a única coisa que retorna é uma tupla.
De fato, se você gosta desse tipo de coisa, pode usar as novas
Tuple<>
classes que foram introduzidas no .NET 4. Pessoalmente, não gosto do fato de que o significado de cada campo é menos explícito porque não posso darItem1
eItem2
nomes úteis.fonte
IMyResult
causa é possível comunicar um resultado mais complexo do que apenastrue
oufalse
ou o valor do resultado.Try*()
só é útil para coisas simples, como conversão de string para int.Como seus exemplos já mostram, cada um desses casos deve ser avaliado separadamente e há um espectro considerável de cinza entre "circunstâncias excepcionais" e "controle de fluxo", especialmente se o seu método for reutilizável e pode ser usado em padrões bastante diferentes do que foi originalmente projetado. Não espere que todos aqui concordemos com o que "não excepcional" significa, especialmente se você discutir imediatamente a possibilidade de usar "exceções" para implementá-lo.
Também podemos não concordar com o design que torna o código mais fácil de ler e manter, mas assumirei que o designer da biblioteca tem uma visão pessoal clara disso e só precisa equilibrá-lo com as outras considerações envolvidas.
Resposta curta
Siga seus sentimentos, exceto quando estiver criando métodos bem rápidos e espere uma possibilidade de reutilização imprevista.
Resposta longa
Cada futuro chamador pode traduzir livremente entre códigos de erro e exceções, conforme desejar nas duas direções; isso torna as duas abordagens de design quase equivalentes, exceto para desempenho, facilidade de depuração e alguns contextos restritos de interoperabilidade. Isso geralmente se resume ao desempenho, então vamos nos concentrar nisso.
Como regra geral, espere que lançar uma exceção seja 200x mais lento que um retorno regular (na realidade, há uma variação significativa nisso).
Como outra regra prática, lançar uma exceção geralmente pode permitir um código muito mais limpo em comparação com os valores mágicos mais brutos, porque você não depende do programador converter o código de erro em outro código de erro, pois ele percorre várias camadas do código do cliente em direção a um ponto em que existe contexto suficiente para lidar com isso de maneira consistente e adequada. (Caso especial:
null
tende a se sair melhor aqui do que outros valores mágicos por causa de sua tendência a se traduzir automaticamente em umNullReferenceException
caso de alguns, mas não todos os tipos de defeitos; geralmente, mas nem sempre, muito perto da fonte do defeito. )Então, qual é a lição?
Para uma função chamada apenas algumas vezes durante a vida útil de um aplicativo (como a inicialização do aplicativo), use qualquer coisa que ofereça um código mais limpo e fácil de entender. O desempenho não pode ser uma preocupação.
Para uma função descartável, use qualquer coisa que forneça um código mais limpo. Em seguida, faça alguns perfis (se necessário) e altere as exceções nos códigos de retorno se eles estiverem entre os principais gargalos suspeitos com base nas medições ou na estrutura geral do programa.
Para uma função reutilizável cara, use qualquer coisa que forneça um código mais limpo. Se você basicamente sempre precisa passar por uma ida e volta da rede ou analisar um arquivo XML em disco, a sobrecarga de gerar uma exceção provavelmente é insignificante. É mais importante não perder detalhes de nenhuma falha, nem mesmo acidentalmente, do que retornar de uma "falha não excepcional" extremamente rápido.
Uma função lean reutilizável requer mais reflexão. Ao empregar exceções, você está forçando algo como um abrandamento de 100 vezes nos chamadores que verão a exceção em metade de suas (muitas) chamadas, se o corpo da função executar muito rapidamente. As exceções ainda são uma opção de design, mas você precisará fornecer uma alternativa de baixo custo para os chamadores que não podem pagar por isso. Vejamos um exemplo.
Você lista um ótimo exemplo de
Dictionary<,>.Item
que, vagamente, mudou de retornarnull
valores para jogarKeyNotFoundException
entre o .NET 1.1 e o .NET 2.0 (somente se você estiver disposto a considerar oHashtable.Item
seu precursor não genérico prático). A razão dessa "mudança" não deixa de interessar aqui. A otimização do desempenho dos tipos de valor (sem mais boxe) transformou o valor mágico original (null
) em uma não opção;out
parâmetros trariam apenas uma pequena parte do custo de desempenho. Essa última consideração de desempenho é completamente insignificante em comparação com a sobrecarga de lançar aKeyNotFoundException
, mas o design da exceção ainda é superior aqui. Por quê?Contains
antes de qualquer chamada para o indexador, e esse padrão é totalmente natural. Se um desenvolvedor quiser, mas se esquecer de ligarContains
, nenhum problema de desempenho poderá surgir;KeyNotFoundException
é alto o suficiente para ser notado e corrigido.fonte
Contains
antes de uma chamada para um indexador pode causar uma condição de corrida que não precisa estar presenteTryGetValue
.ConcurrentDictionary
para acesso multithread eDictionary
para acesso thread único para desempenho ideal. Ou seja, não usar `` Contém` NÃO torna o encadeamento de código seguro com essaDictionary
classe específica .Você não deve permitir falhas.
Eu sei, é ondulado e idealista, mas me ouça. Ao fazer o design, há vários casos em que você tem a oportunidade de favorecer uma versão que não possui modos de falha. Em vez de ter um 'FindAll' que falha, o LINQ usa uma cláusula where que simplesmente retorna um enumerável vazio. Em vez de ter um objeto que precisa ser inicializado antes de ser usado, deixe o construtor inicializar o objeto (ou inicializar quando o não inicializado for detectado). A chave é remover a ramificação de falha no código do consumidor. Esse é o problema, então concentre-se nele.
Outra estratégia para isso é o
KeyNotFound
sscenario. Em quase todas as bases de código em que trabalhei desde o 3.0, existe algo como este método de extensão:Não existe um modo de falha real para isso.
ConcurrentDictionary
possui um similarGetOrAdd
incorporado.Tudo isso dito, sempre haverá momentos em que isso é inevitável. Todos os três têm o seu lugar, mas eu preferiria a primeira opção. Apesar de tudo o que é feito com o perigo de nulo, é bem conhecido e se encaixa em muitos dos cenários 'item não encontrado' ou 'resultado não aplicável' que compõem o conjunto "falha não excepcional". Especialmente quando você cria tipos de valor anuláveis, o significado de 'isso pode falhar' é muito explícito no código e difícil de esquecer / estragar.
A segunda opção é boa o suficiente quando o usuário faz algo estúpido. Dá a você uma string com o formato errado, tenta definir a data para 42 de dezembro ... algo que você deseja que as coisas explodam de maneira rápida e espetacular durante o teste, para que o código incorreto seja identificado e corrigido.
A última opção é uma que eu não gosto cada vez mais. Os parâmetros de saída são estranhos e tendem a violar algumas das práticas recomendadas ao criar métodos, como focar em uma coisa e não ter efeitos colaterais. Além disso, o parâmetro out geralmente é significativo apenas durante o sucesso. Dito isto, eles são essenciais para certas operações que geralmente são limitadas por preocupações de concorrência ou por considerações de desempenho (onde você não deseja fazer uma segunda viagem ao DB, por exemplo).
Se o valor de retorno e o parâmetro out não forem triviais, a sugestão de Scott Whitlock sobre um objeto de resultado é a preferida (como a
Match
classe Regex ).fonte
out
parâmetros são completamente ortogonais à questão dos efeitos colaterais. Modificar umref
parâmetro é um efeito colateral, e modificar o estado de um objeto que você passa através de um parâmetro de entrada é um efeito colateral, mas umout
parâmetro é apenas uma maneira incômoda de fazer uma função retornar mais de um valor. Não há efeito colateral, apenas vários valores de retorno.Sempre prefira lançar uma exceção. Ele possui uma interface uniforme entre todas as funções que podem falhar e indica falhas o mais ruidosamente possível - uma propriedade muito desejável.
Observe que
Parse
eTryParse
não são realmente a mesma coisa além dos modos de falha. O fato de queTryParse
também pode retornar o valor é um tanto ortogonal, na verdade. Considere a situação em que, por exemplo, você está validando alguma entrada. Na verdade, você não se importa com o valor, desde que seja válido. E não há nada de errado em oferecer um tipo deIsValidFor(arguments)
função. Mas nunca pode ser o principal modo de operação.fonte
Como outros observaram, o valor mágico (incluindo um valor de retorno booleano) não é uma solução tão boa, exceto como um marcador de "fim de intervalo". Razão: A semântica não é explícita, mesmo se você examinar os métodos do objeto. Você precisa realmente ler a documentação completa de todo o objeto até "oh sim, se retornar -42 significa bla bla bla".
Esta solução pode ser usada por razões históricas ou por desempenho, mas, caso contrário, deve ser evitada.
Isso deixa dois casos gerais: sondagem ou exceções.
Aqui, a regra geral é que o programa não deve reagir a exceções, exceto para lidar com quando o programa / involuntariamente / viola alguma condição. A sondagem deve ser usada para garantir que isso não aconteça. Portanto, uma exceção significa que a investigação relevante não foi realizada com antecedência ou que algo totalmente inesperado aconteceu.
Exemplo:
Você deseja criar um arquivo a partir de um determinado caminho.
Você deve usar o objeto File para avaliar antecipadamente se esse caminho é legal para criação ou gravação de arquivo.
Se, de alguma forma, o seu programa ainda tentar gravar em um caminho ilegal ou não gravável, você deverá obter uma exceção. Isso pode acontecer devido a uma condição de corrida (algum outro usuário removeu o diretório ou tornou-o somente leitura, depois que você tiver problemas)
A tarefa de lidar com uma falha inesperada (sinalizada por uma exceção) e verificar se as condições são adequadas para a operação com antecedência (sondagem) geralmente será estruturada de maneira diferente e, portanto, deve usar mecanismos diferentes.
fonte
Eu acho que o
Try
padrão é a melhor escolha, quando o código apenas indica o que aconteceu. Eu odeio param e como objeto anulável. Eu criei a seguinte turmapara que eu possa escrever o seguinte código
Parece anulável, mas não apenas para o tipo de valor
Outro exemplo
fonte
try
catch
causará estragos no seu desempenho se você estiver ligandoGetXElement()
e falhando várias vezes.public struct Nullable<T> where T : struct
a principal diferença em uma restrição. btw a versão mais recente está aqui github.com/Nelibur/Nelibur/blob/master/Source/Nelibur.Sword/…Se você está interessado na rota do "valor mágico", outra maneira de resolver isso é sobrecarregar o objetivo da classe Lazy. Embora o Lazy tenha a intenção de adiar a instanciação, nada realmente o impede de usar como um Talvez ou uma Opção. Por exemplo:
fonte