exception - propagacion - ¿Cuándo es correcto que un constructor arroje una excepción?
propagacion de excepciones en java (24)
Consulte las secciones de preguntas frecuentes de C ++ 17.2 y 17.4 .
En general, he encontrado que el código es más fácil de portar y mantener resultados si los constructores se escriben para que no fallen, y el código que puede fallar se coloca en un método separado que devuelve un código de error y deja el objeto en un estado inerte .
¿Cuándo es correcto que un constructor arroje una excepción? (O en el caso del Objetivo C: ¿cuándo es correcto que un iniciador vuelva a cero?)
Me parece que un constructor debe fallar y, por lo tanto, negarse a crear un objeto si el objeto no está completo. Es decir, el constructor debe tener un contrato con la persona que llama para proporcionar un objeto funcional y funcional en el que los métodos se puedan llamar de manera significativa. Es eso razonable?
Debería lanzar una excepción desde un constructor si no puede crear un objeto válido. Esto le permite proporcionar invariantes adecuados en su clase.
En la práctica, debes tener mucho cuidado. Recuerde que en C ++, no se llamará al destructor, por lo que si tira después de asignar sus recursos, debe tener mucho cuidado de manejarlo correctamente.
Esta página tiene una discusión exhaustiva de la situación en C ++.
Debido a todos los problemas que una clase parcialmente creada puede causar, yo diría que nunca.
Si necesita validar algo durante la construcción, haga que el constructor sea privado y defina un método público de fábrica estático. El método puede arrojarse si algo no es válido. Pero si todo sale bien, llama al constructor, que tiene la garantía de no tirar.
El contrato habitual en OO es que los métodos de objetos realmente funcionan.
Entonces, como corrolario, nunca devolver un objeto zombie desde un constructor / init.
Un zombie no es funcional y pueden faltar componentes internos. Solo una excepción de puntero nulo esperando a suceder.
Primero hice zombies en Objective C, hace muchos años.
Como todas las reglas generales, hay una "excepción".
Es muy posible que una interfaz específica pueda tener un contrato que diga que existe un método de "inicialización" que permite una excepción. Es posible que un objeto que implemente esta interfaz no responda correctamente a ninguna llamada, excepto a los que establecen la propiedad hasta que se haya invocado la inicialización. Utilicé esto para los controladores de dispositivo en un sistema operativo OO durante el proceso de arranque, y era viable.
En general, no quieres objetos zombies. En idiomas como Smalltalk, las cosas se ponen un poco burbujeantes, pero el uso excesivo de convertirse también es malo. Convertirse permite que un objeto se convierta en otro objeto in situ, por lo que no hay necesidad de envoltura de sobre (C ++ avanzado) o el patrón de estrategia (GOF).
El mejor consejo que he visto sobre excepciones es emitir una excepción si, y solo si, la alternativa es no cumplir con una condición de publicación o mantener una invariante.
Ese consejo reemplaza una decisión subjetiva poco clara (es una buena idea ) con una pregunta técnica y precisa basada en decisiones de diseño (condiciones invariables y posteriores) que ya debería haber hecho.
Los constructores son solo un caso particular, pero no especial, para ese consejo. Entonces la pregunta es, ¿qué invariantes debería tener una clase? Los defensores de un método de inicialización separado, que se llamará después de la construcción, sugieren que la clase tiene dos o más modos operativos , con un modo no preparado después de la construcción y al menos un modo listo , ingresado después de la inicialización. Esa es una complicación adicional, pero aceptable si la clase tiene múltiples modos de operación de todos modos. Es difícil ver cómo esta complicación vale la pena si la clase no tuviera modos operativos.
Tenga en cuenta que presionar configurar en un método de inicialización separado no le permite evitar excepciones. Ahora el método de inicialización arrojará excepciones que podría haber lanzado su constructor. Todos los métodos útiles de su clase tendrán que lanzar excepciones si se les llama para un objeto no inicializado.
Tenga en cuenta también que evitar la posibilidad de que el constructor genere excepciones es problemático y, en muchos casos, imposible en muchas bibliotecas estándar. Esto se debe a que los diseñadores de esas bibliotecas creen que arrojar excepciones de los constructores es una buena idea. En particular, cualquier operación que intente adquirir un recurso no compartible o finito (como la asignación de memoria) puede fallar, y esa falla generalmente se indica en los lenguajes OO y las bibliotecas lanzando una excepción.
El trabajo del constructor es llevar el objeto a un estado utilizable. Básicamente hay dos escuelas de pensamiento sobre esto.
Un grupo favorece la construcción en dos etapas. El constructor simplemente pone el objeto en un estado de durmiente en el que se niega a hacer ningún trabajo. Hay una función adicional que realiza la inicialización real.
Nunca he entendido el razonamiento detrás de este enfoque. Estoy firmemente en el grupo que admite la construcción en una etapa, donde el objeto se inicializa por completo y se puede usar después de la construcción.
Los constructores de una etapa deben lanzar si no pueden inicializar completamente el objeto. Si el objeto no se puede inicializar, no se debe permitir que exista, por lo que el constructor debe lanzar.
En general, no se gana nada al divorciar la inicialización de objeto de la construcción. RAII es correcto, una llamada exitosa al constructor debería dar como resultado un objeto en vivo completamente inicializado o debería fallar, y TODAS las fallas en cualquier punto en cualquier ruta de código siempre arrojarían una excepción. No se gana nada mediante el uso de un método init () separado, excepto la complejidad adicional en algún nivel. El contrato de ctor debe ser o devuelve un objeto funcional válido o se limpia después de sí mismo y lo arroja.
Considere, si implementa un método de inicio separado, igual debe llamarlo. Todavía tendrá el potencial de arrojar excepciones, todavía tienen que ser manejadas y casi siempre tienen que ser llamadas inmediatamente después del constructor de todos modos, excepto que ahora tiene 4 posibles estados de objeto en lugar de 2 (IE, construido, inicializado, no inicializado, y falló frente a simplemente válido y no existente).
En cualquier caso, me encontré con 25 años de casos de desarrollo de OO en los que parece que un método de inicio separado para ''resolver algún problema'' son fallas de diseño. Si no necesita un objeto AHORA, entonces no debe construirlo ahora, y si lo necesita ahora, entonces necesita inicializarlo. KISS siempre debe ser el principio seguido, junto con el concepto simple de que el comportamiento, estado y API de cualquier interfaz debe reflejar QUÉ hace el objeto, no CÓMO lo hace, el código del cliente ni siquiera debe ser consciente de que el objeto tiene algún tipo del estado interno que requiere inicialización, por lo tanto, el patrón init después viola este principio.
Es razonable que un constructor arroje una excepción siempre que se limpie correctamente. Si sigue el paradigma de RAII (Inicialización de adquisición de recursos es inicialización), entonces es bastante común que un constructor haga un trabajo significativo; un constructor bien escrito limpiará a su vez si no se puede inicializar por completo.
Estoy aprendiendo Objective C, así que no puedo hablar por experiencia, pero sí leí sobre esto en los documentos de Apple.
No solo le dirá cómo manejar la pregunta que hizo, sino que también lo explicará bien.
Hablando estrictamente desde el punto de vista de Java, cada vez que inicialice un constructor con valores ilegales, debería lanzar una excepción. De esta forma no se construye en mal estado.
Lanza una excepción si no puedes inicializar el objeto en el constructor, un ejemplo son los argumentos ilegales.
Como regla general, una excepción siempre debe lanzarse tan pronto como sea posible, ya que facilita la depuración cuando el origen del problema está más cerca del método de señalización de que algo está mal.
Lanzar una excepción durante la construcción es una excelente manera de hacer que su código sea más complejo. Las cosas que parecen simples de repente se vuelven difíciles. Por ejemplo, digamos que tienes una pila. ¿Cómo se saca la pila y se devuelve el valor superior? Bueno, si los objetos en la pila pueden lanzar en sus constructores (construir el temporal para regresar al llamador), no puede garantizar que no perderá datos (disminuya el puntero de la pila, construya el valor de retorno usando el constructor de valor de copia en pila, que arroja, y ahora tiene una pila que acaba de perder un elemento)! Esta es la razón por la que std :: stack :: pop no devuelve un valor, y debe llamar a std :: stack :: top.
Este problema está bien descrito here , verifique el Ítem 10, escribiendo código de excepción de seguridad.
No estoy seguro de que cualquier respuesta pueda ser totalmente independiente del idioma. Algunos lenguajes manejan las excepciones y la administración de memoria de manera diferente.
He trabajado antes bajo estándares de codificación que requieren excepciones que nunca se deben usar y solo códigos de error en inicializadores, porque los desarrolladores se han quemado por el idioma que maneja las excepciones deficientemente. Los lenguajes sin recolección de basura manejarán el montón y la pila de forma muy diferente, lo que puede importar para objetos que no sean RAII. Sin embargo, es importante que un equipo decida ser coherente para que sepa de manera predeterminada si necesita llamar a los inicializadores después de los constructores. Todos los métodos (incluidos los constructores) también deberían estar bien documentados en cuanto a las excepciones que pueden lanzar, para que las personas que llaman sepan cómo manejarlos.
En general, estoy a favor de una construcción de una sola etapa, ya que es fácil olvidarse de inicializar un objeto, pero hay muchas excepciones a eso.
- Su soporte de idioma para excepciones no es muy bueno.
- Tiene un motivo de diseño urgente para seguir usando el
new
ydelete
- Su inicialización es intensiva en el procesador y debe ejecutarse de manera asíncrona al hilo que creó el objeto.
- Está creando una DLL que puede estar lanzando excepciones fuera de su interfaz a una aplicación que usa un idioma diferente. En este caso, puede que no sea tanto cuestión de no lanzar excepciones, sino de asegurarse de que estén atrapadas antes de la interfaz pública. (Puede capturar excepciones de C ++ en C #, pero hay aros para saltar).
- Constructores estáticos (C #)
No puedo abordar las mejores prácticas en Objective-C, pero en C ++ está bien que un constructor genere una excepción. Especialmente porque no hay otra manera de garantizar que se informa una condición excepcional encontrada en la construcción sin recurrir a la invocación de un método isOK ().
La función función de bloqueo de prueba se diseñó específicamente para admitir fallas en la inicialización de miembros del constructor (aunque también se puede usar para funciones regulares). Es la única forma de modificar o enriquecer la información de excepción que se lanzará. Pero debido a su propósito de diseño original (uso en constructores) no permite que la excepción sea tragada por una cláusula catch () vacía.
Para mí, es una decisión de diseño algo filosófica.
Es muy bueno tener instancias que sean válidas mientras existan, desde el momento ctor en adelante. Para muchos casos no triviales esto puede requerir lanzar excepciones del controlador si no se puede hacer una asignación de memoria / recursos.
Algunos otros enfoques son el método init () que viene con algunos problemas propios. Uno de los cuales es asegurar que init () realmente se llame.
Una variante está utilizando un enfoque lento para llamar automáticamente a init () la primera vez que se llama a un accesorio / mutador, pero eso requiere que cualquier persona que llama potencial tenga que preocuparse de que el objeto sea válido. (A diferencia del "existe, de ahí su filosofía válida").
He visto varios patrones de diseño propuestos para tratar este problema también. Como poder crear un objeto inicial a través de ctor, pero tener que llamar a init () para obtener un objeto contenido, inicializado con accesorios / mutators.
Cada enfoque tiene sus altibajos; He usado todos estos con éxito. Si no hace objetos listos para usar desde el momento en que se crean, entonces recomiendo una gran dosis de aseveraciones o excepciones para asegurarse de que los usuarios no interactúen antes de init ().
Apéndice
Escribí desde una perspectiva de programadores de C ++. También asumo que estás usando correctamente el modismo RAII para manejar los recursos que se lanzan cuando se lanzan excepciones.
Por lo que puedo decir, nadie está presentando una solución bastante obvia que incorpore lo mejor de la construcción en una y dos etapas.
nota: esta respuesta asume C #, pero los principios se pueden aplicar en la mayoría de los idiomas.
Primero, los beneficios de ambos:
Un escenario
La construcción en una etapa nos beneficia al evitar que los objetos existentes en un estado no válido, lo que impide todo tipo de gestión de estado erróneo y todos los errores que vienen con él. Sin embargo, algunos de nosotros nos sentimos raros porque no queremos que nuestros constructores generen excepciones, y algunas veces eso es lo que debemos hacer cuando los argumentos de inicialización no son válidos.
public class Person
{
public string Name { get; }
public DateTime DateOfBirth { get; }
public Person(string name, DateTime dateOfBirth)
{
if (string.IsNullOrWhitespace(name))
{
throw new ArgumentException(nameof(name));
}
if (dateOfBirth > DateTime.UtcNow) // side note: bad use of DateTime.UtcNow
{
throw new ArgumentOutOfRangeException(nameof(dateOfBirth));
}
this.Name = name;
this.DateOfBirth = dateOfBirth;
}
}
Dos etapas a través del método de validación
La construcción en dos etapas nos beneficia al permitir que nuestra validación se ejecute fuera del constructor, y por lo tanto evita la necesidad de lanzar excepciones dentro del constructor. Sin embargo, nos deja con instancias "no válidas", lo que significa que hay un estado que tenemos que rastrear y administrar para la instancia, o lo desechamos inmediatamente después de la asignación del montón. Supone la pregunta: ¿Por qué estamos realizando una asignación de montón, y por lo tanto la recolección de memoria, en un objeto que ni siquiera terminamos usando?
public class Person
{
public string Name { get; }
public DateTime DateOfBirth { get; }
public Person(string name, DateTime dateOfBirth)
{
this.Name = name;
this.DateOfBirth = dateOfBirth;
}
public void Validate()
{
if (string.IsNullOrWhitespace(Name))
{
throw new ArgumentException(nameof(Name));
}
if (DateOfBirth > DateTime.UtcNow) // side note: bad use of DateTime.UtcNow
{
throw new ArgumentOutOfRangeException(nameof(DateOfBirth));
}
}
}
Etapa única a través de un constructor privado
Entonces, ¿cómo podemos mantener las excepciones fuera de nuestros constructores, y evitar que realizamos la asignación de montón en objetos que serán descartados inmediatamente? Es bastante básico: hacemos que el constructor sea privado y creamos instancias a través de un método estático designado para realizar una creación de instancias, y por lo tanto, asignación de montones, solo después de la validación.
public class Person
{
public string Name { get; }
public DateTime DateOfBirth { get; }
private Person(string name, DateTime dateOfBirth)
{
this.Name = name;
this.DateOfBirth = dateOfBirth;
}
public static Person Create(
string name,
DateTime dateOfBirth)
{
if (string.IsNullOrWhitespace(Name))
{
throw new ArgumentException(nameof(name));
}
if (dateOfBirth > DateTime.UtcNow) // side note: bad use of DateTime.UtcNow
{
throw new ArgumentOutOfRangeException(nameof(DateOfBirth));
}
return new Person(name, dateOfBirth);
}
}
Async Single-Stage a través de un constructor privado
Además de los beneficios de prevención de validación y distribución ya mencionados, la metodología anterior nos proporciona otra ventaja interesante: el soporte asincrónico. Esto es útil cuando se trata de autenticación en varias etapas, como cuando necesita recuperar un token de portador antes de usar su API. De esta forma, no terminará con un cliente de la API "desconectado" no válido y, en su lugar, puede simplemente volver a crear el cliente API si recibe un error de autorización al intentar realizar una solicitud.
public class RestApiClient
{
public RestApiClient(HttpClient httpClient)
{
this.httpClient = new httpClient;
}
public async Task<RestApiClient> Create(string username, string password)
{
if (username == null)
{
throw new ArgumentNullException(nameof(username));
}
if (password == null)
{
throw new ArgumentNullException(nameof(password));
}
var basicAuthBytes = Encoding.ASCII.GetBytes($"{username}:{password}");
var basicAuthValue = Convert.ToBase64String(basicAuthBytes);
var authenticationHttpClient = new HttpClient
{
BaseUri = new Uri("https://auth.example.io"),
DefaultRequestHeaders = {
Authentication = new AuthenticationHeaderValue("Basic", basicAuthValue)
}
};
using (authenticationHttpClient)
{
var response = await httpClient.GetAsync("login");
var content = response.Content.ReadAsStringAsync();
var authToken = content;
var restApiHttpClient = new HttpClient
{
BaseUri = new Uri("https://api.example.io"), // notice this differs from the auth uri
DefaultRequestHeaders = {
Authentication = new AuthenticationHeaderValue("Bearer", authToken)
}
};
return new RestApiClient(restApiHttpClient);
}
}
}
Las desventajas de este método son pocas, en mi experiencia.
Generalmente, el uso de esta metodología significa que ya no puede usar la clase como DTO porque la deserialización de un objeto sin un constructor predeterminado público es difícil, en el mejor de los casos. Sin embargo, si estuviera utilizando el objeto como DTO, no debería realmente estar validando el objeto en sí, sino invalidar los valores en el objeto cuando intente usarlos, ya que técnicamente los valores no son "inválidos" con respecto a a la DTO.
También significa que terminará creando métodos o clases de fábrica cuando necesite permitir que un contenedor IOC cree el objeto, ya que de lo contrario el contenedor no sabrá cómo crear una instancia del objeto. Sin embargo, en muchos casos, los métodos de fábrica terminan siendo uno de los métodos de Create
.
Sí, si el constructor no puede construir una de sus partes internas, puede ser, por elección, su responsabilidad de lanzar (y en cierto idioma declarar) una excepción explícita , debidamente anotada en la documentación del constructor.
Esta no es la única opción: podría finalizar el constructor y construir un objeto, pero con un método ''isCoherent ()'' que devuelve falso, para poder señalar un estado incoherente (que puede ser preferible en ciertos casos, en orden para evitar una interrupción brutal del flujo de trabajo de ejecución debido a una excepción)
Advertencia: como dijo EricSchaefer en su comentario, eso puede traer cierta complejidad a las pruebas unitarias (un lanzamiento puede aumentar la complejidad ciclomática de la función debido a la condición que la desencadena)
Si falla debido a la persona que llama (como un argumento nulo proporcionado por la persona que llama, donde el constructor llamado espera un argumento no nulo), el constructor lanzará una excepción de tiempo de ejecución no verificado de todos modos.
Se supone que los ctors no deben hacer nada "inteligente", por lo que no es necesario lanzar una excepción de todos modos. Use un método Init () o Setup () si desea realizar una configuración de objetos más complicada.
Si está escribiendo UI-Controls (ASPX, WinForms, WPF, ...), debe evitar lanzar excepciones en el constructor porque el diseñador (Visual Studio) no puede manejarlos cuando crea sus controles. Conozca su ciclo de vida de control (eventos de control) y use la inicialización diferida siempre que sea posible.
Siempre es bastante dudoso, especialmente si está asignando recursos dentro de un constructor; Dependiendo de su idioma, no se llamará al destructor, por lo que debe realizar una limpieza manual. Depende de cómo comienza la vida de un objeto en su idioma.
La única vez que lo he hecho realmente es cuando ha habido un problema de seguridad en alguna parte que significa que el objeto no debe ser creado, más que no puede.
Tenga en cuenta que si lanza una excepción en un inicializador, terminará goteando si algún código está usando el [[[MyObj alloc] init] autorelease]
, ya que la excepción omitirá la liberación automática.
Ver esta pregunta:
¿Cómo previene fugas cuando se genera una excepción en init?
Un constructor debe lanzar una excepción cuando no puede completar la construcción de dicho objeto.
Por ejemplo, si se supone que el constructor debe asignar 1024 KB de memoria RAM, y no lo hace, debe lanzar una excepción, de esta manera la persona que llama sabe que el objeto no está listo para usarse y hay un error. en algún lugar que necesita ser arreglado.
Los objetos que están medio inicializados y medio muertos solo causan problemas y problemas, ya que realmente no hay forma de que la persona que llama lo sepa. Prefiero que mi constructor arroje un error cuando las cosas van mal, que tener que depender de la programación para ejecutar una llamada a la función isOK () que devuelve verdadero o falso.
Usando fábricas o métodos de fábrica para la creación de todos los objetos, puede evitar objetos inválidos sin lanzar excepciones de los constructores. El método de creación debe devolver el objeto solicitado si puede crear uno, o nulo si no lo es. Pierde un poco de flexibilidad en el manejo de errores de construcción en el usuario de una clase, porque devolver nulo no le dice qué salió mal en la creación del objeto. Pero también evita agregar la complejidad de múltiples manejadores de excepciones cada vez que solicita un objeto, y el riesgo de detectar excepciones que no debería manejar.
Eric Lippert dice que hay 4 tipos de excepciones.
- Las excepciones fatales no son tu culpa, no puedes prevenirlas, y no puedes limpiarlas sensiblemente.
- Las excepciones de Boneheaded son tu propia maldita falla, podrías haberlas prevenido y por lo tanto son errores en tu código.
- Las excepciones molestas son el resultado de decisiones de diseño desafortunadas. Las excepciones molestas se lanzan en una circunstancia completamente no excepcional, y por lo tanto deben ser atrapadas y manejadas todo el tiempo.
- Y finalmente, las excepciones exógenas parecen ser algo así como molestas excepciones, excepto que no son el resultado de desafortunadas elecciones de diseño. Más bien, son el resultado de realidades externas desordenadas que afectan a su hermosa y nítida lógica de programa.
Su constructor nunca debe arrojar una excepción fatal por sí mismo, pero el código que ejecuta puede causar una excepción fatal. Algo como "falta de memoria" no es algo que puedas controlar, pero si ocurre en un constructor, oye, sucede.
Las excepciones descartadas nunca deberían ocurrir en ninguno de tus códigos, por lo que están correctas.
Las excepciones Int32.Parse()
(el ejemplo es Int32.Parse()
) no deberían ser lanzadas por los constructores, ya que no tienen circunstancias no excepcionales.
Finalmente, las excepciones exógenas deben evitarse, pero si está haciendo algo en su constructor que depende de circunstancias externas (como la red o el sistema de archivos), sería apropiado emitir una excepción.