Mejores prácticas: lanzar excepciones de propiedades

111

¿Cuándo es apropiado lanzar una excepción desde dentro de un captador o definidor de propiedad? ¿Cuándo no es apropiado? ¿Por qué? Los enlaces a documentos externos sobre el tema serían útiles ... Google resultó sorprendentemente poco.

Jon Seigel
fuente
También relacionado: stackoverflow.com/questions/1488322/…
Brian Rasmussen
1
Leí ambas preguntas, pero ninguna respondió esta pregunta completamente en mi opinión.
Jon Seigel
Cuando sea necesario. Tanto las preguntas como las respuestas anteriores muestran que se permite y se agradece generar excepciones de getter o setter, por lo que puede simplemente "ser inteligente".
Lex Li

Respuestas:

135

Microsoft tiene sus recomendaciones sobre cómo diseñar propiedades en http://msdn.microsoft.com/en-us/library/ms229006.aspx

Esencialmente, recomiendan que los compradores de propiedades sean accesos ligeros a los que siempre se pueda llamar con seguridad. Recomiendan rediseñar los getters para que sean métodos si es necesario lanzar excepciones. Para los establecedores, indican que las excepciones son una estrategia de manejo de errores apropiada y aceptable.

Para los indexadores, Microsoft indica que es aceptable tanto para los captadores como para los definidores lanzar excepciones. Y de hecho, muchos indexadores de la biblioteca .NET hacen esto. La excepción más común es ArgumentOutOfRangeException.

Hay algunas razones bastante buenas por las que no desea lanzar excepciones en los captadores de propiedades:

  • Dado que las propiedades "parecen" ser campos, no siempre es evidente que puedan generar una excepción (por diseño); mientras que con los métodos, los programadores están capacitados para esperar e investigar si las excepciones son una consecuencia esperada de invocar el método.
  • Los captadores son utilizados por una gran cantidad de infraestructura .NET, como los serializadores y el enlace de datos (en WinForms y WPF, por ejemplo); lidiar con excepciones en tales contextos puede volverse problemático rápidamente.
  • Los depuradores evalúan automáticamente los captadores de propiedades cuando observa o inspecciona un objeto. Una excepción aquí puede ser confusa y ralentizar sus esfuerzos de depuración. Tampoco es deseable realizar otras operaciones costosas en propiedades (como acceder a una base de datos) por las mismas razones.
  • Las propiedades se utilizan a menudo en una convención de encadenamiento: obj.PropA.AnotherProp.YetAnother- con este tipo de sintaxis resulta problemático decidir dónde inyectar las sentencias catch de excepción.

Como nota al margen, uno debe tener en cuenta que el hecho de que una propiedad no esté diseñada para lanzar una excepción no significa que no lo hará; fácilmente podría estar llamando a un código que lo hace. Incluso el simple acto de asignar un nuevo objeto (como una cadena) podría resultar en excepciones. Siempre debe escribir su código a la defensiva y esperar excepciones de cualquier cosa que invoque.

LBushkin
fuente
41
Si se encuentra con una excepción fatal como "sin memoria", poco importa si obtiene la excepción en una propiedad o en otro lugar. Si no lo obtuvo en la propiedad, solo lo obtendría un par de nanosegundos más tarde a partir de la siguiente cosa que asigne memoria. La pregunta no es "¿puede una propiedad lanzar una excepción?" Casi todo el código puede generar una excepción debido a una condición fatal. La pregunta es si una propiedad debería, por diseño, lanzar una excepción como parte de su contrato específico.
Eric Lippert
1
No estoy seguro de entender el argumento de esta respuesta. Por ejemplo, con respecto al enlace de datos, tanto WinForms como WPF están escritos específicamente para manejar correctamente las excepciones generadas por las propiedades y para tratarlas como fallas de validación, lo cual es una forma perfecta (algunos incluso creen que es la mejor) para proporcionar validación del modelo de dominio. .
Pavel Minaev
6
@Pavel: si bien tanto WinForms como WPF pueden recuperarse sin problemas de excepciones en los accesos de propiedad, no siempre es fácil identificar y recuperarse de tales errores. En ciertos casos (como cuando en WPF un establecedor de plantillas de control lanza una excepción), la excepción se ingiere silenciosamente. Esto puede conducir a sesiones de depuración dolorosas si nunca antes se ha encontrado con tales casos.
LBushkin
1
@Steven: Entonces, ¿de qué te sirvió esa clase en el caso excepcional? Si luego ha tenido que escribir código defensivo para manejar todas esas excepciones debido a una falla y presumiblemente proporcionar valores predeterminados adecuados, ¿por qué no proporcionar esos valores predeterminados en su captura? Alternativamente, si las excepciones de propiedad se lanzan al usuario, ¿por qué no simplemente lanzar la "InvalidArgumentException" original o similar para que puedan proporcionar el archivo de configuración que falta?
Zhaph - Ben Duguid
6
Hay una razón por la que estas son pautas y no reglas; ninguna directriz cubre todos los casos extremos locos. Probablemente habría creado estos métodos y no propiedades yo mismo, pero es una decisión.
Eric Lippert
34

No hay nada de malo en lanzar excepciones de los establecedores. Después de todo, ¿qué mejor manera de indicar que el valor no es válido para una propiedad determinada?

Para los captadores, generalmente está mal visto, y eso se puede explicar con bastante facilidad: un captador de propiedad, en general, informa el estado actual de un objeto; por lo tanto, el único caso en el que es razonable que un getter lance es cuando el estado no es válido. Pero generalmente también se considera una buena idea diseñar sus clases de manera que simplemente no sea posible obtener un objeto inválido inicialmente, o ponerlo en un estado inválido a través de medios normales (es decir, asegurar siempre la inicialización completa en los constructores, y intente hacer que los métodos sean seguros para excepciones con respecto a la validez de estado y las invariantes de clase). Siempre que cumpla con esa regla, los captadores de su propiedad nunca deben entrar en una situación en la que tengan que informar un estado no válido y, por lo tanto, nunca lanzar.

Hay una excepción que conozco, y en realidad es bastante importante: cualquier objeto que se implemente IDisposable. Disposeestá específicamente diseñado como una forma de llevar el objeto a un estado no válido, e incluso hay una clase de excepción especial ObjectDisposedException, para usarse en ese caso. Es perfectamente normal lanzar ObjectDisposedExceptiondesde cualquier miembro de la clase, incluidos los captadores de propiedades (y excluirse a Disposesí mismo), después de que el objeto haya sido eliminado.

Pavel Minaev
fuente
4
Gracias Pavel. Esta respuesta va en 'por qué' en lugar de simplemente volver a decir que no es una buena idea lanzar una excepción de las propiedades.
SolutionYogi
1
No me gusta la noción de que absolutamente todos los miembros de un IDisposabledeben ser inutilizados después de un Dispose. Si invocar a un miembro requeriría el uso de un recurso que no Disposeestá disponible (por ejemplo, el miembro leería datos de una secuencia que se ha cerrado), el miembro debería lanzar en ObjectDisposedExceptionlugar de filtrar ArgumentException, por ejemplo , pero si uno tiene un formulario con propiedades que representan la valores en ciertos campos, parecería mucho más útil permitir que tales propiedades se lean después de la eliminación (produciendo los últimos valores escritos) que requerir ...
supercat
1
... que Disposese aplazarán hasta que se hayan leído todas esas propiedades. En algunos casos en los que un hilo puede usar lecturas de bloqueo en un objeto mientras otro lo cierra, y donde los datos pueden llegar en cualquier momento antes Dispose, puede ser útil Disposecortar los datos entrantes, pero permitir que se lean los datos recibidos previamente. No se debe forzar una distinción artificial entre Closey Disposeen situaciones en las que de otro modo no debería existir.
supercat
Comprender la razón de la regla le permite saber cuándo romper la regla (Raymond Chen). En este caso, podemos ver que si hay un error irrecuperable de cualquier tipo no debe ocultarlo en el getter, ya que en tales casos la aplicación debe cerrarse lo antes posible.
Ben
El punto que estaba tratando de hacer es que sus captadores de propiedades generalmente no deben contener lógica que permita errores irrecuperables. Si es así, es posible que sea mejor como Get...método. Una excepción aquí es cuando tiene que implementar una interfaz existente que requiere que proporcione una propiedad.
Pavel Minaev
24

Casi nunca es apropiado para un getter y, a veces, apropiado para un colocador.

El mejor recurso para este tipo de preguntas es "Framework Design Guidelines" de Cwalina y Abrams; está disponible como libro encuadernado y gran parte de él también está disponible en línea.

De la sección 5.2: Diseño de la propiedad

EVITE lanzar excepciones de los captadores de propiedades. Los captadores de propiedades deben ser operaciones simples y no deben tener condiciones previas. Si un captador puede lanzar una excepción, probablemente debería rediseñarse para que sea un método. Tenga en cuenta que esta regla no se aplica a los indexadores, donde esperamos excepciones como resultado de validar los argumentos.

Tenga en cuenta que esta pauta solo se aplica a los compradores de propiedades. Está bien lanzar una excepción en un establecedor de propiedades.

Eric Lippert
fuente
2
Si bien (en general) estoy de acuerdo con tales pautas, creo que es útil brindar información adicional sobre por qué deben seguirse y qué tipo de consecuencias pueden surgir cuando se ignoran.
LBushkin
3
¿Cómo se relaciona esto con los objetos desechables y la guía que debería considerar lanzar ObjectDisposedExceptionuna vez que el objeto ha sido Dispose()llamado y algo posteriormente solicita un valor de propiedad? Parece que la guía debería ser "evitar lanzar excepciones de los captadores de propiedades, a menos que el objeto haya sido eliminado, en cuyo caso debería considerar lanzar una ObjectDisposedExcpetion".
Scott Dorman
4
El diseño es el arte y la ciencia de encontrar compromisos razonables frente a requisitos contradictorios. De cualquier manera parece un compromiso razonable; No me sorprendería tener un objeto tirado en una propiedad; ni me sorprendería si no fuera así. Dado que el uso de un objeto desechado es una práctica de programación terrible, no sería prudente tener expectativas.
Eric Lippert
1
Otro escenario en el que es totalmente válido lanzar excepciones desde dentro de getters es cuando un objeto está haciendo uso de invariantes de clase para validar su estado interno, que deben verificarse cada vez que se realiza un acceso público, sin importar si es un método o una propiedad
Trap
2

Un buen enfoque para las excepciones es usarlas para documentar el código para usted y otros desarrolladores de la siguiente manera:

Las excepciones deben ser para estados de programas excepcionales. Esto significa que está bien escribirlos donde quieras.

Una razón por la que puede querer ponerlos en getters es para documentar la API de una clase: si el software genera una excepción tan pronto como un programador intenta usarlo incorrectamente, ¡no lo usará incorrectamente! Por ejemplo, si tiene validación durante un proceso de lectura de datos, es posible que no tenga sentido poder continuar y acceder a los resultados del proceso si hubo errores fatales en los datos. En este caso, es posible que desee obtener el lanzamiento de salida si hubo errores para asegurarse de que otro programador verifique esta condición.

Son una forma de documentar los supuestos y los límites de un subsistema / método / lo que sea. En el caso general, ¡no deberían ser atrapados! Esto también se debe a que nunca se lanzan si el sistema funciona en conjunto de la manera esperada: si ocurre una excepción, muestra que no se cumplen las suposiciones de un fragmento de código, por ejemplo, no está interactuando con el mundo que lo rodea de la manera originalmente estaba destinado a hacerlo. Si detecta una excepción que se escribió para este propósito, probablemente signifique que el sistema ha entrado en un estado impredecible / inconsistente; esto, en última instancia, puede provocar una falla o corrupción de datos o similar, lo que probablemente sea mucho más difícil de detectar / depurar.

Los mensajes de excepción son una forma muy burda de informar errores: no se pueden recopilar en masa y solo contienen una cadena. Esto los hace inadecuados para informar problemas en los datos de entrada. En funcionamiento normal, el sistema en sí no debería entrar en un estado de error. Como resultado de esto, los mensajes en ellos deben diseñarse para programadores y no para usuarios; las cosas que están mal en los datos de entrada se pueden descubrir y transmitir a los usuarios en formatos más adecuados (personalizados).

La excepción (¡jaja!) A esta regla son cosas como IO, donde las excepciones no están bajo su control y no se pueden verificar con anticipación.

JonnyRaa
fuente
2
¿Cómo se rechazó esta respuesta válida y relevante? No debería haber política en StackOverflow, y si esta respuesta pareció perder la diana, agregue un comentario en ese sentido. La votación negativa es para respuestas que son irrelevantes o incorrectas.
debatidor
1

Todo esto está documentado en MSDN (como se vincula en otras respuestas) pero aquí hay una regla general ...

En el setter, si su propiedad debe validarse más allá del tipo. Por ejemplo, una propiedad llamada PhoneNumber probablemente debería tener validación de expresiones regulares y debería generar un error si el formato no es válido.

Para los captadores, posiblemente cuando el valor es nulo, pero lo más probable es que sea algo que querrá manejar en el código de llamada (según las pautas de diseño).

David Stratton
fuente
0

Esta es una pregunta muy compleja y la respuesta depende de cómo se utilice su objeto. Como regla general, los captadores y definidores de propiedades que son "vinculantes tardíos" no deben generar excepciones, mientras que las propiedades con "vinculación anticipada" exclusivamente deben generar excepciones cuando surja la necesidad. Por cierto, en mi opinión, la herramienta de análisis de código de Microsoft define el uso de propiedades de forma demasiado estricta.

"unión tardía" significa que las propiedades se encuentran por reflexión. Por ejemplo, el atributo Serializeable "se usa para serializar / deserializar un objeto a través de sus propiedades. Lanzar una excepción durante este tipo de situación rompe las cosas de una manera catastrófica y no es una buena manera de usar excepciones para hacer un código más robusto.

"enlace anticipado" significa que el uso de una propiedad está enlazado en el código por el compilador. Por ejemplo, cuando algún código que escribe hace referencia a un captador de propiedad. En este caso, está bien lanzar excepciones cuando tengan sentido.

Un objeto con atributos internos tiene un estado determinado por los valores de esos atributos. Las propiedades que expresan atributos que son conscientes y sensibles al estado interno del objeto no deben usarse para el enlace tardío. Por ejemplo, digamos que tiene un objeto que debe abrirse, accederse y luego cerrarse. En este caso, acceder a las propiedades sin llamar a open primero debería resultar en una excepción. Supongamos, en este caso, que no lanzamos una excepción y permitimos que el código acceda a un valor sin lanzar una excepción. El código parecerá feliz a pesar de que obtuvo un valor de un captador que no tiene sentido. Ahora hemos puesto el código que llamó al getter en una mala situación ya que debe saber comprobar el valor para ver si no tiene sentido. Esto significa que el código debe hacer suposiciones sobre el valor que obtuvo del captador de propiedad para poder validarlo. Así es como se escribe el código incorrecto.

Jack D Menendez
fuente
0

Tenía este código donde no estaba seguro de qué excepción lanzar.

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);
}

En primer lugar, evité que el modelo tuviera la propiedad nula forzándola como argumento en el constructor.

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
fuente