何时构造函数抛出exception?

何时构造函数抛出exception? (或者在目标C的情况下:初始化程序何时返回零?

在我看来,一个构造函数应该失败 – 因此拒绝创build一个对象 – 如果对象不完整。 也就是说,构造函数应该和调用者有一个契约来提供一个function和工作对象,在这个对象上可以有效地调用方法? 这是合理的吗?

构造函数的工作是使对象进入可用状态。 基本上有两个学派的思想。

一组赞成两阶段build设。 构造函数只是将对象带入睡眠状态,拒绝做任何工作。 还有一个额外的function,做实际的初始化。

我从来没有理解这种方法背后的推理。 我坚决支持一个阶段的build设,其中的对象完全初始化和build设后可用。

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

Eric Lippert说有四种例外。

  • 致命的例外不是你的错,你不能阻止他们,你不能明智地从他们身上清理。
  • Boneheadedexception是你自己的故障,你可以阻止他们,因此他们是你的代码中的错误。
  • 令人烦恼的例外是不幸的devise决定的结果。 令人烦恼的例外情况在完全非例外的情况下被抛出,因此必须一直被抓住和处理。
  • 最后,外生的例外似乎有点像令人烦恼的例外,只是它们不是不幸的deviseselect的结果。 相反,他们是凌乱的外部现实冲击你美丽,清晰的程序逻辑的结果。

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

Boneheadedexception不应该出现在你的任何代码中,所以他们是正确的。

Vexingexception(示例是Int32.Parse() )不应该由构造函数抛出,因为它们没有非常Int32.Parse()情况。

最后,应该避免外生的exception,但是如果你在构造函数中依赖外部环境(如networking或文件系统)做某些事情,那么抛出一个exception是适当的。

离开对象的初始化, 通常没有什么可以获得的。 RAII是正确的,对构造函数的成功调用应该导致完全初始化的活动对象,否则它应该失败,并且任何代码path中的任何点的所有失败都应该总是抛出exception。 通过使用单独的init()方法,除了在某个级别上增加复杂性外,您什么也得不到。 ctor契约应该是它返回一个function有效的对象,或者它自己清理并抛出。

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

无论如何,我已经在25年的面向对象开发案例中遇到过,似乎是一个单独的初始化方法将“解决一些问题”的devise缺陷。 如果你现在不需要一个对象,那么现在你不应该构造它,如果你现在需要它,那么你需要它初始化。 KISS应该总是遵循的原则,以及一个简单的概念,即任何接口的行为,状态和API都应该反映对象所做的事情,而不是如何做,客户端代码甚至不应该意识到对象有任何types需要初始化的内部状态,因此init之后的模式违反了这个原则。

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

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

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

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

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

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

我唯一真正做到的地方就是当某个地方出现安全问题,这意味着这个对象不应该被创build,而不是被创build。

就我所知,没有人能提出一个相当明显的解决scheme,体现了一,二阶段build设的最佳体现。

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

首先,两者的好处:

单级

一阶段的构build是通过防止对象处于无效状态,从而避免各种错误的状态pipe理以及随之而来的所有错误。 然而,这让我们感到有些奇怪,因为我们不希望我们的构造函数抛出exception,有时这就是我们在初始化参数无效时所要做的。

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

两阶段validation方法

两阶段构造使得我们的validation可以在构造函数外部执行,从而避免了在构造函数中抛出exception的需要。 但是,它给我们留下了“无效”的实例,这意味着我们必须跟踪和pipe理实例,或者在堆分配之后立即抛弃它。 它提出了一个问题:为什么我们要对一个对象进行堆分配和内存收集?

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

单阶段通过私人构造函数

那么我们怎样才能把exception排除在构造函数之外呢,并且阻止我们对将被立即抛弃的对象执行堆分配? 这是非常基本的:我们使构造函数是私有的,并通过静态方法创build实例,该静态方法被指定为执行实例化,因此只有validation之后进行堆分配。

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

asynchronous单阶段通过私人构造函数

除了上述validation和堆分配预防的好处之外,以前的方法为我们提供了另一个很好的优势:asynchronous支持。 这在处理多阶段身份validation时非常方便,比如在使用API​​之前需要检索不记名令牌。 这样,您就不会收到无效的“已注销”API客户端,而只需在尝试执行请求时收到授权错误,就可以简单地重新创buildAPI客户端。

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

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

一般来说,使用这种方法意味着你不能再使用这个类作为DTO,因为反序列化到没有公共默认构造函数的对象是很难的。 但是,如果将对象用作DTO,则不应该真正validation对象本身,而是在尝试使用对象时对对象的值进行无效化处理,因为从技术上讲,这些值不会“无效”到DTO。

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

参见C ++ FAQ第17.2和17.4节。

通常情况下,我发现如果编写构造函数不会失败,那么更易于移植和维护结果的代码,并且可能失败的代码放在一个单独的方法中,该方法返回一个错误代码,并使对象处于惰性状态。

一个构造函数抛出一个exception,只要它能自行清理它是合理的。 如果你遵循RAII范式(Resource Acquisition Is Initialization),那么构造函数做一些有意义的工作很常见的。 如果不能完全初始化,一个写得很好的构造函数会自行清理。

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

请注意,如果在初始化程序中抛出exception,如果任何代码使用[[[MyObj alloc] init] autorelease]模式,则最终会发生泄漏,因为exception将跳过autorelease。

看到这个问题:

在init中引发exception时如何防止泄漏?

如果您无法创build有效的对象,则绝对应该从构造函数中抛出一个exception。 这使您可以在课堂上提供适当的不variables。

在实践中,你可能要非常小心。 请记住,在C ++中,析构函数不会被调用,所以如果在分配资源之后抛出,则需要非常小心地处理它。

这个页面详细讨论了C ++的情况。

如果您无法在构造函数中初始化该对象,则抛出exception,一个示例是非法参数。

作为一般的经验法则,总是应该尽可能地抛出exception,因为当问题的源头更接近于某种方法发出错误的信号时,它会使debugging更容易。

在构造过程中抛出一个exception是使代码更加复杂的好方法。 似乎简单的事情突然变得困难。 例如,假设你有一个堆栈。 你如何popup堆栈并返回最高值? 那么,如果堆栈中的对象可以抛出构造函数(构造临时返回给调用者),则不能保证不会丢失数据(减less堆栈指针,使用值的拷贝构造函数构造返回值堆栈,抛出,现在有一个堆栈,只是失去了一个项目)! 这就是为什么std :: stack :: pop没有返回值,你必须调用std :: stack :: top。

这个问题在这里很好的描述,检查第10项,编写exception安全的代码。

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

所以作为一个佐证,永远不会返回一个构造函数/ init的僵尸对象。

僵尸不起作用,可能缺less内部组件。 只是一个空指针exception等待发生。

我多年前首先在Objective C中制作僵尸。

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

一个特定的接口完全有可能有一个契约,说有一个方法“初始化”允许exception。 实现此接口的对象可能不会正确地响应除了属性设置器之外的任何调用,直到初始化被调用。 在启动过程中,我将其用于OO操作系统中的设备驱动程序,并且可行。

一般来说,你不需要僵尸对象。 在像Smalltalk这样的语言中会变得有点泡沫,但过度使用也是不好的风格。 成为让一个对象就地变成另一个对象,所以不需要封套包装(Advanced C ++)或策略模式(GOF)。

我不能解决Objective-C中的最佳实践,但是在C ++中,构造函数抛出exception是可以的。 特别是因为没有其他方法来确保在施工中遇到exception情况,而不诉诸于isOK()方法。

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

是的,如果构造函数未能构build其内部部分之一,则可以 – select – 它负责抛出(以及以某种语言声明) 明确的exception ,在构造函数文档中正确注明。

这不是唯一的select:它可以完成构造函数并构build一个对象,但是使用isCoherent()方法返回false,以便能够发出不连贯的状态(在某些情况下,这可能更可取避免由于例外而导致的执行工作stream程的残酷中断)
警告:正如EricSchaefer在他的评论中所说的,这会给unit testing带来一些复杂性(由于触发条件的缘故,抛出会增加函数的圈复杂度 )

如果由于调用者而失败(就像调用者提供的一个null参数,被调用的构造函数需要一个非null参数),构造函数将抛出一个未经检查的运行时exception。

严格地说,从Java的angular度来看,任何时候你初始化一个非法值的构造函数,它应该抛出一个exception。 这样它不会被build立在一个糟糕的状态。

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

从存在的时间起,只要存在有效的实例,就非常好。 对于许多非平凡的情况,如果不能进行内存/资源分配,这可能需要从ctor抛出exception。

其他一些方法是init()方法,它自带了一些问题。 其中之一是确保init()实际上被调用。

一个变体是使用懒惰的方法来自动调用init()函数,第一次调用accessor / mutator,但是这需要任何潜在的调用者必须担心该对象是有效的。 (相对于“它存在,因此它是有效的哲学”)。

我也看到了各种提出的devise模式来处理这个问题。 比如能够通过ctor创build一个初始对象,但是必须调用init()来获得一个包含初始化对象的accesors / mutators。

每种方法都有其起伏; 我已经成功地使用了所有这些。 如果您不立即创build即用对象,那么我build议使用大量的断言或exception来确保用户在init()之前不进行交互。

附录

我从C ++程序员的angular度写道。 我还假设你正确地使用RAII语言来处理抛出exception时释放的资源。

我只是在学习目标C,所以我不能从经验中说话,但是我在苹果的文档中已经读到了这个。

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

它不仅会告诉你如何处理你所问的问题,而且它也很好的解释它。

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

我所看到的关于exception的最好的build议是,当且仅当替代scheme不能满足后置条件或保持不变时,抛出exception。

这个build议取代了一个不明确的主观决定(这是一个好主意 ),并且基于你应该已经做出的devise决定(不变和后置条件)的技术性的精确的问题。

对于这个build议,构造函数只是一个特殊的情况,但不是特殊的情况。 那么问题就变成了什么样的阶级呢? 倡导一个单独的初始化方法,在施工之后被调用,build议该类有两个或更多的工作模式 ,在施工之后具有未准备模式,并且至less有一个就绪模式,在初始化之后进入。 这是一个额外的复杂性,但是如果class级有多种工作模式,这是可以接受的。 如果课程不具备操作模式,很难看出这种复杂性是否值得。

请注意,将设置推入单独的初始化方法不会使您避免引发exception。 现在,初始化方法将引发构造函数可能抛出的exception。 如果调用未初始化的对象,则类的所有有用方法都必须抛出exception。

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

ctors不应该做任何“聪明”的事情,所以不需要抛出exception。 如果要执行一些更复杂的对象设置,请使用Init()或Setup()方法。