it-swarm-pt.tech

Quando é certo para um construtor lançar uma exceção?

Quando é certo para um construtor lançar uma exceção? (Ou no caso do objetivo C: quando é certo que um iniciador retorne zero?)

Parece-me que um construtor falhará - e, assim, se recusará a criar um objeto - se o objeto não estiver completo. Ou seja, o construtor deve ter um contrato com o responsável pela chamada para fornecer um objeto funcional e funcional sobre o qual os métodos podem ser chamados de maneira significativa? Isso é razoável?

195
Mark R Lindsey

O trabalho do construtor é trazer o objeto para um estado utilizável. Existem basicamente duas escolas de pensamento sobre isso.

Um grupo favorece a construção em duas etapas. O construtor apenas traz o objeto para um estado adormecido, no qual se recusa a realizar qualquer trabalho. Há uma função adicional que faz a inicialização real.

Eu nunca entendi o raciocínio por trás dessa abordagem. Estou firmemente no grupo que suporta a construção em um estágio, onde o objeto é totalmente inicializado e utilizável após a construção.

Construtores de um estágio devem ser lançados se não conseguirem inicializar completamente o objeto. Se o objeto não puder ser inicializado, ele não deve existir, portanto o construtor deve lançar.

256
Sebastian Redl

Eric Lippert diz existem 4 tipos de exceções.

  • As exceções fatais não são sua culpa, você não pode evitá-las e não pode limpar sensivelmente delas.
  • Exceções desordenadas são sua própria falha, você poderia tê-las evitado e, portanto, são erros no seu código.
  • Exceções irritantes são o resultado de decisões infelizes de design. As exceções irritantes são lançadas em uma circunstância completamente não excepcional e, portanto, devem ser capturadas e manipuladas o tempo todo.
  • E, finalmente, as exceções exógenas parecem um pouco como as exceções irritantes, exceto que elas não são o resultado de escolhas infelizes de design. Em vez disso, são o resultado de realidades externas desarrumadas que afetam sua lógica bonita e nítida do programa.

Seu construtor nunca deve lançar uma exceção fatal por conta própria, mas o código executado pode causar uma exceção fatal. Algo como "falta de memória" não é algo que você pode controlar, mas se ocorre em um construtor, ei, isso acontece.

As exceções desatualizadas nunca devem ocorrer em nenhum código, portanto estão corretas.

Exceções irritantes (o exemplo é Int32.Parse()) não devem ser lançadas pelos construtores, porque eles não têm circunstâncias não excepcionais.

Finalmente, exceções exógenas devem ser evitadas, mas se você estiver fazendo algo em seu construtor que depende de circunstâncias externas (como a rede ou o sistema de arquivos), seria apropriado lançar uma exceção.

Link de referência: https://blogs.msdn.Microsoft.com/ericlippert/2008/09/10/vexing-exceptions/

56
Jacob Krall

geralmente nada a ser ganho ao divorciar a inicialização do objeto da construção. RAII está correto, uma chamada bem-sucedida ao construtor deve resultar em um objeto ativo totalmente inicializado ou falhar e ALL falhas em qualquer ponto de qualquer caminho de código sempre devem gerar uma exceção. Você não ganha nada usando um método init () separado, exceto complexidade adicional em algum nível. O contrato do ctor deve ser: ele retorna um objeto válido funcional ou limpa depois de si mesmo e lança.

Considere, se você implementar um método init separado, você ainda precisará chamá-lo. Ele ainda terá o potencial de gerar exceções, eles ainda precisam ser manipulados e praticamente sempre precisam ser chamados imediatamente após o construtor, exceto agora que você tem quatro estados possíveis de objeto em vez de 2 (IE, construído, inicializado, não inicializado, e falhou vs apenas válido e inexistente).

De qualquer forma, eu me deparei em 25 anos de OO casos de desenvolvimento em que parece que um método init separado 'resolveria algum problema' são falhas de design. Se você não precisa de um objeto AGORA, não deve construí-lo agora e, se precisar dele agora, precisará inicializá-lo. KISS deve sempre ser o princípio seguido, juntamente com o conceito simples de que o comportamento, estado e API de qualquer interface deve refletir O QUE o objeto faz, não COMO, o código do cliente nem deve estar ciente de que o O objeto possui qualquer tipo de estado interno que requer inicialização, portanto, o padrão init after viola esse princípio.

30
Alhazred

Por causa de todos os problemas que uma classe parcialmente criada pode causar, eu diria que nunca.

Se você precisar validar algo durante a construção, torne o construtor privado e defina um método público de fábrica estática. O método pode ser lançado se algo for inválido. Mas se tudo der certo, ele chama o construtor, que é garantido para não jogar.

6
Michael L Perry

Um construtor deve lançar uma exceção quando for incapaz de concluir a construção do referido objeto.

Por exemplo, se o construtor deve alocar 1024 KB de ram e não o faz, deve lançar uma exceção, desta forma o responsável pela chamada do construtor sabe que o objeto não está pronto para ser usado e há um erro em algum lugar que precise ser consertado.

Objetos semi-inicializados e semi-mortos apenas causam problemas e problemas, pois realmente não há como o chamador saber. Prefiro que meu construtor gere um erro quando as coisas dão errado, do que ter que confiar na programação para executar uma chamada para a função isOK () que retorna verdadeiro ou falso.

5
Denice

É sempre muito complicado, especialmente se você estiver alocando recursos dentro de um construtor; dependendo do seu idioma, o destruidor não será chamado, portanto, você precisa limpar manualmente. Depende de como quando a vida útil de um objeto começa no seu idioma.

A única vez que realmente o fiz foi quando houve um problema de segurança em algum lugar que significa que o objeto não deve ser criado, e não pode.

4
blowdart

É razoável para um construtor lançar uma exceção, desde que se limpe adequadamente. Se você seguir o paradigma RAII (Aquisição de Recursos É Inicialização), então é bastante comum para um construtor realizar tarefas significativas trabalhos; um construtor bem escrito, por sua vez, será limpo se não puder ser totalmente inicializado.

4
Matt Dillard

Até onde eu sei, ninguém está apresentando uma solução bastante óbvia que incorpora o melhor da construção de um estágio e de dois estágios.

note : Esta resposta assume C #, mas os princípios podem ser aplicados na maioria dos idiomas.

Primeiro, os benefícios de ambos:

Um estágio

A construção em um estágio nos beneficia ao impedir a existência de objetos em um estado inválido, impedindo todos os tipos de gerenciamento de estado incorreto e todos os erros que acompanham o mesmo. No entanto, alguns de nós nos sentimos estranhos porque não queremos que nossos construtores gerem exceções, e às vezes é isso que precisamos fazer quando os argumentos de inicialização são invá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;
    }
}

Dois estágios via método de validação

A construção em dois estágios nos beneficia ao permitir que nossa validação seja executada fora do construtor e, portanto, evita a necessidade de lançar exceções dentro do construtor. No entanto, isso nos deixa com instâncias "inválidas", o que significa que há um estado que precisamos rastrear e gerenciar para a instância ou descartamos imediatamente após a alocação de heap. Isso levanta a questão: por que estamos realizando uma alocação de heap e, portanto, uma coleção de memória, em um objeto que nem acabamos 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));
        }
    }
}

Estágio único via construtor privado

Então, como podemos manter exceções fora de nossos construtores e impedir a execução de alocação de heap em objetos que serão descartados imediatamente? É bastante básico: tornamos o construtor privado e criamos instâncias por meio de um método estático designado para executar uma instanciação e, portanto, alocação de heap, apenas depois validação.

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

Estágio único assíncrono via construtor privado

Além dos benefícios de validação e prevenção de alocação de pilha mencionados acima, a metodologia anterior nos fornece outra vantagem bacana: suporte assíncrono. Isso é útil ao lidar com autenticação em vários estágios, como quando você precisa recuperar um token de portador antes de usar sua API. Dessa forma, você não acaba com um cliente de API "desconectado" inválido e, em vez disso, pode simplesmente recriar o cliente da API se receber um erro de autorização ao tentar executar uma solicitação.

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

As desvantagens deste método são poucas, na minha experiência.

Geralmente, o uso dessa metodologia significa que você não pode mais usar a classe como um DTO porque a desserialização para um objeto sem um construtor público padrão é difícil, na melhor das hipóteses. No entanto, se você estivesse usando o objeto como um DTO, não deveria realmente validar o objeto em si, mas invalidar os valores no objeto ao tentar usá-los, pois tecnicamente os valores não são "inválidos" para o DTO.

Isso também significa que você acabará criando métodos ou classes de fábrica quando precisar permitir que um contêiner IOC crie o objeto, pois, caso contrário, o contêiner não saberá como instanciar o objeto. No entanto, em muitos casos, os métodos de fábrica acabam sendo um dos métodos Create.

4
cwharris

Observe que, se você lançar uma exceção em um inicializador, acabará vazando se algum código estiver usando o padrão [[[MyObj alloc] init] autorelease], pois a exceção ignorará o autorelease.

Veja esta pergunta:

Como você evita vazamentos ao gerar uma exceção no init?

3
stevex

Se você estiver criando Controles de Interface do Usuário (ASPX, WinForms, WPF, ...), evite lançar exceções no construtor porque o designer (Visual Studio) não pode lidar com elas quando cria seus controles. Conheça o seu ciclo de vida de controle (eventos de controle) e use a inicialização lenta sempre que possível.

3
Nick

Consulte as seções C++ FAQ 17,2 e 17,4 .

Em geral, descobri que o código é mais fácil de portar e manter resultados se os construtores forem escritos para que não falhem, e o código que pode falhar é colocado em um método separado que retorna um código de erro e deixa o objeto em um estado inerte .

3
moonshadow

Lance uma exceção se você não conseguir inicializar o objeto no construtor, um exemplo são argumentos ilegais.

Como regra geral, uma exceção sempre deve ser lançada o mais rápido possível, pois facilita a depuração quando a fonte do problema está mais próxima do método que sinaliza que algo está errado.

2
user14070

Você absolutamente deve lançar uma exceção de um construtor se não conseguir criar um objeto válido. Isso permite que você forneça invariantes apropriados em sua classe.

Na prática, você pode ter que ter muito cuidado. Lembre-se que em C++, o destruidor não será chamado, então se você jogar depois de alocar seus recursos, você precisa tomar muito cuidado para lidar com isso corretamente!

Esta página tem uma discussão completa da situação em C++.

2
Luke Halliwell

Eu não posso abordar a melhor prática em Objective-C, mas em C++ é bom para um construtor lançar uma exceção. Especialmente porque não há outra maneira de garantir que uma condição excepcional encontrada na construção seja relatada sem recorrer a um método isOK ().

O recurso de bloqueio de função try foi projetado especificamente para suportar falhas na inicialização do construtor em membros (embora também possa ser usado para funções regulares). É a única maneira de modificar ou enriquecer as informações de exceção que serão lançadas. Mas devido ao seu propósito de design original (uso em construtores), ele não permite que a exceção seja engolida por uma cláusula catch () vazia.

1
mlbrock

Não tenho certeza de que qualquer resposta possa ser totalmente agnóstica quanto à linguagem. Alguns idiomas lidam com exceções e gerenciamento de memória de maneira diferente.

Eu trabalhei antes sob os padrões de codificação, exigindo que as exceções nunca fossem usadas e apenas os códigos de erro nos inicializadores, porque os desenvolvedores tinham sido gravados pela linguagem, mal tratando de exceções. Os idiomas sem coleta de lixo manipularão heap e pilha de maneira muito diferente, o que pode ser importante para objetos não RAII. No entanto, é importante que uma equipe decida ser consistente, de modo que saiba, por padrão, se precisa chamar os inicializadores depois dos construtores. Todos os métodos (incluindo os construtores) também devem ser bem documentados quanto às exceções que eles podem lançar, para que os chamadores saibam como lidar com eles.

Sou geralmente a favor de uma construção de estágio único, pois é fácil esquecer de inicializar um objeto, mas há muitas exceções para isso.

  • Seu suporte de idioma para exceções não é muito bom.
  • Você tem um motivo de design urgente para continuar usando new e delete
  • Sua inicialização é intensiva no processador e deve ser executada de forma assíncrona no encadeamento que criou o objeto.
  • Você está criando um DLL que pode estar lançando exceções fora de sua interface para um aplicativo usando um idioma diferente. Nesse caso, pode não ser tanto uma questão de não lançar exceções, mas garantir que elas sejam detectadas antes da interface pública. (Você pode pegar exceções do C++ em C #, mas existem aros para saltar.)
  • Construtores estáticos (C #)
1
Denise Skidmore

Sim, se o construtor falhar em construir uma de suas partes internas, pode ser - por escolha - sua responsabilidade lançar (e em determinada linguagem declarar) um exceção explícita , devidamente anotado na documentação do construtor .

Esta não é a única opção: poderia terminar o construtor e construir um objeto, mas com um método 'isCoherent ()' retornando false, para poder sinalizar um estado incoerente (que pode ser preferível em um determinado caso, para para evitar uma interrupção brutal do fluxo de trabalho de execução devido a uma exceção)
Atenção: como dito por EricSchaefer em seu comentário, isso pode trazer alguma complexidade ao teste unitário (um lance pode aumentar a complexidade ciclomática da função devido à condição que o desencadeia)

Se falhar por causa do chamador (como um argumento nulo fornecido pelo responsável pela chamada, onde o construtor chamado espera um argumento não nulo), o construtor lançará uma exceção de tempo de execução não verificada de qualquer maneira.

1
VonC

Lançar uma exceção durante a construção é uma ótima maneira de tornar seu código mais complexo. Coisas que parecem simples de repente se tornam difíceis. Por exemplo, digamos que você tenha uma pilha. Como você abre a pilha e retorna o valor máximo? Bem, se os objetos na pilha puderem lançar seus construtores (construindo o temporário para retornar ao chamador), você não pode garantir que não perderá dados (diminua o ponteiro da pilha, construa o valor de retorno usando o construtor de cópia de valor em pilha, que lança, e agora tem uma pilha que acabou de perder um item)! É por isso que std :: stack :: pop não retorna um valor e você precisa chamar std :: stack :: top.

Esse problema é bem descrito aqui , verifique o Item 10, escrevendo código seguro para exceção.

1
Don Neufeld

O contrato usual em OO é que os métodos de objeto realmente funcionam.

Então, como um corrolary, nunca retornar um objeto zumbi de um construtor/init.

Um zumbi não é funcional e pode estar faltando componentes internos. Apenas uma exceção de ponteiro nulo esperando para acontecer.

Eu fiz zumbis pela primeira vez no Objetivo C, há muitos anos.

Como todas as regras práticas, há uma "exceção".

É perfeitamente possível que uma interface específica possa ter um contrato que diga que existe um método "inicializar" que é permitido gerar uma exceção. Que um objeto na implementação dessa interface pode não responder corretamente a qualquer chamada, exceto os setters de propriedade, até que a inicialização seja chamada. Eu usei isso para drivers de dispositivo em um sistema operacional OO durante o processo de inicialização, e foi viável.

Em geral, você não deseja objetos zumbis. Em linguagens como Smalltalk com torne-se as coisas ficam um pouco confusas, mas o uso excessivo de tornou-se também é um estilo ruim. Become permite que um objeto mude para outro objeto in situ, portanto, não há necessidade de invólucro de envelope (Advanced C++) ou o padrão de estratégia (GOF).

1
Tim Williscroft

A pergunta do OP tem uma tag "agnóstica de linguagem" ... essa questão não pode ser respondida com segurança da mesma maneira para todos os idiomas/situações.

A seguinte hierarquia de classes do exemplo C # lança o construtor da classe B, pulando uma chamada imediata para o IDisposeable.Dispose da classe A ao sair do using da classe principal, pulando a disposição explícita dos recursos da classe A.

Se, por exemplo, a classe A tivesse criado um Socket em construção, conectado a um recurso de rede, tal provavelmente ainda seria o caso após o bloco using (uma anomalia 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

Eu estou apenas aprendendo o Objective C, então eu não posso falar da experiência, mas eu li sobre isso nos documentos da Apple.

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

Não só lhe dirá como lidar com a pergunta que você fez, mas também explica bem o problema.

0
Scott Swezey

O melhor conselho que tenho visto sobre as exceções é lançar uma exceção se, e somente se, a alternativa for a falha em atender a uma condição de postagem ou manter uma invariante.

Esse conselho substitui uma decisão subjetiva pouco clara (é uma boa ideia) com uma pergunta técnica e precisa baseada em decisões de design (condições invariáveis ​​e posteriores) que você já deveria ter feito.

Os construtores são apenas um caso particular, mas não especial, para esse conselho. Então, a pergunta se torna: quais invariantes uma classe deve ter? Os defensores de um método de inicialização separado, a ser chamado após a construção, estão sugerindo que a classe tem dois ou mais modo de operação, com um não modo após a construção e pelo menos um - pronto, inserido após a inicialização. Essa é uma complicação adicional, mas aceitável se a classe tiver vários modos de operação de qualquer maneira. É difícil ver como essa complicação vale a pena se a classe não tivesse modos de operação.

Observe que o envio da configuração para um método de inicialização separado não permite evitar exceções. As exceções que seu construtor pode ter lançado agora serão lançadas pelo método de inicialização. Todos os métodos úteis da sua classe terão que lançar exceções se forem chamados para um objeto não inicializado.

Note também que evitar a possibilidade de exceções serem lançadas por seu construtor é problemático, e em muitos casos impossível em muitas bibliotecas padrão. Isso ocorre porque os designers dessas bibliotecas acreditam que lançar exceções de construtores é uma boa idéia. Em particular, qualquer operação que tente adquirir um recurso não compartilhável ou finito (como alocar memória) pode falhar, e essa falha é normalmente indicada em idiomas e bibliotecas OO, lançando uma exceção.

0
Raedwald

Falando estritamente de um ponto de vista Java, sempre que você inicializar um construtor com valores ilegais, ele deve lançar uma exceção. Dessa forma, ele não é construído em um estado ruim.

0
scubabbl

Para mim, é uma decisão de design um tanto filosófica.

É muito bom ter instâncias que sejam válidas desde que existam, desde o tempo de execução. Para muitos casos não triviais, isso pode exigir exceções do coordenador, se uma alocação de memória/recurso não puder ser feita.

Algumas outras abordagens são o método init (), que vem com algumas questões próprias. Uma delas é garantir que o init () seja chamado.

Uma variante está usando uma abordagem preguiçosa para chamar automaticamente init () na primeira vez em que um acessador/mutador é chamado, mas isso requer que qualquer potencial chamador tenha que se preocupar com o objeto sendo válido. (Em oposição ao "existe, por isso é uma filosofia válida").

Eu já vi vários padrões de design propostos para lidar com esse problema também. Como ser capaz de criar um objeto inicial via ctor, mas ter que chamar init () para colocar as mãos em um objeto inicializado e contido com acessores/mutadores.

Cada abordagem tem seus altos e baixos; Eu usei todos esses com sucesso. Se você não criar objetos prontos para uso a partir do instante em que são criados, recomendo uma dose alta de afirmações ou exceções para garantir que os usuários não interajam antes do init ().

Adendo

Eu escrevi de uma perspectiva de programadores de C++. Eu também suponho que você está usando corretamente o idioma RAII para lidar com recursos sendo liberados quando exceções são lançadas.

0
nsanders

Usando fábricas ou métodos de fábrica para toda a criação de objetos, é possível evitar objetos inválidos sem gerar exceções de construtores. O método de criação deve retornar o objeto solicitado, se for capaz de criar um, ou null, se não for. Você perde um pouco de flexibilidade ao lidar com erros de construção no usuário de uma classe, porque retornar nulo não informa o que deu errado na criação do objeto. Mas também evita adicionar a complexidade de vários manipuladores de exceção toda vez que você solicita um objeto e o risco de capturar exceções que você não deve manipular.

0
Tegan Mulholland