【问题标题】:Sending reference of object before its construction在构造之前发送对象的引用
【发布时间】:2012-08-02 20:00:34
【问题描述】:

我在我们的一个应用程序中看到了以下代码:

public class First()
{
      private Second _second;

      public First()
      {
          _second = new Second(this);
          // Doing some other initialization stuff,
      }

}

public class Second
{
    public Second(First f)
    {
    }
}

First() 构造函数中,我们发送类First() 的引用它完全构造之前不是很糟糕吗?我认为只有在控制逻辑离开构造函数后才完全构造对象。

或者这样可以吗?

【问题讨论】:

  • 如果您可以保证在对象的构造函数完成之前不会访问对 first 的引用,那么就可以了。如果不能保证,那么您可能会在访问未完全初始化的内容时遇到问题。不推荐这种风格的代码。
  • 我需要找到并简化我的生产 VB.NET 代码,它执行与此类似的操作,我们会收到一个编译器警告。在我找到它之前,我不记得它是直接警告还是代码分析警告。我尝试手动编码,但没有出现警告。
  • 在 VS2008 中,我记得 Code Analysis warning CA2214 -- 不要在构造函数中调用可覆盖的方法 -- 这实际上是一个不同但相似的问题。

标签: c# reference constructor


【解决方案1】:

我的问题是,在 First() 构造函数中,我们在 First() 类完全构造之前发送一个引用不是很糟糕吗?

有点。这可能是个问题,当然。

如果Second 构造函数只是 保留一个引用以供以后使用,那还不错。另一方面,如果Second 构造函数回调到First

public Second(First f)
{
    f.DoSomethingUsingState();
}

...而且状态还没有建立,那当然是一件非常糟糕的事情。如果您在 First 上调用 virtual 方法,那么情况可能会更糟 - 您最终可能会调用一些甚至没有机会运行 any 的代码它的构造函数主体还没有(尽管它的变量初始化器将被执行)。

特别是,readonly 字段可能首先出现一个值,然后再出现另一个值...

我不久前blogged about this,可能会提供更多信息。

当然,如果没有做这种事情,创建两个相互引用的不可变对象是相当困难的......

【讨论】:

【解决方案2】:

如果您遇到这种模式,您可以检查是否可以将其重构为:

public class First()
{
      private Second _second;

      public First()
      {
          _second = new Second(this);
          // Doing some other initialization stuff,
      }

      private class Second
      {
          public Second(First f)
          {
          }
      }
}

将依赖传递给构造函数意味着两个类之间的一种紧密耦合,因为 First 必须相信 Second 知道它在做什么,并且不会尝试依赖 First 的未初始化状态。当 Second 是一个私有的嵌套子类(因此是一个清晰的实现细节)或者可能是一个内部类时,这种强耦合更合适。

【讨论】:

  • 很有趣,我得看看我们是否真的可以像这样重构它……谢谢你的建议!
【解决方案3】:

答案是,视情况而定。不过,一般来说,由于潜在的后果,这将被视为一个坏主意。

不过,更具体地说,只要Second 在构造之前实际上没有使用来自First 的任何东西,那么你应该没问题。如果你不能以某种方式保证,你肯定会遇到问题。

【讨论】:

  • 谢谢..我现在明白了。
【解决方案4】:

是的,这有点糟糕。可能在First 的字段完全初始化之前对其进行操作,这会导致不希望的或未定义的行为。

当您从构造函数调用虚方法时,也会发生同样的事情。

【讨论】:

    【解决方案5】:

    与例如C++,CLR 没有完全构造或不完全构造的对象的概念。一旦内存分配器返回一个归零的对象并且在构造函数运行之前,它就可以使用了(从 CLR 的角度来看)。它有它的最终类型,对虚拟方法的调用调用最派生的覆盖等。您可以在构造函数的主体中使用this,调用虚拟方法等。这确实可能会导致初始化顺序问题,但 CLR 中没有什么可以防止他们。

    【讨论】:

    • 一般来说,其成员尚未收到其初始值(例如readonly 字段)的对象不应被称为准备使用 IMO。
    • 是的,但不是从作者的角度来看。从运行时的角度来看,readonly 字段也可以在构建对象后随时更改(例如,通过使用反射),但这不是您在针对 API 进行编码时所遵循的。
    • 作者的观点在我看来就像是从 C++ 继承而来的,其中存在不完整构造对象的概念,如果构造函数中发生异常,运行时会销毁不完整对象。我的观点是,在 CLR 中,没有任何东西与这些概念相对应,您所做的任何初始化都是您自己的责任。
    • 是的,好的。应该在什么上下文中说清楚准备使用; API 的规范毕竟是 API 的重要组成部分。 +1。
    【解决方案6】:

    确实,这可能会导致您所描述的问题。因此,通常建议仅在您的注释暗示的其他初始化内容之后运行诸如_second = new Second(this); 之类的命令。

    很多时候,这种模式是存储两个对象之间相互引用的唯一解决方案。然而,在许多情况下,这种情况的发生方式是接收可能未完全初始化的实例的类与引用的类紧密耦合(例如,由同一作者编写;同一应用程序的一部分;或嵌套类,可能是私人的)。在这种情况下,可以避免负面影响,因为Second 的作者知道(或者甚至可能写过)First 的内部结构。

    【讨论】:

      【解决方案7】:

      这取决于场景,但可能导致难以预测行为。如果Second 在构造函数中对First 执行任何操作,那么一旦您更改First 的构造函数,该行为可能会变得不明确。其他构造函数指南还建议您不应在构造函数中调用虚拟或抽象方法(在构造的类上),因为这可能会导致类似的后果,而行为可能难以推理。

      【讨论】:

        【解决方案8】:

        这个问题的答案取决于FirstSecond 之间关系的性质。

        想想什么样的对象可能由另一个对象组成,该对象本身由First类型的对象组成(或需要其初始化)。在这种情况下,您应该警惕创建带有循环的对象图。

        尽管如此,在许多合理的情况下,对象图中应该出现循环。如果First 依赖Second 的状态来执行其初始化,那么您应该保持方法不变,这通常是可以的。如果Second 依赖于First 的状态来执行自己的初始化,那么您可能应该重新排列构造函数:

          public First()
          {
        
              // Doing some other initialization stuff,
              _second = new Second(this);
          }
        

        如果上述两个陈述都为真(Second 取决于First 的状态,First 取决于Second 的状态),那么您几乎肯定应该重新审视您的设计,并弄清楚更准确地说是FirstSecond 之间关系的性质。 (也许应该有一些对象Third包含对FirstSecond的引用,后两者的关系应该由Third仲裁。)

        【讨论】:

          猜你喜欢
          • 2015-07-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2023-03-31
          • 2022-11-21
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多