【问题标题】:When is it right for a constructor to throw an exception?构造函数什么时候抛出异常合适?
【发布时间】:2010-09-09 19:26:07
【问题描述】:

构造函数什么时候抛出异常合适? (或者在 Objective C 的情况下:init'er 什么时候返回 nil 是正确的?)

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

【问题讨论】:

    标签: exception language-agnostic constructor


    【解决方案1】:

    构造函数的工作是使对象进入可用状态。在这方面基本上有两种思想流派。

    一组赞成两阶段建设。构造函数只是将对象带入休眠状态,在这种状态下它拒绝做任何工作。还有一个额外的函数可以进行实际的初始化。

    我一直不明白这种方法背后的原因。我坚定地支持一阶段构造,其中对象在构造后完全初始化并可用。

    如果单阶段构造函数未能完全初始化对象,则应该抛出异常。如果对象不能初始化,就一定不允许存在,所以构造函数必须抛出。

    【讨论】:

    • 具有一级构造函数的类不容易通过子类化用于单元测试。
    • 两阶段构造适用于异常无法正常工作或未实施的环境。 MFC 使用两阶段构造,因为最初编写它时,Visual C++ 没有 C++ 异常工作。 Windows CE 直到 v4.0 和 MFC 8.0 才出现 C++ 异常。
    • @EricSchaefer:对于单元测试,我觉得最好模拟依赖,而不是使用子类化。
    • 当一组对象需要相互链接才能正常运行时,可能需要两个阶段的构造函数。两阶段方法对于依赖注入很有用。
    • @Patrick:不知道你的意思——如果你执行“new Foo”并且 Foo 的构造函数抛出,该语言将回收内存。如果您在构造函数中分配内存并且除了析构函数之外没有提供释放它的方法,那么如果您稍后在 cnostructor 中抛出,该语言将不会回收它。但是,无论如何,您应该立即将每个分配包装在一个 RAII 对象中。
    【解决方案2】:

    Eric Lippert says有4种异常。

    • 致命异常不是您的错,您无法阻止它们,也无法明智地清除它们。
    • 愚蠢的异常是您自己的错误,您本可以阻止它们,因此它们是您的代码中的错误。
    • 令人烦恼的异常是不幸的设计决策的结果。令人烦恼的异常是在完全非异常的情况下抛出的,因此必须始终捕获和处理。
    • 最后,外生异常看起来有点像令人烦恼的异常,只是它们不是不幸的设计选择的结果。相反,它们是杂乱的外部现实影响你漂亮、清晰的程序逻辑的结果。

    你的构造函数不应该自己抛出一个致命的异常,但是它执行的代码可能会导致一个致命的异常。像“内存不足”这样的事情不是你可以控制的,但是如果它发生在构造函数中,嘿,它会发生。

    在您的任何代码中都不应出现愚蠢的异常,因此它们是正确的。

    构造函数不应该抛出令人烦恼的异常(例如Int32.Parse()),因为它们没有非异常情况。

    最后,应避免外生异常,但如果您在构造函数中做一些依赖于外部环境(如网络或文件系统)的事情,则抛出异常是合适的。

    参考链接:https://blogs.msdn.microsoft.com/ericlippert/2008/09/10/vexing-exceptions/

    【讨论】:

    • 那么 Argument[Null]Exception 在这个方案中的位置在哪里?这是一个愚蠢的异常,所以不应该抛出?还是这是一个致命的异常,因此可以抛出?
    • 我现在意识到我没有包含指向原始文章的链接,blogs.msdn.com/b/ericlippert/archive/2008/09/10/… ArgumentException/ArgumentNullException 之类的东西是愚蠢的:只是调用代码中的简单错误。他说“修复你的代码,使其永远不会触发愚蠢的异常——‘索引超出范围’的异常不应该在生产代码中发生。”
    • @alastairs:你绝对应该throw ArgumentExceptions,因为唯一的选择是假装参数是有效的,而实际上它们是无效的。 (导致 NullReferenceExceptions,或者可能更糟糕的事情。)但就像 Jacob 所说,您应该永远捕获它们。
    • 肯定会抛出ArgumentException / ArgumentNullException / ArgumentoutOfRangeException 异常来保护构造函数或方法中的参数,这对于public 或protected 的任何构造函数或方法来说只是普通的常识防御性编程?毕竟,您无法确保所有调用代码都将传递有效参数。但是,对于您可以控制的private 和internal,则可能不需要它们,因为程序员ARE 控制着调用代码。
    • @Dib 是的。你同意 Joren 和我的 cmets 关于愚蠢的例外。绝对扔掉它们;永远不要抓住他们。
    【解决方案3】:

    通常将对象初始化从构造中分离出来并没有什么好处。 RAII 是正确的,对构造函数的成功调用应该导致完全初始化的活动对象或者应该失败,并且任何代码路径中任何点的 ALL 失败都应该总是抛出异常。使用单独的 init() 方法除了在某种程度上增加了复杂性之外,您什么也得不到。 ctor 合约应该是它返回一个功能有效的对象,或者它自己清理并抛出。

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

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

    【讨论】:

    • “仅在需要时构建”的反例。如果在循环体中需要它,则在此声明它会在范围退出时将其销毁,这可能会造成浪费。具有单独的构造(在更大范围内)和初始化/重新初始化(在循环内)允许为后续迭代重用资源,而不是完全清理并不得不多次重建它们。
    • 如果在初始化中有重要的逻辑要完成,并且您希望有各种可以重用该逻辑的 c'tors,那么从 DRY 的角度来看,使用单独的 init() 是正确的,这是 KISS 的一部分。在构建时仍然可以“在幕后”进行初始化。
    • 为调用构造函数的对象创建工厂方法,然后调用init方法
    【解决方案4】:

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

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

    【讨论】:

    • 我想说的情况正好相反——如果我们不想要部分创建的对象,构造函数应该在出现问题时抛出——这样调用者就会知道出了点问题。
    • @TimBezhashvyly:因为它与现实不符。在现实世界中,有些故障是无法预测的,只能检测到。
    • @BenVoigt 这不是借口。
    • @TimBezhashvyly:从不这个词只有在您声称该规则涵盖所有场景时才使用。这条规则没有。假装它是没有用的。 “没用”是网站批准的实际投反对票的原因之一......而且您对人们选择不投赞成票感到不安,这是一个具有近乎无限正当理由的决定。
    • @BlairConrad 在这种情况下,构造函数是私有的。静态工厂方法有效地成为构造函数。没有人说我们不应该在构造的时候抛出异常......我们只是在争论开发人员不应该从构造函数中发起异常。工厂方法还能够处理从构造函数中抛出的异常,在不幸的情况下会导致这样的事情发生,并从中恢复。异常不必是应用程序的结束。
    【解决方案5】:

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

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

    【讨论】:

      【解决方案6】:

      构造函数在无法完成该对象的构造时应该抛出异常。

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

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

      【讨论】:

        【解决方案7】:

        它总是很狡猾,尤其是当你在构造函数中分配资源时;根据您的语言,不会调用析构函数,因此您需要手动清理。这取决于对象的生命周期在您的语言中是如何开始的。

        我真正做到这一点的唯一一次是某处存在安全问题,这意味着不应该创建对象,而不是不能创建对象。

        【讨论】:

        • 我不知道为什么有人对此评价不高......对我来说,在构造函数中抛出异常时考虑这似乎是一件好事。我敢肯定有不止几个项目存在内存泄漏,因为在抛出异常后没有调用析构函数。
        • 我是投票否决的人。在任何语言中,单一责任原则都适用:如果一个对象负责管理资源,它不应该做任何其他事情,因此它的构造函数抛出的唯一情况是它无法获取资源。我知道的每一种语言都会可靠地破坏完全构造的对象,因此这些资源将得到处理。如果您希望一个对象手动管理多个资源,您只会遇到麻烦,这只是一个错误。问题不在于抛出构造函数。
        【解决方案8】:

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

        【讨论】:

        • @cgreen 请重新检查这些帖子上的日期。该博客条目来自 2008 年 12 月 3 日——上述帖子来自 2008 年 9 月 16 日——几乎该博客帖子存在三个月前。
        • @cgreeno:这篇博文是在这篇博文前三个月发布的,怎么能被复制?
        • @JeffAtwood 在发表评论之前,我仔细检查了日期。有时会更改博客帖子的日期以使它们看起来更相关。但是,我将删除评论以避免任何争议,因为我可能错了并且没有证据。
        【解决方案9】:

        请参阅 C++ 常见问题解答部分 17.2 和 17.4。

        一般来说,我发现如果编写构造函数以便它们不会失败,那么代码更容易移植和维护结果,并且可能失败的代码被放置在一个单独的方法中,该方法返回错误代码并将对象留在惰性状态。

        【讨论】:

          【解决方案10】:

          如果您正在编写 UI 控件(ASPX、WinForms、WPF...),则应避免在构造函数中引发异常,因为设计器 (Visual Studio) 在创建控件时无法处理它们。了解您的控件生命周期(控件事件)并尽可能使用延迟初始化。

          【讨论】:

            【解决方案11】:

            请注意,如果您在初始化程序中抛出异常,如果任何代码使用[[[MyObj alloc] init] autorelease] 模式,您最终会泄漏,因为异常会跳过自动释放。

            看到这个问题:

            How do you prevent leaks when raising an exception in init?

            【讨论】:

            • 这似乎是特定于语言的。您能否举一个这种模式很常见的语言示例(我猜代码看起来像 TCL 或 Objective-C)。
            【解决方案12】:

            如果您无法创建有效对象,您绝对应该从构造函数中抛出异常。这允许您在类中提供适当的不变量。

            在实践中,您可能必须非常小心。请记住,在 C++ 中,不会调用析构函数,因此如果在分配资源后抛出异常,则需要非常小心地正确处理!

            This page 对 C++ 中的情况进行了彻底的讨论。

            【讨论】:

              【解决方案13】:

              如果无法在构造函数中初始化对象,则抛出异常,例如非法参数。

              作为一般经验法则,应始终尽快抛出异常,因为当问题的根源更接近发出错误信号的方法时,它会使调试更容易。

              【讨论】:

                【解决方案14】:

                在构造过程中抛出异常是使代码更加复杂的好方法。看似简单的事情突然变得困难了。例如,假设您有一个堆栈。你如何弹出堆栈并返回顶部值?好吧,如果堆栈中的对象可以在它们的构造函数中抛出(构造临时返回给调用者),你不能保证你不会丢失数据(递减堆栈指针,使用 value in 的复制构造函数构造返回值堆栈,它会抛出,现在有一个堆栈刚刚丢失了一个项目)!这就是 std::stack::pop 不返回值的原因,你必须调用 std::stack::top。

                这个问题很好描述here,检查第10条,编写异常安全代码。

                【讨论】:

                • 这可能是确保复制构造函数不能抛出而不是普通构造函数的绝佳论据。
                【解决方案15】:

                OO 中通常的约定是对象方法确实起作用。

                因此,作为推论,永远不要从构造函数/init 中返回僵尸对象。

                僵尸不起作用,可能缺少内部组件。只是等待发生的空指针异常。

                多年前,我第一次在 Objective C 中制作僵尸。

                就像所有的经验法则一样,有一个“例外”。

                一个特定的接口完全有可能有一个合同说 存在一种方法“initialize”,它允许抛出异常。 实现此接口的对象可能无法正确响应除属性设置器之外的任何调用,直到调用了 initialize。我在启动过程中将它用于OO操作系统中的设备驱动程序,并且它是可行的。

                一般来说,您不想要僵尸对象。在像 Smalltalk 这样带有 become 的语言中,事情会变得有点混乱,但过度使用 become 也是不好的风格。 Become 让一个对象在原地变为另一个对象,因此不需要信封包装器(高级 C++)或策略模式(GOF)。

                【讨论】:

                  【解决方案16】:

                  我无法解决 Objective-C 中的最佳实践,但在 C++ 中,构造函数抛出异常是可以的。特别是因为没有其他方法可以确保在不调用 isOK() 方法的情况下报告构造中遇到的异常情况。

                  函数 try 块特性是专门为支持构造函数成员初始化失败而设计的(尽管它也可用于常规函数)。这是修改或丰富将抛出的异常信息的唯一方法。但由于其最初的设计目的(在构造函数中使用),它不允许异常被空的 catch() 子句吞噬。

                  【讨论】:

                    【解决方案17】:

                    是的,如果构造函数未能构建其内部部分之一,则可以选择 - 由其负责抛出(并以某种语言声明)explicit exception ,在构造函数文档中已适当注明。

                    这不是唯一的选择:它可以完成构造函数并构建一个对象,但方法 'isCoherent()' 返回 false,以便能够发出不连贯状态的信号(在某些情况下可能更可取) , 以免因异常而残酷地中断执行工作流)
                    警告:正如 EricSchaefer 在他的评论中所说,这会给单元测试带来一些复杂性(由于触发它的条件,抛出会增加函数的 cyclomatic complexity)

                    如果由于调用者而失败(如调用者提供的空参数,被调用的构造函数需要一个非空参数),构造函数无论如何都会抛出未经检查的运行时异常。

                    【讨论】:

                      【解决方案18】:

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

                      我之前曾根据要求从不使用异常并且只在初始化程序上使用错误代码的编码标准工作过,因为开发人员一直被处理异常的语言所困扰。没有垃圾收集的语言将处理堆和堆栈非常不同,这对于非 RAII 对象可能很重要。尽管团队决定保持一致很重要,但他们默认知道是否需要在构造函数之后调用初始化程序。所有方法(包括构造函数)也应该详细记录它们可以抛出哪些异常,以便调用者知道如何处理它们。

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

                      • 您对异常的语言支持不是很好。
                      • 您有一个紧迫的设计理由仍然使用new 和delete
                      • 您的初始化是处理器密集型的,应该与创建对象的线程异步运行。
                      • 您正在创建一个 DLL,该 DLL 可能会在其与使用不同语言的应用程序的接口之外引发异常。在这种情况下,可能不是不抛出异常的问题,而是确保它们在公共接口之前被捕获。 (您可以在 C# 中捕获 C++ 异常,但需要跳过一些障碍。)
                      • 静态构造函数 (C#)

                      【讨论】:

                        【解决方案19】:

                        OP 的问题有一个“与语言无关”的标签...对于所有语言/情况,这个问题不能以同样的方式安全地回答。

                        以下 C# 示例的类层次结构抛出类 B 的构造函数,在退出主类的 using 时跳过对类 A 的 IDisposeable.Dispose 的立即调用,跳过对 A 类资源的显式处置。

                        例如,如果 A 类在构造时创建了一个 Socket,连接到网络资源,那么在 using 块之后可能仍然是这种情况(一个相对隐藏的异常)。

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

                        【讨论】:

                          【解决方案20】:

                          严格来说,从 Java 的角度来看,任何时候你用非法值初始化一个构造函数,它都应该抛出一个异常。这样它就不会在糟糕的状态下构建。

                          【讨论】:

                            【解决方案21】:

                            对我来说,这是一个有点哲学的设计决定。

                            从 ctor 时间开始,拥有只要存在就有效的实例非常好。对于许多重要的情况,如果无法进行内存/资源分配,这可能需要从 ctor 抛出异常。

                            其他一些方法是 init() 方法,它本身就有一些问题。其中之一是确保 init() 实际被调用。

                            一个变体使用惰性方法在第一次调用访问器/修改器时自动调用 init(),但这要求任何潜在的调用者都必须担心对象是否有效。 (与“它存在,因此它是有效的哲学”相反)。

                            我也看到了各种建议的设计模式来处理这个问题。例如能够通过 ctor 创建一个初始对象,但必须调用 init() 才能获得一个包含访问器/突变器的初始化对象。

                            每种方法都有其起伏;我已经成功地使用了所有这些。如果您没有从创建它们的那一刻起制作即用型对象,那么我建议使用大量断言或异常以确保用户在 init() 之前不会进行交互。

                            附录

                            我是从 C++ 程序员的角度写的。我还假设您正确使用 RAII 习惯用法来处理引发异常时释放的资源。

                            【讨论】:

                              【解决方案22】:

                              对所有对象创建使用工厂或工厂方法,可以避免无效对象,而不会从构造函数中抛出异常。如果创建方法能够创建一个对象,则创建方法应返回所请求的对象,否则返回 null。您在处理类用户的构造错误时失去了一点灵活性,因为返回 null 并不能告诉您创建对象时出了什么问题。但它也避免了每次请求对象时增加多个异常处理程序的复杂性,以及捕获不应处理的异常的风险。

                              【讨论】:

                                【解决方案23】:

                                我见过的关于异常的最佳建议是,当且仅当替代方案是未能满足后置条件或保持不变量时才抛出异常。

                                该建议用一个基于您应该已经做出的设计决策(不变和后置条件)的技术性、精确性问题代替了一个不清楚的主观决定(这是一个好主意)。

                                构造函数只是该建议的一种特殊情况,但并非特殊情况。那么问题就变成了,一个类应该有哪些不变量?主张在构造后调用单独的初始化方法,建议该类具有两个或多个操作模式,构造后有一个未就绪模式,并且至少有一个ready 模式,初始化后进入。这是一个额外的复杂性,但如果该类无论如何都有多种操作模式,则可以接受。如果该类没有操作模式,那么很难看出这种复杂性是多么值得。

                                请注意,将设置推送到单独的初始化方法中并不能避免引发异常。您的构造函数可能抛出的异常现在将由初始化方法抛出。如果为未初始化的对象调用类中所有有用的方法,则它们都必须抛出异常。

                                还要注意,避免构造函数抛出异常的可能性很麻烦,而且在许多标准库中不可能。这是因为这些库的设计者认为从构造函数中抛出异常是个好主意。特别是,任何试图获取不可共享或有限资源(例如分配内存)的操作都可能失败,而这种失败通常在 OO 语言和库中通过抛出异常来指示。

                                【讨论】:

                                  猜你喜欢
                                  • 1970-01-01
                                  • 2022-11-19
                                  • 1970-01-01
                                  • 1970-01-01
                                  • 2019-04-01
                                  • 2011-11-04
                                  • 1970-01-01
                                  • 1970-01-01
                                  • 1970-01-01
                                  相关资源
                                  最近更新 更多