当前位置:  开发笔记 > 运维 > 正文

什么时候构造函数抛出异常?

如何解决《什么时候构造函数抛出异常?》经验,为你挑选了7个好方法。

什么时候构造函数抛出异常?(或者在目标C的情况下:什么时候初始化者返回nil是正确的?)

在我看来,如果对象不完整,构造函数应该失败 - 因此拒绝创建对象.即,构造函数应该与其调用者签订合同,以提供一个功能和工作对象,可以在其上有意义地调用方法?这合理吗?



1> Sebastian Re..:

构造函数的工作是将对象置于可用状态.基本上有两种思想流派.

一组赞成两阶段建设.构造函数只是将对象置于睡眠状态,在该状态下它拒绝做任何工作.还有一个额外的功能可以进行实际的初始化.

我从来没有理解这种方法背后的原因.我坚定地支持一阶段建设,其中对象在施工后完全初始化并可用.

如果一阶段构造函数无法完全初始化对象,则应该抛出它们.如果无法初始化对象,则不能允许它存在,因此构造函数必须抛出.


@EricSchaefer:对于单元测试,我觉得最好是模拟依赖,而不是使用子类.
两阶段构造适用于异常无法正常工作或未实现的环境.MFC使用两阶段构造,因为在最初编写时,Visual C++没有C++异常工作.Windows CE在v4.0和MFC 8.0之前没有获得C++异常.
具有一个阶段构造函数的类不能通过子类化在单元测试中轻松使用.
当一组对象需要彼此链接以正常工作时,可能需要两个阶段构造器.两阶段方法对依赖注入很有用.
@Patrick:不确定你的意思 - 如果你做'新Foo'并且Foo的构造函数抛出,语言将回收内存.如果你在构造函数中分配内存并且除了析构函数之外没有提供释放它的方法,那么如果你稍后在cnostructor中抛出它,语言将不会回收它.但是,你应该立即将RAII对象中的每个分配包装起来.
避免不完整的对象,这就是让构造函数抛出的重点.如果获取一个完整的对象需要一个可以抛出的操作,那么你可以拥有抛出或不完整对象的构造函数.你发布的文章基本上是胡说八道.这个故事的真正寓意是,如果您想要安全性,则不要将安全检查与操作分开.

2> Jacob Krall..:

Eric Lippert说有4种例外.

致命的例外不是你的错,你不能阻止他们,你不能明智地清理它们.

Boneheaded异常是你自己的错误,你可以防止它们,因此它们是你的代码中的错误.

不幸的例外是不幸的设计决策的结果.在完全非特殊情况下抛出异常情况,因此必须始终抓住并处理.

最后,外生异常似乎有点像烦恼的异常,除了它们不是不幸的设计选择的结果.相反,它们是不整洁的外部现实影响你美丽,清晰的程序逻辑的结果.

您的构造函数不应该自己抛出致命异常,但它执行的代码可能会导致致命的异常.像"内存不足"这样的东西不是你可以控制的东西,但如果它出现在构造函数中,嘿,它就会发生.

任何代码都不应该出现斩首异常,所以它们就是正确的.

Int32.Parse()构造函数不应抛出Vexing异常(示例是),因为它们没有非特殊情况.

最后,应避免使用外部异常,但如果您在构造函数中执行某些依赖于外部环境(如网络或文件系统)的操作,则抛出异常是合适的.


@alastairs:你绝对应该*抛出ArgumentExceptions,因为唯一的选择是假装参数在它们不存在时是有效的.(导致NullReferenceExceptions,或者可能更糟糕的事情.)但是就像雅各布说的那样,你应该**永远不要抓住它们.
我现在意识到我没有包含原始文章的链接,http://blogs.msdn.com/b/ericlippert/archive/2008/09/10/vexing-exceptions.aspx像ArgumentException/ArgumentNullException这样的东西都是骨头的:只是调用代码中的普通错误.他说:"修复你的代码,以便它永远不会触发一个愚蠢的例外 - 在生产代码中永远不会发生'超出范围的索引'异常."
那么Argument [Null] Exception在这个方案中的位置是什么?这是一个愚蠢的例外,所以不应该抛出?或者这是一个致命的例外,因此可以抛出?
@Dib是的.你同意Joren和我对愚蠢异常的评论.绝对扔掉它们; 从不抓住他们.

3> 小智..:

一般没什么可通过从建筑离婚对象的初始化获得.RAII是正确的,对构造函数的成功调用应该导致完全初始化的活动对象,否则它应该失败,并且任何代码路径中任何点的所有失败都应该抛出异常.除了某种程度上的额外复杂性之外,您不会通过使用单独的init()方法获得任何收益.ctor契约应该是它返回一个功能有效的对象,或者它自己清理后抛出.

考虑一下,如果你实现一个单独的init方法,你仍然需要调用它.它仍然有可能抛出异常,它们仍然必须被处理,它们实际上总是必须在构造函数之后立即被调用,除了现在你有4个可能的对象状态而不是2(IE,构造,初始化,未初始化,并失败vs只有效和不存在).

无论如何,我在25年的OO开发案例中遇到过,似乎单独的init方法"解决了一些问题"是设计缺陷.如果您现在不需要对象,那么您现在不应该构建它,如果您现在需要它,那么您需要初始化它.KISS应该始终遵循的原则,以及任何接口的行为,状态和API应该反映对象做什么的简单概念,而不是它如何做,客户端代码甚至不应该知道对象有任何种类需要初始化的内部状态,因此模式后的init违反了这个原则.



4> Michael L Pe..:

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

如果需要在构造期间验证某些内容,请将构造函数设为私有并定义公共静态工厂方法.如果某些内容无效,该方法可以抛出.但如果一切都检出,它会调用构造函数,保证不会抛出.


我会说相反的情况 - 如果我们不想要部分创建的对象,那么构造函数_should_会在出现问题时抛出 - 这样调用者就会知道出了什么问题.

5> Matt Dillard..:

只要构造函数正确地清理自身,构造函数就抛出异常是合理的。如果遵循RAII范式(资源获取就是初始化),那么构造函数执行有意义的工作很普遍的。一个编写良好的构造函数如果无法完全初始化,则会依次清除。


@cgreen请重新检查这些帖子的日期。该博客文章的发布日期为2008年12月3日-以上帖子的发布日期为2008年9月16日-差不多*该博客文章存在之前三个月*。

6> 小智..:

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

例如,如果构造函数应该分配1024 KB的ram,并且它不能这样做,它应该抛出一个异常,这样构造函数的调用者知道对象没有准备好被使用并且有一个错误某处需要修复.

半初始化和半死的对象只会导致问题和问题,因为调用者无法知道.当出现问题时,我宁愿让构造函数抛出错误,而不是依赖编程来运行对isOK()函数的调用,该函数返回true或false.



7> cwharris..:

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

注意:此答案假定使用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 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方法本身之一。

推荐阅读
TXCWB_523
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有