Melhor exceção para um argumento de tipo genérico inválido

106

Atualmente estou escrevendo algum código para UnconstrainedMelody que tem métodos genéricos para fazer com enums.

Agora, eu tenho uma classe estática com um monte de métodos que são unicamente destinadas a ser usado com "bandeiras" enums. Eu não posso adicionar isso como uma restrição ... então é possível que eles sejam chamados com outros tipos de enum também. Nesse caso, gostaria de lançar uma exceção, mas não tenho certeza de qual lançar.

Só para tornar isso concreto, se eu tiver algo assim:

// Returns a value with all bits set by any values
public static T GetBitMask<T>() where T : struct, IEnumConstraint
{
    if (!IsFlags<T>()) // This method doesn't throw
    {
        throw new ???
    }
    // Normal work here
}

Qual é a melhor exceção para lançar? ArgumentExceptionparece lógico, mas é um argumento de tipo em vez de um argumento normal, o que poderia facilmente confundir as coisas. Devo apresentar minha própria TypeArgumentExceptionclasse? Use InvalidOperationException? NotSupportedException? Algo mais?

Eu sim não criar meu próprio exceção para isso a menos que seja claramente a coisa certa a fazer.

Jon Skeet
fonte
Me deparei com isso hoje ao escrever um método genérico em que requisitos extras são colocados no tipo que está sendo usado e não podem ser descritos com restrições. Fiquei surpreso ao não encontrar um tipo de exceção já no BCL. Mas esse mesmo dilema foi um que eu também enfrentei alguns dias atrás no mesmo projeto para um genérico que só funcionará com um atributo Flags. Assustador!
Andras Zoltan,

Respostas:

46

NotSupportedException parece que se encaixa perfeitamente, mas a documentação afirma claramente que deve ser usado para uma finalidade diferente. Dos comentários da classe MSDN:

Existem métodos que não são suportados na classe base, com a expectativa de que esses métodos sejam implementados nas classes derivadas. A classe derivada pode implementar apenas um subconjunto dos métodos da classe base e lançar NotSupportedException para os métodos sem suporte.

Claro, há uma maneira que NotSupportedExceptioné obviamente boa o suficiente, especialmente devido ao seu significado de senso comum. Dito isso, não tenho certeza se está certo.

Dado o propósito de Melodia Irrestrita ...

Existem várias coisas úteis que podem ser feitas com métodos / classes genéricos onde há uma restrição de tipo de "T: enum" ou "T: delegate" - mas, infelizmente, eles são proibidos em C #.

Esta biblioteca de utilitários contorna as proibições de usar ildasm / ilasm ...

... parece que um novo Exceptionpode estar em ordem, apesar do alto ônus da prova que, com justiça, temos que enfrentar antes de criar o personalizado Exceptions. Algo assim InvalidTypeParameterExceptionpode ser útil em toda a biblioteca (ou talvez não - este é certamente um caso extremo, certo?).

Os clientes precisarão ser capazes de distinguir isso das exceções BCL? Quando um cliente pode acidentalmente chamar isso usando uma baunilha enum? Como você responderia às perguntas feitas pela resposta aceita para Quais fatores devem ser levados em consideração ao escrever uma classe de exceção personalizada?

Jeff Sternal
fonte
Na verdade, é quase tentador lançar uma exceção apenas interna em primeiro lugar, da mesma forma que o Code Contracts faz ... Não acredito que alguém deva entender isso.
Jon Skeet
Pena que não pode retornar nulo!
Jeff Sternal,
25
Vou com TypeArgumentException.
Jon Skeet
Adicionar exceções à Estrutura pode ter um alto "ônus da prova", mas definir exceções personalizadas não deveria. Coisas comoInvalidOperationException são nojentas, porque "Foo pede a coleção Bar para adicionar algo que já existe, então Bar lança IOE" e "Foo pede coleção Bar para adicionar algo, então Bar chama Boz que lança IOE mesmo que Bar não esteja esperando" ambos irão lançar o mesmo tipo de exceção; o código que espera capturar o primeiro não estará esperando o último. Dito isto ...
supercat
... Eu acho que o argumento a favor de uma exceção do Framework aqui é mais convincente do que para uma exceção personalizada. A natureza geral do NSE é que, quando uma referência a um objeto como um tipo geral, e alguns, mas não todos os tipos específicos de objeto para os quais os pontos de referência suportam uma habilidade, tentando usar a habilidade em um tipo específico que não não suporta deve lançar NSE. Eu consideraria Foo<T>a um "tipo geral" e Foo<Bar>um "tipo específico" naquele contexto, embora não haja nenhuma relação de "herança" entre eles.
supercat
24

Eu evitaria NotSupportedException. Esta exceção é usada na estrutura onde um método não é implementado e há uma propriedade indicando que este tipo de operação não é suportado. Não cabe aqui

Acho que InvalidOperationException é a exceção mais apropriada que você pode lançar aqui.

JaredPar
fonte
Obrigado pelo aviso sobre a NSE. Também gostaria de receber contribuições de seus colegas, aliás ...
Jon Skeet
A questão é que a funcionalidade de que Jon precisa não tem nada semelhante no BCL. O compilador deve pegá-lo. Se você remover o requisito de "propriedade" de NotSupportedException, as coisas que você mencionou (como a coleção ReadOnly) são o que mais se aproxima do problema de Jon.
Mehrdad Afshari
Um ponto - eu tenho um método IsFlags (tem que ser um método para ser genérico) que é uma espécie de indicação de que esse tipo de operação não é suportado ... então, nesse sentido, NSE seria apropriado. ou seja, o chamador pode verificar primeiro.
Jon Skeet,
@Jon: Acho que mesmo que você não tenha essa propriedade, mas todos os membros do seu tipo dependam inerentemente do fato de que Té um enumdecorado com Flags, seria válido lançar o NSE.
Mehrdad Afshari
1
@Jon: StupidClrExceptionfaz um nome divertido;)
Mehrdad Afshari
13

A programação genérica não deve lançar em tempo de execução para parâmetros de tipo inválido. Ele não deve compilar, você deve ter uma aplicação de tempo de compilação. Não sei o que IsFlag<T>()contém, mas talvez você possa transformar isso em uma aplicação de tempo de compilação, como tentar criar um tipo que só é possível criar com 'sinalizadores'. Talvez uma traitsaula possa ajudar.

Atualizar

Se você deve jogar, eu votaria em InvalidOperationException. O raciocínio é que os tipos genéricos têm parâmetros e os erros relacionados aos parâmetros (método) são centralizados na hierarquia ArgumentException. No entanto, a recomendação em ArgumentException afirma que

se a falha não envolver os próprios argumentos, InvalidOperationException deve ser usado.

Há pelo menos um salto de fé nisso, que as recomendações de parâmetros de método também devem ser aplicadas a parâmetros genéricos , mas não há nada melhor no SystemException hierachy imho.

Remus Rusanu
fonte
1
Não, não há como isso ser restringido em tempo de compilação. IsFlag<T>determina se o enum foi [FlagsAttribute]aplicado a ele e o CLR não tem restrições baseadas em atributos. Seria em um mundo perfeito - ou haveria alguma outra maneira de restringi-lo - mas neste caso simplesmente não funciona :(
Jon Skeet
(1 para o princípio geral - adoraria ser capaz de restringi-lo.)
Jon Skeet,
9

Eu usaria NotSupportedException, pois é isso que você está dizendo. Outros enums além dos específicos não são suportados . É claro que isso seria declarado mais claramente na mensagem de exceção.

Robban
fonte
2
NotSupportedException é usado para um propósito muito diferente no BCL. Não cabe aqui. blogs.msdn.com/jaredpar/archive/2008/12/12/…
JaredPar
8

Eu iria com NotSupportedException. Embora ArgumentExceptionpareça bom, é realmente esperado quando um argumento passado para um método é inaceitável. Um argumento de tipo é uma característica definidora do método real que você deseja chamar, não um "argumento" real. InvalidOperationExceptiondeve ser acionado quando a operação que você está realizando pode ser válida em alguns casos, mas para a situação específica, é inaceitável.

NotSupportedExceptioné lançado quando uma operação é inerentemente sem suporte. Por exemplo, ao implementar uma interface onde um membro específico não faz sentido para uma classe. Isso parece uma situação semelhante.

Mehrdad Afshari
fonte
Mmm. Ainda não bastante sentir bem, mas eu acho que vai ser a coisa mais próxima a ele.
Jon Skeet,
Jon: não parece certo porque naturalmente esperamos que seja detectado pelo compilador.
Mehrdad Afshari
Sim. Este é um tipo estranho de restrição que gostaria de aplicar, mas não posso :)
Jon Skeet
6

Aparentemente, a Microsoft usa ArgumentExceptionpara isso, conforme demonstrado no exemplo de Expression.Lambda <> , Enum.ExperimenteParse <> ou Marshal.GetDelegateForFunctionPointer <> na seção Exceções. Não consegui encontrar nenhum exemplo indicando o contrário (apesar de pesquisar na fonte de referência local TDelegatee TEnum).

Portanto, acho que é seguro presumir que, pelo menos no código da Microsoft, é uma prática comum usar ArgumentExceptionpara argumentos de tipo genérico inválidos, além dos de variáveis ​​básicas. Dado que a descrição da exceção nos documentos não faz distinção entre elas, também não é muito forçado.

Esperançosamente, ele decide as questões de uma vez por todas.

Alice
fonte
Um único exemplo no quadro não é suficiente para mim, não - dado o número de lugares onde eu acho que MS tem feito má escolha em outros casos :) eu não iria derivar TypeArgumentExceptionde ArgumentException, simplesmente porque o tipo de argumento não é um regular argumento.
Jon Skeet de
1
Isso é certamente mais atraente em termos de "é o que a MS faz consistentemente". Isso não o torna mais atraente em termos de correspondência com a documentação ... e eu sei que muitas pessoas na equipe C # se preocupam profundamente com a diferença entre argumentos regulares e argumentos de tipo :) Mas obrigado pelos exemplos - eles são muito úteis.
Jon Skeet de
@Jon Skeet: Fez uma edição; agora inclui 3 exemplos de diferentes bibliotecas MS, todos com ArgumentException documentado como aquele lançado; então, se for uma má escolha, pelo menos é uma má escolha consistente. ;) Eu acho que a Microsoft assume que argumentos regulares e argumentos de tipo são ambos argumentos; e pessoalmente, acho que essa suposição é bastante razoável. ^^ '
Alice
Ah, esquece, parece que você já percebeu isso. Que bom que pude ajudar. ^^
Alice
Acho que teremos que concordar em discordar sobre se é razoável tratá-los da mesma forma. Eles certamente não são os mesmos quando se trata de reflexão, ou regras de linguagem, etc ... eles são tratados de forma muito diferente.
Jon Skeet de
3

Ide ir com NotSupportedExpcetion.

Carl Bergquist
fonte
2

Lançar uma exceção feita sob encomenda deve sempre ser feito em qualquer caso em que seja questionável. Uma exceção personalizada sempre funcionará, independentemente das necessidades dos usuários da API. O desenvolvedor pode capturar qualquer tipo de exceção se ele não se importar, mas se o desenvolvedor precisar de tratamento especial, ele será SOL.

Eric Schneider
fonte
Além disso, o desenvolvedor deve documentar todas as exceções lançadas nos comentários XML.
Eric Schneider,
1

Que tal herdar de NotSupportedException. Embora eu concorde com @Mehrdad que faz mais sentido, ouço seu ponto de que não parece se encaixar perfeitamente. Portanto, herde de NotSupportedException e, dessa forma, as pessoas codificando em sua API ainda podem capturar uma NotSupportedException.

BFree
fonte
1

Sempre fico cauteloso ao escrever exceções personalizadas, simplesmente porque elas nem sempre são documentadas de forma clara e causam confusão se não forem nomeadas corretamente.

Nesse caso, eu lançaria uma ArgumentException para a falha de verificação de sinalizadores. É tudo uma questão de preferência. Alguns padrões de codificação que vi vão tão longe a ponto de definir quais tipos de exceções devem ser lançados em cenários como este.

Se o usuário estava tentando passar algo que não era um enum, eu lançaria um InvalidOperationException.

Editar:

Os outros levantam um ponto interessante de que isso não é suportado. Minha única preocupação com uma NotSupportedException é que geralmente essas são as exceções que são lançadas quando a "matéria escura" é introduzida no sistema ou, dito de outra forma, "Este método deve entrar no sistema nesta interface, mas nós vencemos não ligue até a versão 2.4 "

Eu também vi NotSupportedExceptions ser lançada como uma exceção de licenciamento "você está executando a versão gratuita deste software, esta função não é compatível".

Editar 2:

Outro possível:

System.ComponentModel.InvalidEnumArgumentException  

A exceção lançada ao usar argumentos inválidos que são enumeradores.

Peter
fonte
Vou restringi-lo para ser um enum (depois de alguns truques) - estou preocupado apenas com as bandeiras.
Jon Skeet,
Acho que os caras do licenciamento deveriam lançar uma instância de uma LicensingExceptionclasse que herda de InvalidOperationException.
Mehrdad Afshari
Eu concordo, Mehrdad, as exceções são, infelizmente, uma daquelas áreas em que há muito cinza na estrutura. Mas tenho certeza de que é o mesmo para muitos idiomas. (não estou dizendo que eu voltaria para o erro de tempo de execução do vb6 13 hehe)
Peter