it-swarm-es.tech

¿Cuándo es correcto que un constructor lance una excepción?

¿Cuándo es correcto que un constructor lance una excepción? (O en el caso del Objetivo C: ¿cuándo es correcto que un iniciador devuelva cero?)

Me parece que un constructor debería fallar, y por lo tanto rehusarse a crear un objeto, si el objeto no está completo. Es decir, el constructor debe tener un contrato con su interlocutor para proporcionar un objeto funcional y de trabajo sobre qué métodos se pueden llamar de manera significativa. ¿Es eso razonable?

195
Mark R Lindsey

El trabajo del constructor es llevar el objeto a un estado utilizable. Hay básicamente dos escuelas de pensamiento sobre esto.

Un grupo favorece la construcción en dos etapas. El constructor simplemente pone el objeto en un estado durmiente en el que se niega a realizar cualquier trabajo. Hay una función adicional que hace la inicialización real.

Nunca he entendido el razonamiento detrás de este enfoque. Estoy firmemente en el grupo que soporta la construcción de una etapa, donde el objeto está completamente inicializado y se puede utilizar después de la construcción.

Los constructores de una etapa deberían 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.

256
Sebastian Redl

Eric Lippert dice hay 4 tipos de excepciones.

  • Las excepciones fatales no son su culpa, no puede evitarlas y no puede limpiarlas con sensatez.
  • Las excepciones sin sentido son su propia maldita culpa, podría haberlas evitado y, por lo tanto, son errores en su código.
  • Las excepciones molestas son el resultado de desafortunadas decisiones de diseño. Las excepciones molestas son lanzadas 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 excepciones un tanto desconcertantes, excepto que no son el resultado de desafortunadas elecciones de diseño. Más bien, son el resultado de realidades externas desordenadas que inciden en su lógica de programa hermosa y nítida.

Su constructor nunca debe lanzar una excepción fatal por sí mismo, pero el código que ejecuta puede causar una excepción fatal. Algo como "fuera de la memoria" no es algo que puedas controlar, pero si ocurre en un constructor, oye, sucede.

Las excepciones sin cabeza nunca deben ocurrir en ninguno de sus códigos, por lo que son justas.

Las excepciones molestas (el ejemplo es Int32.Parse()) no deben ser lanzadas por los constructores, ya que no tienen circunstancias no excepcionales.

Finalmente, deben evitarse las excepciones exógenas, pero si está haciendo algo en su constructor que depende de circunstancias externas (como la red o el sistema de archivos), sería apropiado lanzar una excepción.

55
Jacob Krall

Hay en general no se gana nada al separar la inicialización de los objetos de la construcción. RAII es correcto, una llamada exitosa al constructor debería dar como resultado un objeto vivo completamente inicializado o debería fallar, yTODAS LAS FALLASen cualquier punto de cualquier ruta de código siempre deben generar una excepción. Usted no gana nada mediante el uso de un método init () aparte, excepto la complejidad adicional en algún nivel. El contrato ctor debe ser, ya sea que devuelve un objeto funcional válido o se limpia después de sí mismo y arroja.

Considere, si implementa un método de inicio por separado, usted todavía tiene que llamarlo. Todavía tendrá el potencial de lanzar excepciones, aún tienen que ser manejadas y virtualmente siempre tienen que ser llamadas inmediatamente después del constructor, excepto que ahora tiene 4 estados de objeto posibles en lugar de 2 (IE, construido, inicializado, no inicializado, y falló vs solo válido e inexistente).

En cualquier caso, me he topado con 25 años de OO casos de desarrollo en los que parece que un método de inicio por separado 'solucionaría algún problema' son fallas de diseño. Si no necesita un objeto AHORA, entonces no debería estar construyéndolo ahora, y si lo necesita ahora, entonces necesita inicializarlo. KISS siempre debe ser el principio seguido, junto con el simple concepto de que el comportamiento, el estado y la API de cualquier interfaz deben reflejar LO QUE el objeto hace, no CÓMO lo hace, el código del cliente ni siquiera debe ser consciente que el objeto tiene algún tipo de estado interno que requiera inicialización, por lo tanto, el patrón de inicio después del inicio viola este principio.

30
Alhazred

Debido a todos los problemas que puede causar una clase parcialmente creada, diría que nunca.

Si necesita validar algo durante la construcción, haga que el constructor sea privado y defina un método de fábrica estático público. El método puede lanzar si algo no es válido. Pero si todo sale bien, llama al constructor, que está garantizado para no lanzar.

6
Michael L Perry

Un constructor debe lanzar una excepción cuando no pueda completar la construcción de dicho objeto.

Por ejemplo, si se supone que el constructor asigna 1024 KB de RAM, y no lo hace, debería lanzar una excepción, de esta manera el llamador del constructor sabe que el objeto no está listo para ser utilizado y hay un error. En algún lugar que necesita ser arreglado.

Los objetos que están medio inicializados y medio muertos simplemente 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, en lugar de tener que confiar en la programación para ejecutar una llamada a la función isOK () que devuelve verdadero o falso.

5
Denice

Siempre es un poco peligroso, especialmente si está asignando recursos dentro de un constructor; Dependiendo de su idioma, no se llamará al destructor, por lo que necesita una limpieza manual. Depende de cómo comienza la vida útil de un objeto en su idioma.

La única vez que lo he hecho es cuando ha habido un problema de seguridad en algún lugar que significa que el objeto no debe, en lugar de no poder, ser creado.

4
blowdart

Es razonable que un constructor lance una excepción siempre que se limpie correctamente. Si sigue el paradigma RAII (La adquisición de recursos es la inicialización), entonces es bastante común para que un constructor realice un trabajo significativo; un constructor bien escrito, a su vez, se limpiará después de sí mismo si no se puede inicializar completamente.

4
Matt Dillard

Por lo que puedo decir, nadie presenta una solución bastante obvia que incorpore lo mejor de la construcción de una etapa y de 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 existan en un estado no válido, evitando así todo tipo de gestión de estados erróneos y todos los errores que vienen con ella. Sin embargo, algunos de nosotros nos sentimos extraños porque no queremos que nuestros constructores hagan excepciones, y en ocasiones 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 de pila. Plantea la pregunta: ¿Por qué estamos realizando una asignación de almacenamiento dinámico y, por lo tanto, una recopilació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 constructor privado

Entonces, ¿cómo podemos mantener las excepciones fuera de nuestros constructores e impedirnos realizar una asignación de montón en objetos que se descartarán 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 instanciación, y por lo tanto, la asignación de pila, solo después de 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 constructor privado

Aparte de los beneficios de la validación y la prevención de la asignación de pilas mencionados anteriormente, la metodología anterior nos proporciona otra ventaja ingeniosa: el soporte asíncrono. Esto resulta útil cuando se trata de autenticación de múltiples etapas, como cuando necesita recuperar un token de portador antes de usar su API. De esta manera, no terminará con un cliente API "cerrado" no válido, y en su lugar, simplemente podrá 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);
        }
    }
}

Los inconvenientes de este método son pocos, en mi experiencia.

En general, usar esta metodología significa que ya no puede usar la clase como DTO porque deserializar a un objeto sin un constructor público público es difícil, en el mejor de los casos. Sin embargo, si estaba usando el objeto como DTO, no debería realmente validar el objeto en sí, sino invalidar los valores del objeto cuando intenta usarlos, ya que técnicamente los valores no son "no válidos". a la DTO.

También significa que terminará creando métodos o clases de fábrica cuando necesite permitir que un IOC contenedor 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.

4
cwharris

Tenga en cuenta que si lanza una excepción en un inicializador, terminará perdiendo si algún código está usando el patrón [[[MyObj alloc] init] autorelease], ya que la excepción omitirá la liberación automática.

Vea esta pregunta:

¿Cómo evita las fugas al generar una excepción en init?

3
stevex

Si está escribiendo controles de interfaz de usuario (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 perezosa siempre que sea posible.

3
Nick

Consulte C++ FAQ secciones 17.2 y 17.4 .

En general, he encontrado que el código es más fácil de portar y mantener los 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 .

3
moonshadow

Lance una excepción si no puede inicializar el objeto en el constructor, un ejemplo son argumentos ilegales.

Como regla general, siempre se debe lanzar una excepción lo antes posible, ya que facilita la depuración cuando la fuente del problema está más cerca del método que indica que algo está mal.

2
user14070

Absolutamente debería lanzar una excepción de un constructor si no puede crear un objeto válido. Esto le permite proporcionar invariantes adecuados en su clase.

En la práctica, puede que tenga que ser muy cuidadoso. Recuerde que en C++, no se llamará al destructor, por lo que si lanza después de asignar sus recursos, ¡debe tener mucho cuidado para manejarlo adecuadamente!

Esta página tiene una discusión detallada de la situación en C++.

2
Luke Halliwell

No puedo abordar las mejores prácticas en Objective-C, pero en C++ está bien que un constructor lance una excepción. Especialmente porque no hay otra manera de asegurar que se informe una condición excepcional encontrada en la construcción sin tener que recurrir a un método isOK ().

La función de bloqueo del bloque de prueba fue diseñada 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.

1
mlbrock

No estoy seguro de que cualquier respuesta pueda ser totalmente independiente del lenguaje. Algunos idiomas manejan las excepciones y la administración de la memoria de manera diferente.

He trabajado antes bajo estándares de codificación que requieren excepciones y nunca se usan los códigos de error en los inicializadores, porque los desarrolladores se habían quemado debido al lenguaje que manejaba las excepciones. Los idiomas sin recolección de basura manejarán el montón y la pila de manera muy diferente, lo que puede ser importante para los objetos que no son RAII. Sin embargo, es importante que un equipo decida ser coherente para que sepan de forma predeterminada si necesitan llamar a los inicializadores después de los constructores. Todos los métodos (incluidos los constructores) también deben estar bien documentados en cuanto a qué excepciones pueden lanzar, para que las personas que llaman sepan cómo manejarlos.

Generalmente 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 new y delete
  • Su inicialización requiere un uso intensivo del procesador y debe ejecutarse de forma asíncrona al subproceso que creó el objeto.
  • Está creando un 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 una cuestión de no lanzar excepciones, sino asegurarse de que se detecten antes de la interfaz pública. (Puedes capturar las excepciones de C++ en C #, pero hay obstáculos para saltar).
  • Constructores estáticos (C #)
1
Denise Skidmore

Sí, si el constructor no construye 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 terminar el constructor y construir un objeto, pero con un método 'isCoherent ()' devolviendo false, para poder señalar un estado incoherente (que puede ser preferible en cierto caso, en orden para evitar una interrupción brutal del flujo de trabajo de ejecución debido a una excepción)
Advertencia: como lo dijo EricSchaefer en su comentario, eso puede aportar algo de complejidad a la prueba de la unidad (un lanzamiento puede aumentar la complejidad ciclomática de la función debido a la condición que lo activa)

Si falla debido a la persona que llama (como un argumento nulo provisto 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 verificada de todos modos.

1
VonC

Lanzar una excepción durante la construcción es una excelente manera de hacer que su código sea mucho más complejo. Las cosas que parecen simples de repente se vuelven difíciles. Por ejemplo, digamos que tienes una pila. ¿Cómo haces estallar la pila y devuelves el valor superior? Bueno, si los objetos en la pila pueden agregar sus constructores (construyendo el temporal para devolver al llamante), no puede garantizar que no perderá datos (disminuir el puntero de la pila, construir el valor de retorno usando el constructor de copia del valor en pila, que lanza, y ahora tiene una pila que acaba de perder un elemento)! Esta es la razón por la cual std :: stack :: pop no devuelve un valor, y debe llamar a std :: stack :: top.

Este problema está bien descrito aquí , verifique el Artículo 10, escribiendo el código de excepción segura.

1
Don Neufeld

El contrato habitual en OO es que los métodos de objeto realmente funcionan.

Entonces, como corolario, nunca devolver un objeto zombie desde un constructor/init.

Un zombie no es funcional y puede que falten componentes internos. Sólo una excepción de puntero nulo esperando a suceder.

La primera vez que hice zombies en Objective C, hace muchos años.

Como todas las reglas de oro, hay una "excepción".

Es completamente posible que una interfaz específica pueda tener un contrato que diga que existe un método "inicializar" que está permitido para hacer una excepción. Es posible que un objeto que implementa esta interfaz no responda correctamente a ninguna llamada, excepto a los establecedores de propiedades, hasta que se haya llamado a la inicialización. Utilicé esto para los controladores de dispositivo en un OO sistema operativo durante el proceso de arranque, y fue viable.

En general, no quieres objetos zombie. En idiomas como Smalltalk con Become las cosas se ponen un poco efervescentes, pero el uso excesivo de Become también es un mal estilo. Become permite que un objeto se convierta en otro objeto in-situ, por lo que no es necesario envolver el sobre (Advanced C++) o el patrón de estrategia (GOF).

1
Tim Williscroft

La pregunta del OP tiene una etiqueta "idioma agnóstico" ... esta pregunta no se puede responder de manera segura de la misma manera para todos los idiomas/situaciones.

La siguiente jerarquía de clases del ejemplo de C # incluye el constructor de la clase B, omitiendo una llamada inmediata a IDisposeable.Dispose de la clase A al salir de la using de main, omitiendo la eliminación explícita de los recursos de la clase A.

Si, por ejemplo, la clase A hubiera creado una Socket en la construcción, conectada a un recurso de red, es probable que ese sea el caso después del bloque using (una anomalía relativamente oculta).

class A : IDisposable
{
    public A()
    {
        Console.WriteLine("Initialize A's resources.");
    }

    public void Dispose()
    {
        Console.WriteLine("Dispose A's resources.");
    }
}

class B : A, IDisposable
{
    public B()
    {
        Console.WriteLine("Initialize B's resources.");
        throw new Exception("B construction failure: B can cleanup anything before throwing so this is not a worry.");
    }

    public new void Dispose()
    {
        Console.WriteLine("Dispose B's resources.");
        base.Dispose();
    }
}
class C : B, IDisposable
{
    public C()
    {
        Console.WriteLine("Initialize C's resources. Not called because B throws during construction. C's resources not a worry.");
    }

    public new void Dispose()
    {
        Console.WriteLine("Dispose C's resources.");
        base.Dispose();
    }
}


class Program
{
    static void Main(string[] args)
    {
        try
        {
            using (C c = new C())
            {
            }
        }
        catch
        {           
        }

        // Resource's allocated by c's "A" not explicitly disposed.
    }
}
1
Ashley

Estoy aprendiendo el Objetivo C, así que no puedo hablar por experiencia, pero sí leí esto en los documentos de Apple.

http://developer.Apple.com/documentation/Cocoa/Conceptual/CocoaFundamentals/CocoaObjects/chapter_3_section_6.html

No solo le dirá cómo manejar la pregunta que hizo, sino que también hace un buen trabajo para explicarlo.

0
Scott Swezey

El mejor consejo que he visto sobre las excepciones es lanzar una excepción si, y solo si, la alternativa es no cumplir 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 (invariantes y post condiciones) que ya debería haber tomado.

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 modo de funcionamiento , con un no preparado modo después de la construcción y al menos uno listo modo, ingresado después de la inicialización. Esa es una complicación adicional, pero aceptable si la clase tiene modos de operación múltiples de todos modos. Es difícil ver cómo vale la pena esa complicación si la clase no tuviera modos operativos.

Tenga en cuenta que el ajuste de configuración en un método de inicialización independiente no le permite evitar que se generen excepciones. Las excepciones que su constructor podría haber lanzado ahora serán lanzadas por el método de inicialización. Todos los métodos útiles de su clase tendrán que lanzar excepciones si se les llama para un objeto sin inicializar.

Tenga en cuenta también que evitar la posibilidad de que su constructor emita 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 lanzar 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 OO idiomas y bibliotecas al lanzar una excepción.

0
Raedwald

Hablando estrictamente desde un punto de vista de Java, cada vez que inicialice un constructor con valores ilegales, debería lanzar una excepción. De esa forma no se construye en mal estado.

0
scubabbl

Para mí es una decisión de diseño un tanto filosófica.

Es muy bueno tener instancias que sean válidas mientras existan, desde el momento del ctor. Para muchos casos no triviales, esto puede requerir el lanzamiento de excepciones desde el ctor si no se puede hacer una asignación de memoria/recurso.

Algunos otros enfoques son el método init () que viene con algunos problemas propios. Uno de los cuales es asegurar que init () sea realmente llamado.

Una variante es usar un enfoque perezoso para llamar automáticamente a init () la primera vez que se llama a un accesor/mutador, pero eso requiere que cualquier persona que llama tenga que preocuparse por la validez del objeto. (A diferencia del "existe, por lo tanto, es una filosofía válida").

También he visto varios patrones de diseño propuestos para tratar este problema. Como ser capaz de crear un objeto inicial a través de ctor, pero tener que llamar a init () para tener en sus manos un objeto inicializado contenido con accesores/mutadores.

Cada enfoque tiene sus altibajos; He utilizado todos estos con éxito. Si no crea objetos listos para usar desde el momento en que se crean, entonces recomiendo una gran cantidad de afirmaciones o excepciones para asegurarse de que los usuarios no interactúen antes de init ().

Addendum

Escribí desde la perspectiva de los programadores de C++. También asumo que está utilizando correctamente el lenguaje RAII para manejar los recursos que se liberan cuando se lanzan las excepciones.

0
nsanders

Al utilizar fábricas o métodos de fábrica para la creación de todos los objetos, puede evitar los objetos no válidos sin lanzar excepciones de los constructores. El método de creación debe devolver el objeto solicitado si es capaz de crear uno, o nulo si no lo es. Pierde un poco de flexibilidad en el manejo de los errores de construcción en el usuario de una clase, porque devolver el valor nulo no le dice qué falló en la creación del objeto. Pero también evita agregar la complejidad de múltiples controladores de excepciones cada vez que solicite un objeto, y el riesgo de capturar excepciones que no debería manejar.

0
Tegan Mulholland