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

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

===============>>#1 票数:261 已采纳

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

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

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

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

===============>>#2 票数:56

Eric Lippert说有4种例外。

  • 致命的例外不是你的错,你不能阻止他们,你不能明智地清理它们。
  • Boneheaded异常是你自己的错误,你可以防止它们,因此它们是你的代码中的错误。
  • 不幸的例外是不幸的设计决策的结果。 在完全非特殊情况下抛出异常情况,因此必须始终抓住并处理。
  • 最后,外生异常似乎有点像烦恼的异常,除了它们不是不幸的设计选择的结果。 相反,它们是不整洁的外部现实影响你美丽,清晰的程序逻辑的结果。

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

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

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

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

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

===============>>#3 票数:30

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

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

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

===============>>#4 票数:6

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

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

===============>>#5 票数:5

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

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

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

===============>>#6 票数:4

据我所知,没有人提出一个相当明显的解决方案,它体现了一阶段和两阶段建设的最佳状态。

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

===============>>#7 票数:4

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

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

===============>>#8 票数:4

构造函数抛出异常是合理的,只要它能够正常清理它。 如果您遵循RAII范例(资源获取是初始化),那么构造函数执行有意义的工作很常见的; 如果无法完全初始化,那么编写良好的构造函数将依次清理。

===============>>#9 票数:3

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

===============>>#10 票数:3

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

看到这个问题:

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

===============>>#11 票数:3

请参阅C ++ FAQ第17.217.4节。

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

===============>>#12 票数:2

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

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

本页详细讨论了C ++中的情况。

===============>>#13 票数:2

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

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

===============>>#14 票数:1

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

之前我曾经在编码标准下工作,要求永远不会使用异常,只有初始化器上的错误代码,因为开发人员被语言焚烧处理异常很差。 没有垃圾收集的语言将以非常不同的方式处理堆和堆栈,这对于非RAII对象可能很重要。 尽管团队决定保持一致非常重要,因此默认情况下他们知道是否需要在构造函数之后调用初始化程序。 所有方法(包括构造函数)也应该很好地记录它们可以抛出的异常,因此调用者知道如何处理它们。

我一般都赞成单阶段构造,因为很容易忘记初始化一个对象,但是有很多例外。

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

===============>>#15 票数:1

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

===============>>#16 票数:1

是的,如果构造函数无法构建其内部部分之一,则可以 - 通过选择 - 它有责任抛出(并以某种语言声明) 显式异常 ,在构造函数文档中适当注明。

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

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

===============>>#17 票数:1

在构造期间抛出异常是使代码更复杂的好方法。 看似简单的事情突然变得艰难。 例如,假设你有一个堆栈。 如何弹出堆栈并返回最高值? 好吧,如果堆栈中的对象可以抛出它们的构造函数(构造临时函数以返回调用者),则无法保证不会丢失数据(减少堆栈指针,使用值的复制构造函数构造返回值)堆栈,它抛出,现在有一个堆栈,只是丢失了一个项目)! 这就是为什么std :: stack :: pop不返回值,你必须调用std :: stack :: top。

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

===============>>#18 票数:1

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

所以作为一种证据,永远不要从构造函数/ init返回一个僵尸对象。

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

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

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

完全可能的是, 特定接口可能具有一个契约,该契约表明存在允许异常的“初始化”方法。 补充此接口的对象可能无法正确响应除属性设置器之外的任何调用,直到调用初始化为止。 我在启动过程中将它用于OO操作系统中的设备驱动程序,并且它是可行的。

通常,您不需要僵尸对象。 在像Smalltalk这样的语言中, 变得有点变得模糊不清,但是过度使用变成了糟糕的风格。 让一个对象原位更改为另一个对象,因此不需要信封包装(Advanced C ++)或策略模式(GOF)。

===============>>#19 票数:1

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

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

===============>>#20 票数:0

我所看到的关于异常的最好建议是,当且仅当替代方案未能满足后置条件或维持不变量时抛出异常。

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

构造函数只是该建议的一个特定但非特殊的情况。 那么问题就变成了,一个班级应该有哪些不变量? 在构造之后调用的单独初始化方法的倡导者建议该类具有两个或更多个操作模式 ,在构造之后具有未准备模式并且在初始化之后输入至少一个就绪模式。 这是一个额外的复杂功能,但如果该类有多种操作模式,则可以接受。 如果该类不具备操作模式,很难看出该复杂性是如何值得的。

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

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

===============>>#21 票数:0

严格地说,从Java的角度来看,每次使​​用非法值初始化构造函数时,都应该抛出异常。 这样它就不会构建成糟糕的状态。

===============>>#22 票数:0

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

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

其他一些方法是init()方法,它带有一些自己的问题。 其中一个是确保init()实际被调用。

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

我已经看到了各种提出的设计模式来处理这个问题。 例如能够通过ctor创建一个初始对象,但必须调用init()来获取带有加速器/ mutator的包含的初始化对象。

每种方法都有其起伏不定; 我成功地使用了所有这些。 如果你没有在创建它们的瞬间创建现成的对象,那么我建议使用大量的断言或异常来确保用户在init()之前不进行交互。

附录

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

===============>>#23 票数:0

我只是在学习Objective C,所以我无法从经验中说出来,但我确实在苹果的文档中读过这篇文章。

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

它不仅会告诉您如何处理您提出的问题,而且还可以很好地解释它。

===============>>#24 票数:0

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

===============>>#25 票数:-2

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

  ask by Mark R Lindsey translate from so

未解决问题?本站智能推荐:

4回复

何时为构造函数抛出异常

嘿上面我有一个类的构造函数。 我的问题是我应该在构造函数中添加空指针异常还是不必要? 老实说,我只是不明白何时应该为我的代码添加例外。 但在这种情况下,我应该使用哪个构造函数?
1回复

构造函数不抛出异常[重复]

这个问题在这里已有答案: C ++构造函数未调用 1个答案 在我下面的代码中,我想测试如果我有一个对象,包含另一个构造函数抛出异常的对象会发生什么。 但是下面的代码绝对没有。 控制台上根本没有打印任何内容。 如果在try块中,我使用注释代码,那么它显示正
2回复

构造函数通过抛出异常来完成?有内存泄漏吗?

我正在审阅这篇文章,并指出 注意:如果构造函数通过抛出异常结束,则清除与对象本身关联的内存 - 没有内存泄漏。 例如: 我很难理解这一点,如果有人能澄清这一点,我会很感激。 我尝试了下面的示例,该示例显示了构造函数中的异常,不会调用析构函数。 我该如何解决这个问题
2回复

通过在构造函数主体之前抛出异常来防止构造

C ++ 我想一个类来throw它的构造身体的左大括号之前的异常{使用其自己的成员函数,以防止施工。 我定义了一个成员函数,其目的只是为了无条件地throw异常,它具有一个任意选择的非void返回类型和一个与该返回类型相匹配的伪数据成员,因此我可以通过构造该数据成员来触发throw构造函
6回复

为什么在构造函数中抛出异常导致空引用?

为什么在构造函数中抛出异常导致空引用? 例如,如果我们运行下面的代码,则teacher的值为null,而st.teacher则不是(创建了Teacher对象)。 为什么?
3回复

获取对构造函数抛出异常的实例的引用

请考虑以下问题 在设计框架时,会出现一个暴露某些事件的界面 该接口最终将由许多不同的第三方供应商实现,并且可能被各种客户端使用。 由于每个供应商都可能使用无效数据来创建事件args,因此我作为框架作者的唯一控件是在事件args级别,所以我想到了以下模式: 这可以确保客
1回复

当构造函数抛出异常时,构造函数中分配的内存是如何释放的?

以下是我的示例代码。 基类具有A类的指针,其中使用new运算符从堆分配内存。 我明确地抛出异常。 由于我没有在* ptr上调用delete,因此未分配new分配的内存。 由于对象没有完全构造,我们如何释放内存? 当Base构造函数抛出异常时,控件转到catch块并处理异常。 但
3回复

使用Java构造函数抛出异常从类中进行子类化

我的理解是,子类中的重写方法不应该在父类的基本方法上抛出异常或更窄的异常。 为什么它在构造函数中起反作用,子类的构造函数必须抛出相同的异常或更广泛,对此有任何合理的解释?
1回复

C ++构造函数初始化列表抛出异常

我有以下代码的问题。 正如我们所看到的,我已经处理了A的构造函数在C的构造函数中抛出的异常,为什么我还要在main函数中再次捕获并处理异常呢?
1回复

从C ++构造函数向静态[非动态]对象抛出异常

我似乎无法从C ++构造函数中为堆栈上实例化的对象引发异常(与使用new关键字分配的动态对象相反)。 如何完成的?