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 um NullReferenceException
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 retornar null
valores para jogar KeyNotFoundException
entre o .NET 1.1 e o .NET 2.0 (somente se você estiver disposto a considerar o Hashtable.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 a KeyNotFoundException
, mas o design da exceção ainda é superior aqui. Por quê?
- Os parâmetros ref / out incorrem em seus custos todas as vezes, não apenas no caso de "falha"
- Qualquer pessoa que se preocupe pode ligar
Contains
antes de qualquer chamada para o indexador, e esse padrão é totalmente natural. Se um desenvolvedor quiser, mas se esquecer de ligar Contains
, nenhum problema de desempenho poderá surgir; KeyNotFoundException
é alto o suficiente para ser notado e corrigido.