Pregunta

¿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 negarse a crear un objeto) si el objeto no está completo.Es decir, ¿el constructor debe tener un contrato con su llamador para proporcionar un objeto funcional y de trabajo sobre el cual los métodos se pueden llamar de manera significativa?¿Es eso razonable?

¿Fue útil?

Solución

El trabajo del constructor es llevar el objeto a un estado utilizable.Existen básicamente dos escuelas de pensamiento al respecto.

Un grupo está a favor de la construcción en dos etapas.El constructor simplemente pone el objeto en un estado de sueño en el que se niega a realizar cualquier 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 apoya la construcción en una etapa, donde el objeto está completamente inicializado y utilizable después de la construcción.

Los constructores de una etapa deberían lanzar si no logran inicializar completamente el objeto.Si el objeto no se puede inicializar, no se debe permitir que exista, por lo que el constructor debe lanzarlo.

Otros consejos

Eric Lippert dice Hay 4 tipos de excepciones.

  • Las excepciones fatales no son culpa tuya, no puedes evitarlas y no puedes eliminarlas sensatamente.
  • Las excepciones tontas son tu propia culpa, podrías haberlas evitado y, por lo tanto, son errores en tu código.
  • Las molestas excepciones son el resultado de decisiones de diseño desafortunadas.Las excepciones molestas se producen en circunstancias completamente no excepcionales y, por lo tanto, deben detectarse y manejarse todo el tiempo.
  • Y, finalmente, las excepciones exógenas parecen ser algo así como excepciones molestas excepto que no son el resultado de decisiones de diseño desafortunadas.Más bien, son el resultado de realidades externas desordenadas que inciden en la hermosa y nítida lógica de su programa.

Su constructor nunca debería generar una excepción fatal por sí solo, 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, sucede.

Las excepciones descabelladas nunca deberían ocurrir en ningún código, por lo que están descartadas.

Excepciones molestas (el ejemplo es Int32.Parse()) no deberían ser lanzados por los constructores, porque no tienen circunstancias que no sean excepcionales.

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

Hay generalmente No se gana nada divorciando la inicialización del 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 TODO Las fallas en cualquier punto de cualquier ruta de código siempre deberían generar una excepción.No se gana nada con el uso de un método init() separado, excepto complejidad adicional en algún nivel.El contrato ctor debe devolver un objeto funcional válido o limpiarlo y arrojarlo.

Considere, si implementa un método init separado, aún hay que llamarlo.Todavía tendrá el potencial de generar excepciones, aún deben manejarse y, de todos modos, prácticamente siempre deben llamarse inmediatamente después del constructor, excepto que ahora tiene 4 posibles estados de objeto en lugar de 2 (IE, construido, inicializado, no inicializado, y fallido versus simplemente válido e inexistente).

En cualquier caso, me he encontrado en 25 años de casos de desarrollo OO en los que parece que un método de inicio separado "resolvería algún problema" son fallas de diseño.Si no necesita un objeto AHORA, entonces no debería construirlo ahora, y si lo necesita ahora, entonces necesita inicializarlo.KISS siempre debe ser el principio a seguir, junto con el concepto simple de que el comportamiento, el estado y la API de cualquier interfaz deben 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. de estado interno que requiere inicialización, por lo que el patrón init after viola este principio.

Debido a todos los problemas que puede causar una clase parcialmente creada, yo 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 arrojar resultados si algo no es válido.Pero si todo sale bien, llama al constructor, que se garantiza que no arrojará nada.

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 RAM y no lo hace, debería lanzar una excepción, de esta manera la persona que llama al constructor sabe que el objeto no está listo para ser usado 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 cuestiones, 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.

Siempre es bastante dudoso, especialmente si estás asignando recursos dentro de un constructor;Dependiendo de su idioma, no se llamará al destructor, por lo que deberá realizar una limpieza manual.Depende de cuándo comienza la vida útil de un objeto en su idioma.

La única vez que realmente lo he hecho es cuando ha habido un problema de seguridad en alguna parte que significa que el objeto no debería, en lugar de no poder, crearse.

Es razonable que un constructor lance una excepción siempre que se limpie correctamente.Si sigues el RAII paradigma (La adquisición de recursos es inicialización), entonces es bastante común 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 por completo.

Hasta donde yo sé, nadie presenta una solución bastante obvia que represente lo mejor de la construcción en una y dos etapas.

nota: Esta respuesta supone C#, pero los principios se pueden aplicar en la mayoría de los lenguajes.

Primero, los beneficios de ambos:

Un escenario

La construcción en una etapa nos beneficia al evitar que existan objetos en un estado no válido, evitando así todo tipo de gestión de estado errónea y todos los errores que la acompañan.Sin embargo, a algunos de nosotros nos hace sentir raros porque no queremos que nuestros constructores generen excepciones y, a 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;
    }
}

Método de validación en dos etapas

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 generar excepciones dentro del constructor.Sin embargo, nos deja con instancias "no válidas", lo que significa que hay un estado que debemos rastrear y administrar para la instancia, o lo desechamos inmediatamente después de la asignación del montón.Se plantea la pregunta:¿Por qué realizamos una asignación de montón y, por 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));
        }
    }
}

Monoetapa a través de constructor privado

Entonces, ¿cómo podemos mantener las excepciones fuera de nuestros constructores y evitar realizar una asignación de almacenamiento dinámico 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, una asignación de montón, únicamente. después 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);
    }
}

Asíncrono de una sola etapa a través de un constructor privado

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

Según mi experiencia, las desventajas de este método son pocas.

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

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 fabriles acaban siendo uno de Create métodos mismos.

Consulte las secciones de preguntas frecuentes sobre C++ 17.2 y 17.4.

En general, he descubierto que el código es más fácil de portar y mantener los resultados si los constructores se escriben de manera 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. .

Si está escribiendo controles de interfaz de usuario (ASPX, WinForms, WPF, ...), debe evitar generar excepciones en el constructor porque el diseñador (Visual Studio) no puede manejarlas cuando crea sus controles.Conozca su ciclo de vida de control (eventos de control) y utilice la inicialización diferida siempre que sea posible.

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

Vea esta pregunta:

¿Cómo se evitan fugas al generar una excepción en init?

Absolutamente deberías lanzar una excepción desde un constructor si no puedes crear un objeto válido.Esto le permite proporcionar invariantes adecuadas en su clase.

En la práctica, es posible que deba tener mucho cuidado.Recuerde que en C++, no se llamará al destructor, por lo que si lo lanza después de asignar sus recursos, ¡debe tener mucho cuidado para manejarlo correctamente!

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

Lanza una excepción si no puedes inicializar el objeto en el constructor, un ejemplo son los 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 anda mal.

Lanzar una excepción durante la construcción es una excelente manera de hacer que su código sea mucho más complejo.Cosas que parecían simples de repente se vuelven difíciles.Por ejemplo, digamos que tienes una pila.¿Cómo se abre la pila y se devuelve el valor superior?Bueno, si los objetos en la pila pueden incluir sus constructores (construyendo el temporal para regresar a la persona que llama), no puede garantizar que no perderá datos (disminuya el puntero de la pila, construya el valor de retorno usando el constructor de copia del valor en pila, que tira, y ahora tengo una pila que acaba de perder un objeto)!Es por eso que std::stack::pop no devuelve un valor y hay que llamar a std::stack::top.

Este problema está bien descrito. aquí, consulte el elemento 10, escritura de código seguro para excepciones.

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 zombi no funciona y es posible que le falten componentes internos.Sólo una excepción de puntero nulo esperando a ocurrir.

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

Como todas las reglas generales, existe una "excepción".

Es completamente posible que un interfaz específica Puede tener un contrato que diga que existe un método "Inicializar" que puede aplicar una excepción.Que un objeto que implemente esta interfaz puede no responder correctamente a ninguna llamada, excepto a los establecedores de propiedades, hasta que se haya llamado a inicialización.Utilicé esto para controladores de dispositivos en un sistema operativo OO durante el proceso de arranque y funcionó.

En general, no querrás objetos zombies.En lenguajes como Smalltalk con convertirse las cosas se ponen un poco efervescentes, pero el uso excesivo de convertirse También es de mal estilo. Become permite que un objeto se convierta en otro objeto in situ, por lo que no hay necesidad de envoltorio de sobre (C++ avanzado) o patrón de estrategia (GOF).

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 garantizar que se informe una condición excepcional encontrada en la construcción sin recurrir a invocar un método isOK().

La característica de bloque de prueba de función fue diseñada específicamente para admitir fallas en la inicialización de miembros del constructor (aunque también puede usarse para funciones regulares).Es la única forma de modificar o enriquecer la información de excepción que se generará.Pero debido a su propósito de diseño original (uso en constructores), no permite que la excepción sea absorbida por una cláusula catch() vacía.

Sí, si el constructor no logra construir una de sus partes internas, puede ser, por elección propia, su responsabilidad de lanzar (y en cierto lenguaje declarar) una excepción explícita , debidamente anotado 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()' que devuelve falso, para poder señalar un estado incoherente (que puede ser preferible en ciertos casos, para evitar una interrupción brutal del proceso). 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 el 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 generará una excepción de tiempo de ejecución no verificada de todos modos.

No estoy seguro de que ninguna respuesta pueda ser completamente independiente del idioma.Algunos lenguajes manejan las excepciones y la administración de la memoria de manera diferente.

He trabajado antes bajo estándares de codificación que requieren que nunca se usen excepciones y solo códigos de error en los inicializadores, porque los desarrolladores se habían visto afectados por el lenguaje que manejaba mal las excepciones.Los lenguajes 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 saber 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 sobre qué excepciones pueden generar, para que quienes llaman sepan cómo manejarlas.

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 idiomas para excepciones no es muy bueno.
  • Tienes una razón de diseño apremiante para seguir usando new y delete
  • Su inicialización requiere un uso intensivo del procesador y debe ejecutarse de forma asíncrona con el hilo que creó el objeto.
  • Está creando una DLL que puede estar generando excepciones fuera de su interfaz para una aplicación que utiliza un idioma diferente.En este caso, puede que no se trate tanto de no lanzar excepciones, sino de asegurarse de que se detecten antes de la interfaz pública.(Puede detectar excepciones de C++ en C#, pero hay obstáculos que superar).
  • Constructores estáticos (C#)

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

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

Si, por ejemplo, la clase A hubiera creado un Socket en la construcción, conectado a un recurso de red, probablemente ese seguiría siendo el caso después de la using bloque (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.
    }
}

Hablando estrictamente desde el punto de vista de Java, cada vez que inicializa un constructor con valores ilegales, debería generar una excepción.De esta forma no quedará construido en mal estado.

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 del ctor en adelante.En muchos casos no triviales, esto puede requerir lanzar excepciones desde el ctor si no se puede realizar una asignación de memoria/recursos.

Algunos otros enfoques son el método init() que presenta algunos problemas propios.Uno de los cuales es garantizar que realmente se llame a init().

Una variante es utilizar un enfoque diferido para llamar automáticamente a init() la primera vez que se llama a un descriptor de acceso/mutador, pero eso requiere que cualquier posible llamador tenga que preocuparse de que el objeto sea válido.(A diferencia del "existe, por lo tanto es una filosofía válida").

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

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

Apéndice

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 excepciones.

Recién estoy aprendiendo Objective C, por lo que realmente no puedo hablar por experiencia, pero leí sobre esto en los documentos de Apple.

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

No sólo le dirá cómo manejar la pregunta que hizo, sino que también la explicará bien.

Al utilizar fábricas o métodos de fábrica para toda la creación de objetos, puede evitar objetos no válidos sin generar excepciones por parte de los constructores.El método de creación debe devolver el objeto solicitado si puede crear uno, o nulo si no es posible.Se 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 controladores de excepciones cada vez que solicita un objeto y el riesgo de detectar excepciones que no debería manejar.

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

Los constructores son sólo 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 tenga dos o más modo operativo, 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 múltiples modos de funcionamiento de todos modos.Es difícil ver cómo esa complicación vale la pena si la clase no tuviera modos de funcionamiento.

Tenga en cuenta que incluir la configuración en un método de inicialización independiente no le permite evitar que se produzcan excepciones.Las excepciones que su constructor podría haber generado ahora serán generadas por el método de inicialización.Todos los métodos útiles de su clase tendrán que generar excepciones si se llaman para un objeto no inicializado.

Tenga en cuenta también que evitar la posibilidad de que su constructor lance 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 generar excepciones desde los constructores es una buena idea.En particular, cualquier operación que intente adquirir un recurso finito o no compartible (como la asignación de memoria) puede fallar, y ese error generalmente se indica en los lenguajes y bibliotecas OO lanzando una excepción.

Se supone que los directores no deben hacer ninguna cosa "inteligente", por lo que de todos modos no es necesario lanzar una excepción.Utilice un método Init() o Setup() si desea realizar una configuración de objetos más complicada.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top