Como seus exemplos já mostram, cada caso deve ser avaliado separadamente e existe um espectro considerável de cinza entre "circunstâncias excepcionais" e "controle de fluxo", especialmente se o método se destina a ser reutilizável e pode ser usado em padrões bastante diferentes do que foi originalmente projetado para. Não espere que todos nós aqui concordemos com o que "não-excepcional" significa, especialmente se você discutir imediatamente a possibilidade de usar "exceções" para implementar isso.
Também podemos não concordar com o que o design torna o código mais fácil de ler e manter, mas vou supor que o designer da biblioteca tenha uma visão pessoal clara disso e só precise equilibrá-lo com as outras considerações envolvidas.
Resposta curta
Siga as suas intuições, exceto quando estiver projetando métodos 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, como deseja em ambas as direções; Isso faz com que as duas abordagens de design sejam quase equivalentes, exceto pelo desempenho, pela facilidade com o depurador e por 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 lenta que um retorno regular (na realidade, há uma variação significativa nisso).
-
Como outra regra prática, lançar uma exceção pode muitas vezes permitir um código muito mais limpo em comparação com os valores mágicos mais cruéis, porque você não está confiando no programador para traduzir o código de erro em outro código de erro múltiplas camadas de código do cliente em um ponto onde há 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 devido a sua tendência de se traduzir automaticamente para um NullReferenceException
no caso de alguns, mas nem todos os tipos de defeitos; geralmente, mas nem sempre, muito próximos a fonte do defeito.)
Então, qual é a lição?
Para uma função que é chamada apenas algumas vezes durante o tempo de vida de um aplicativo (como a inicialização do aplicativo), use qualquer coisa que ofereça 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 código mais limpo. Em seguida, faça algum perfil (se necessário) e altere as exceções para os códigos de retorno se 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 tem que passar por uma rede de ida e volta ou analisar um arquivo XML em disco, a sobrecarga de lançar uma exceção provavelmente é insignificante. É mais importante não perder detalhes de qualquer falha, nem mesmo acidentalmente, do que retornar de uma "falha não excepcional" extra rápido.
Uma função enxuta reutilizável requer mais reflexão. Ao empregar exceções, você está forçando algo como uma diminuição de 100 vezes em chamadores que verão a exceção na metade de suas (muitas) chamadas, se o corpo da função for executado muito rapidamente. Exceções ainda são uma opção de design, mas você terá que fornecer uma alternativa de baixa sobrecarga para os chamadores que não podem pagar isso. Vamos ver um exemplo.
Você lista um ótimo exemplo de Dictionary<,>.Item
, que, falando livremente, mudou de retornar null
valores para % KeyNotFoundException
entre o .NET 1.1 e o .NET 2.0 (somente se você estiver disposto a considerar Hashtable.Item
como ser seu precursor prático não genérico). A razão desta "mudança" não é sem interesse aqui. A otimização de desempenho de tipos de valor (sem mais boxe) fez com que o valor mágico original ( null
) fosse uma não opção; Os parâmetros out
apenas devolveriam uma pequena parte do custo de desempenho. Esta última consideração de desempenho é completamente insignificante em comparação com a sobrecarga de lançar um KeyNotFoundException
, mas o design de exceção ainda é superior aqui. Por quê?
Os parâmetros de - ref / out incorrem em seus custos sempre, não apenas no caso de "falha"
- Qualquer pessoa que se importe pode chamar
Contains
antes de qualquer chamada para o indexador, e esse padrão é lido completamente naturalmente. Se um desenvolvedor quiser, mas esquecer de chamar Contains
, nenhum problema de desempenho poderá ocorrer; KeyNotFoundException
é alto o suficiente para ser notado e corrigido.