构造函数何时抛出异常是正确的?(或者在Objective C的情况下:什么情况下init ` er才应该返回nil?)

在我看来,如果对象不完整,构造函数应该失败——因此拒绝创建对象。也就是说,构造函数应该与它的调用者有一个合同,以提供一个函数和工作对象,在哪些方法可以被有意义地调用?这合理吗?


当前回答

构造函数抛出异常是合理的,只要它正确地清理了自己。如果您遵循RAII范式(资源获取即初始化),那么构造函数通常会做有意义的工作;如果构造函数不能完全初始化,那么编写良好的构造函数将自行清理。

其他回答

据我所知,没有人能提出一个相当明显的解决方案,同时体现了一阶段和两阶段结构的优点。

注意:这个答案假设是c#,但是这些原则可以应用于大多数语言。

一、两者的好处:

单程

一级构造通过防止对象以无效状态存在而使我们受益,从而防止了各种错误的状态管理和随之而来的所有错误。然而,这让我们中的一些人感到奇怪,因为我们不希望我们的构造函数抛出异常,而当初始化参数无效时,有时我们需要这样做。

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

两阶段验证法

两阶段构造的好处是允许在构造函数外部执行验证,因此避免了在构造函数内部抛出异常的需要。然而,这给我们留下了“无效”实例,这意味着我们必须跟踪和管理实例的状态,或者在堆分配后立即丢弃它。这就引出了一个问题:为什么我们要在一个我们最终甚至不使用的对象上执行堆分配,从而进行内存收集?

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

通过私有构造函数实现单阶段

那么,我们如何在构造函数中保持异常,并防止自己对立即被丢弃的对象执行堆分配呢?这是非常基本的:我们将构造函数设为私有,并通过指定的静态方法创建实例来执行实例化,因此只有在验证之后才能进行堆分配。

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

通过私有构造函数异步单级

除了前面提到的验证和堆分配预防的好处之外,前面的方法还为我们提供了另一个漂亮的优点:异步支持。这在处理多阶段身份验证时非常方便,例如在使用API之前需要检索承载令牌。这样,您就不会得到一个无效的“签出”API客户端,相反,如果在尝试执行请求时收到授权错误,您可以简单地重新创建API客户端。

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

根据我的经验,这种方法的缺点很少。

通常,使用这种方法意味着您不能再将该类用作DTO,因为在没有公共默认构造函数的情况下反序列化到对象是困难的。但是,如果您使用对象作为DTO,则不应该真正验证对象本身,而应该在尝试使用对象上的值时使它们无效,因为从技术上讲,这些值对于DTO来说并不是“无效”的。

这也意味着当您需要允许IOC容器创建对象时,您将最终创建工厂方法或类,否则容器将不知道如何实例化对象。然而,在很多情况下,工厂方法本身就是Create方法之一。

当构造函数无法完成所述对象的构造时,它应该抛出异常。

例如,如果构造函数应该分配1024 KB的ram,但它没有这样做,它应该抛出一个异常,这样构造函数的调用者就知道对象还没有准备好使用,并且在某个地方存在需要修复的错误。

半初始化和半死亡的对象只会引起问题和问题,因为调用者确实没有办法知道。我宁愿让我的构造函数在出错时抛出一个错误,而不是不得不依赖编程来运行返回true或false的isOK()函数的调用。

由于部分创建的类可能导致的所有麻烦,我认为永远不会。

如果需要在构造过程中验证某些内容,请将构造函数设为私有并定义一个公共静态工厂方法。如果某些东西无效,该方法可以抛出。但是如果一切都检查出来了,它就调用构造函数,保证不会抛出。

这总是很危险的,特别是当你在构造函数中分配资源时;根据你的语言,析构函数不会被调用,所以你需要手动清理。这取决于在您的语言中对象的生命周期何时开始。

我真正这么做的唯一一次是在某个地方出现了安全问题,这意味着对象不应该,而不是不能,被创建。

我不确定是否有答案可以完全与语言无关。有些语言处理异常和内存管理的方式不同。

I've worked before under coding standards requiring exceptions never be used and only error codes on initializers, because developers had been burned by the language poorly handling exceptions. Languages without garbage collection will handle heap and stack very differently, which may matter for non RAII objects. It is important though that a team decide to be consistent so they know by default if they need to call initializers after constructors. All methods (including constructors) should also be well documented as to what exceptions they can throw, so callers know how to handle them.

我通常支持单阶段构造,因为很容易忘记初始化对象,但也有很多例外。

Your language support for exceptions isn't very good. You have a pressing design reason to still use new and delete Your initialization is processor intensive and should run async to the thread that created the object. You are creating a DLL that may be throwing exceptions outside it's interface to an application using a different language. In this case it may not be so much an issue of not throwing exceptions, but making sure they are caught before the public interface. (You can catch C++ exceptions in C#, but there are hoops to jump through.) Static constructors (C#)