Práticas recomendadas: lançando exceções de propriedades

111

Quando é apropriado lançar uma exceção de dentro de um getter ou setter de propriedade? Quando não é apropriado? Por quê? Links para documentos externos sobre o assunto seriam úteis ... O Google descobriu surpreendentemente poucos.

Jon Seigel
fonte
Também relacionado: stackoverflow.com/questions/1488322/…
Brian Rasmussen
1
Eu li ambas as perguntas, mas nenhuma respondeu a esta pergunta completamente IMO.
Jon Seigel
Quando necessário. As perguntas e respostas anteriores mostram que é permitido e apreciado levantar exceções de getter ou setter, então você pode simplesmente "ser inteligente".
Lex Li

Respostas:

135

A Microsoft tem suas recomendações sobre como projetar propriedades em http://msdn.microsoft.com/en-us/library/ms229006.aspx

Essencialmente, eles recomendam que os getters de propriedade sejam acessadores leves que sempre são seguros para chamadas. Eles recomendam redesenhar getters para serem métodos se as exceções forem algo que você precisa lançar. Para setters, eles indicam que as exceções são uma estratégia de tratamento de erros apropriada e aceitável.

Para indexadores, a Microsoft indica que é aceitável para getters e setters lançar exceções. E, de fato, muitos indexadores na biblioteca .NET fazem isso. A exceção mais comum é ArgumentOutOfRangeException.

Existem alguns bons motivos pelos quais você não deseja lançar exceções em getters de propriedade:

  • Como as propriedades "parecem" ser campos, nem sempre é aparente que elas podem lançar uma exceção (por design); enquanto com métodos, os programadores são treinados para esperar e investigar se as exceções são uma consequência esperada da invocação do método.
  • Getters são usados ​​por uma grande quantidade de infraestrutura .NET, como serializadores e databinding (em WinForms e WPF, por exemplo) - lidar com exceções em tais contextos pode rapidamente se tornar problemático.
  • Os getters de propriedade são avaliados automaticamente por depuradores quando você observa ou inspeciona um objeto. Uma exceção aqui pode ser confusa e retardar seus esforços de depuração. Também é indesejável realizar outras operações caras em propriedades (como acessar um banco de dados) pelos mesmos motivos.
  • As propriedades são freqüentemente usadas em uma convenção de encadeamento: obj.PropA.AnotherProp.YetAnother- com esse tipo de sintaxe, torna-se problemático decidir onde injetar instruções catch de exceção.

Como observação, deve-se estar ciente de que só porque uma propriedade não foi projetada para lançar uma exceção, isso não significa que não o fará; poderia facilmente chamar um código que o faz. Até o simples ato de alocar um novo objeto (como uma string) pode resultar em exceções. Você deve sempre escrever seu código defensivamente e esperar exceções de qualquer coisa que invocar.

LBushkin
fonte
41
Se você estiver encontrando uma exceção fatal como "falta de memória", não importa se você obtém a exceção em uma propriedade ou em outro lugar. Se você não conseguiu isso na propriedade, você só conseguiu alguns nanossegundos depois da próxima coisa que alocar memória. A questão não é "uma propriedade pode lançar uma exceção?" Quase todo código pode lançar uma exceção devido a uma condição fatal. A questão é se uma propriedade deve, por definição, lançar uma exceção como parte de seu contrato específico.
Eric Lippert,
1
Não tenho certeza se entendi o argumento desta resposta. Por exemplo, com relação à vinculação de dados - ambos WinForms e WPF são escritos especificamente para lidar adequadamente com exceções lançadas por propriedades e tratá-las como falhas de validação - o que é uma maneira perfeitamente correta (alguns até acreditam que seja a melhor) maneira de fornecer validação de modelo de domínio .
Pavel Minaev
6
@Pavel - embora ambos WinForms e WPF possam se recuperar de exceções em acessadores de propriedade, nem sempre é fácil identificar e se recuperar de tais erros. Em certos casos, (como quando no WPF um configurador de modelo de controle lança uma exceção), a exceção é silenciosamente engolida. Isso pode levar a sessões de depuração dolorosas se você nunca se deparou com esses casos antes.
LBushkin,
1
@Steven: Então, quanto uso essa classe foi para você no caso excepcional? Se você teve que escrever um código defensivo para lidar com todas essas exceções por causa de uma falha e, presumivelmente, fornecer padrões adequados, por que não fornecer esses padrões em sua captura? Alternativamente, se as exceções de propriedade são lançadas para o usuário, por que não apenas lançar a "InvalidArgumentException" original ou semelhante para que eles possam fornecer o arquivo de configurações ausente?
Zhaph - Ben Duguid
6
Há uma razão pela qual essas são diretrizes e não regras; nenhuma diretriz cobre todos os casos extremos malucos. Eu provavelmente teria feito esses métodos e não propriedades sozinho, mas é um julgamento.
Eric Lippert
34

Não há nada de errado em lançar exceções de setters. Afinal, qual a melhor forma de indicar que o valor não é válido para uma determinada propriedade?

Para getters, é geralmente desaprovado e isso pode ser explicado com bastante facilidade: um getter de propriedade, em geral, relata o estado atual de um objeto; portanto, o único caso em que é razoável para um getter lançar é quando o estado é inválido. Mas também é geralmente considerado uma boa ideia projetar suas classes de forma que simplesmente não seja possível obter um objeto inválido inicialmente, ou colocá-lo em um estado inválido por meios normais (ou seja, sempre garantir a inicialização completa nos construtores, e tente tornar os métodos seguros para exceções no que diz respeito à validade do estado e invariantes de classe). Contanto que você siga essa regra, os getters de propriedade nunca devem entrar em uma situação em que tenham que relatar um estado inválido e, portanto, nunca jogue.

Há uma exceção que eu conheço, e na verdade é bastante importante: qualquer implementação de objeto IDisposable. Disposedestina-se especificamente a ser uma forma de colocar o objeto em um estado inválido e há até mesmo uma classe de exceção especial,, ObjectDisposedExceptionpara ser usada nesse caso. É perfeitamente normal lançar ObjectDisposedExceptionde qualquer membro da classe, incluindo getters de propriedade (e excluindo a Disposesi mesmo), após o objeto ter sido descartado.

Pavel Minaev
fonte
4
Obrigado Pavel. Essa resposta vai para 'por que' em vez de simplesmente afirmar novamente que não é uma boa ideia lançar uma exceção das propriedades.
SolutionYogi
1
Não gosto da noção de que absolutamente todos os membros de um IDisposabledevem se tornar inúteis após um Dispose. Se invocar um membro exigiria o uso de um recurso que Disposese tornou indisponível (por exemplo, o membro leria dados de um fluxo que foi fechado), o membro deve lançar ObjectDisposedExceptionao invés de vazar ArgumentException, por exemplo , mas se um tiver um formulário com propriedades que representam o valores em certos campos, seria muito mais útil permitir que tais propriedades sejam lidas após o descarte (produzindo os últimos valores digitados) do que exigir ...
supercat
1
... que Disposeseja adiado até depois que todas essas propriedades forem lidas. Em alguns casos em que um thread pode usar o bloqueio de leituras em um objeto enquanto outro o fecha, e onde os dados podem chegar a qualquer momento anterior Dispose, pode ser útil Disposecortar os dados recebidos, mas permitir que os dados recebidos anteriormente sejam lidos. Não se deve forçar uma distinção artificial entre Closee Disposeem situações em que, de outra forma, nada precisaria existir.
supercat
Entender o motivo da regra permite que você saiba quando infringir a regra (Raymond Chen). Nesse caso, podemos ver que se houver um erro irrecuperável de qualquer tipo, você não deve ocultá-lo no getter, pois nesses casos o aplicativo precisa ser encerrado o mais rápido possível.
Ben
O que eu estava tentando mostrar é que os getters de propriedade geralmente não devem conter lógica que permitiria erros irrecuperáveis. Em caso afirmativo, pode ser que seja melhor como Get...método. Uma exceção aqui é quando você precisa implementar uma interface existente que requer que você forneça uma propriedade.
Pavel Minaev
24

Quase nunca é apropriado em um getter e às vezes apropriado em um setter.

O melhor recurso para esse tipo de pergunta é "Framework Design Guidelines", de Cwalina e Abrams; está disponível como um livro encadernado e grandes partes dele também estão disponíveis online.

Da seção 5.2: Projeto de propriedade

EVITE lançar exceções de getters de propriedade. Os getters de propriedade devem ser operações simples e não devem ter pré-condições. Se um getter pode lançar uma exceção, provavelmente deve ser reprojetado para ser um método. Observe que esta regra não se aplica a indexadores, onde esperamos exceções como resultado da validação dos argumentos.

Observe que esta diretriz se aplica apenas a getters de propriedade. É normal lançar uma exceção em um configurador de propriedade.

Eric Lippert
fonte
2
Embora (em geral) eu concorde com essas diretrizes, acho útil fornecer alguns insights adicionais sobre por que devem ser seguidas - e que tipo de consequências podem surgir quando são ignoradas.
LBushkin,
3
Como isso se relaciona com objetos descartáveis ​​e a orientação que você deve considerar jogar ObjectDisposedExceptionuma vez que o objeto foi Dispose()chamado e algo subsequentemente pede um valor de propriedade? Parece que a orientação deve ser "evite lançar exceções de getters de propriedade, a menos que o objeto tenha sido descartado, caso em que você deve considerar lançar um ObjectDisposedExcpetion".
Scott Dorman
4
O design é a arte e a ciência de encontrar compromissos razoáveis ​​diante de requisitos conflitantes. De qualquer maneira, parece um acordo razoável; Eu não ficaria surpreso se um objeto descartado fosse jogado em uma propriedade; nem eu ficaria surpreso se não o fizesse. Visto que usar um objeto descartado é uma prática de programação terrível, não seria sensato ter expectativas.
Eric Lippert,
1
Outro cenário em que é totalmente válido lançar exceções de dentro de getters é quando um objeto está fazendo uso de invariantes de classe para validar seu estado interno, que precisa ser verificado sempre que um acesso público é feito, independentemente de ser um método ou uma propriedade
Trap
2

Uma boa abordagem para as exceções é usá-las para documentar o código para você e outros desenvolvedores da seguinte maneira:

As exceções devem ser para estados de programa excepcionais. Isso significa que você pode escrevê-los onde quiser!

Um motivo pelo qual você pode querer colocá-los em getters é documentar a API de uma classe - se o software lançar uma exceção assim que um programador tentar usá-lo errado, ele não usará errado! Por exemplo, se você tiver validação durante um processo de leitura de dados, pode não fazer sentido poder continuar e acessar os resultados do processo se houver erros fatais nos dados. Nesse caso, você pode querer fazer a obtenção da saída lançar se houver erros para garantir que outro programador verifique essa condição.

Eles são uma forma de documentar as premissas e limites de um subsistema / método / qualquer coisa. No caso geral, eles não devem ser capturados! Isso também ocorre porque eles nunca são lançados se o sistema estiver trabalhando em conjunto da maneira esperada: se uma exceção acontecer, isso mostra que as suposições de um trecho de código não são atendidas - por exemplo, ele não está interagindo com o mundo ao seu redor da maneira foi originalmente planejado para. Se você detectar uma exceção que foi escrita para este propósito, provavelmente significa que o sistema entrou em um estado imprevisível / inconsistente - isso pode levar a uma falha ou corrupção de dados ou algo semelhante, o que provavelmente será muito mais difícil de detectar / depurar.

Mensagens de exceção são uma forma muito grosseira de relatar erros - elas não podem ser coletadas em massa e contêm apenas uma string. Isso os torna inadequados para relatar problemas nos dados de entrada. Na execução normal, o próprio sistema não deve entrar em estado de erro. Como resultado, as mensagens neles devem ser projetadas para programadores e não para usuários - coisas que estão erradas nos dados de entrada podem ser descobertas e retransmitidas para os usuários em formatos mais adequados (personalizados).

A exceção (haha!) Para esta regra são coisas como IO, onde as exceções não estão sob seu controle e não podem ser verificadas com antecedência.

JonnyRaa
fonte
2
Como essa resposta válida e relevante foi rejeitada? Não deve haver política no StackOverflow, e se essa resposta parecer errar o alvo, adicione um comentário nesse sentido. A votação negativa é para respostas irrelevantes ou erradas.
debatedor de
1

Tudo isso está documentado no MSDN (conforme vinculado a outras respostas), mas aqui está uma regra geral ...

No setter, se sua propriedade deve ser validada acima e além do tipo. Por exemplo, uma propriedade chamada PhoneNumber provavelmente deve ter validação regex e deve gerar um erro se o formato não for válido.

Para getters, possivelmente quando o valor for nulo, mas provavelmente isso é algo que você desejará tratar no código de chamada (de acordo com as diretrizes de design).

David Stratton
fonte
0

Esta é uma pergunta muito complexa e a resposta depende de como seu objeto é usado. Como regra geral, getters e setters de propriedade que são "vinculação tardia" não devem lançar exceções, enquanto propriedades com "vinculação antecipada" exclusivamente devem lançar exceções quando necessário. A propósito, a ferramenta de análise de código da Microsoft está definindo o uso de propriedades de maneira muito restrita na minha opinião.

"ligação tardia" significa que as propriedades são encontradas por meio de reflexão. Por exemplo, o atributo Serializeable "é usado para serializar / desserializar um objeto por meio de suas propriedades. Lançar uma exceção durante esse tipo de situação quebra as coisas de uma forma catastrófica e não é uma boa maneira de usar exceções para criar um código mais robusto.

"vinculação antecipada" significa que o uso de uma propriedade é vinculado ao código pelo compilador. Por exemplo, quando algum código que você escreve faz referência a um getter de propriedade. Nesse caso, não há problema em lançar exceções quando elas fizerem sentido.

Um objeto com atributos internos tem um estado determinado pelos valores desses atributos. Propriedades que expressam atributos que estão cientes e sensíveis ao estado interno do objeto não devem ser usadas para vinculação tardia. Por exemplo, digamos que você tenha um objeto que deve ser aberto, acessado e fechado. Nesse caso, acessar as propriedades sem chamar open primeiro deve resultar em uma exceção. Suponha, neste caso, que não lançamos uma exceção e permitimos o acesso do código a um valor sem lançar uma exceção. O código parecerá feliz, embora tenha obtido um valor de um getter que não faz sentido. Agora colocamos o código que chamou o getter em uma situação ruim, pois ele deve saber como verificar o valor para ver se ele não faz sentido. Isso significa que o código deve fazer suposições sobre o valor que obteve do getter da propriedade para validá-lo. É assim que um código ruim é escrito.

Jack D Menendez
fonte
0

Eu tinha esse código em que não tinha certeza de qual exceção lançar.

public Person
{
    public string Name { get; set; }
    public boolean HasPets { get; set; }
}

public void Foo(Person person)
{
    if (person.Name == null) {
        throw new Exception("Name of person is null.");
        // I was unsure of which exception to throw here.
    }

    Console.WriteLine("Name is: " + person.Name);
}

Impedi que o modelo tivesse a propriedade nula em primeiro lugar, forçando-o como um argumento no construtor.

public Person
{
    public Person(string name)
    {
        if (name == null) {
            throw new ArgumentNullException(nameof(name));
        }
        Name = name;
    }

    public string Name { get; private set; }
    public boolean HasPets { get; set; }
}

public void Foo(Person person)
{
    Console.WriteLine("Name is: " + person.Name);
}
Fred
fonte