虚拟成员在构造函数中调用

Jas*_*onS 1270 c# resharper constructor warnings virtual-functions

我从ReSharper收到一条关于从我的对象构造函数调用虚拟成员的警告.

为什么不做这件事?

Gre*_*ech 1137

构造用C#编写的对象时,会发生的情况是初始化程序按从最派生类到基类的顺序运行,然后构造函数按顺序从基类运行到最派生类(请参阅Eric Lippert的博客了解详细信息至于为什么这是).

同样在.NET对象中,不会在构造时更改类型,而是从最派生类型开始,方法表用于最派生类型.这意味着虚方法调用始终在最派生类型上运行.

当你将这两个事实结合起来时,你会遇到这样的问题:如果你在构造函数中进行虚方法调用,并且它不是其继承层次结构中派生类型最多的类型,那么它将在没有构造函数的类上调用它.运行,因此可能不适合调用该方法.

当然,如果将类标记为已密封以确保它是继承层次结构中派生类型最多的类型,则可以缓解此问题 - 在这种情况下,调用虚方法是完全安全的.

  • 格雷格,请告诉我,为什么有人有一个SEALED(不能是INHERITED),当它有VIRTUAL成员[在DERIVED类中覆盖]时? (141认同)
  • 如果你想确保派生类不能进一步派生,那么密封它是完全可以接受的. (111认同)
  • @Paul - 关键在于已经完成派生*base*class [es]的虚拟成员,因此将类标记为完全派生,就像你想要的那样. (58认同)
  • @PaulPacurar - 如果你想在派生程度最高的类中调用虚方法,你仍会得到警告,而你知道它不会导致问题.在这种情况下,您可以通过密封该类来与系统分享您的知识. (11认同)
  • @Greg如果虚方法的行为与实例变量无关,那不行吗?似乎我们应该能够声明虚方法不会修改实例变量?(静态?)例如,如果您想要一个可以重写的虚方法来实例化更多派生类型.这对我来说似乎是安全的,并不保证这个警告. (8认同)
  • @Sahuagin - 可以编写一个可以从构造函数中调用的虚方法,没有任何问题,你可以添加`// ReSharper Warning Disable`注释来阻止它抱怨.但是R#只是警告你它有潜在危险.与往常一样,如果您知道自己在做什么,警告有时是安全的. (5认同)
  • @mo. - 调用任何不保证直接或间接位于继承链顶部的内容都可能导致问题.如果您不想密封整个类但想要调用特定的虚方法或使用构造函数中的特定虚拟属性,则可以覆盖并密封您需要访问的成员,例如`protected sealed override void Foo(){ ......}` (2认同)
  • 在一种情况下,您可能会在构造函数中调用虚拟成员。当你使用一些ORM框架时,要求实体类的属性是虚拟的。并且您可以初始化与构造函数中的实体具有一对多关系的属性,以给它一个默认值。这就是我收到警告并找到这篇文章的地方。我认为我们与此无关。 (2认同)
  • @CADbloke,当方法是虚拟的时,不是它的工作原理. (2认同)

Mat*_*lls 615

为了回答您的问题,请考虑以下问题:在Child实例化对象时,下面的代码将打印出来的内容是什么?

class Parent
{
    public Parent()
    {
        DoSomething();
    }

    protected virtual void DoSomething() 
    {
    }
}

class Child : Parent
{
    private string foo;

    public Child() 
    { 
        foo = "HELLO"; 
    }

    protected override void DoSomething()
    {
        Console.WriteLine(foo.ToLower()); //NullReferenceException!?!
    }
}
Run Code Online (Sandbox Code Playgroud)

答案是,实际上NullReferenceException会抛出一个,因为它foo是null.在自己的构造函数之前调用对象的基础构造函数.通过virtual在对象的构造函数中调用,您可以介绍继承对象在完全初始化之前执行代码的可能性.

  • 这比上面的答案更清楚.示例代码值得千言万语. (8认同)
  • 我认为就地初始化 `foo` (如 `private string foo="INI";`)会更清楚地表明 `foo` 确实被初始化了。(而不是某些*未初始化*状态)。 (2认同)

Llo*_*oyd 159

C#的规则与Java和C++的规则非常不同.

当您在C#中的某个对象的构造函数中时,该对象以完全初始化(仅非"构造")形式存在,作为其完全派生类型.

namespace Demo
{
    class A 
    {
      public A()
      {
        System.Console.WriteLine("This is a {0},", this.GetType());
      }
    }

    class B : A
    {      
    }

    // . . .

    B b = new B(); // Output: "This is a Demo.B"
}
Run Code Online (Sandbox Code Playgroud)

这意味着如果从A的构造函数调用虚函数,它将解析为B中的任何覆盖(如果提供了一个).

即使你故意设置这样的A和B,完全理解系统的行为,你可能会在以后感到震惊.假设您在B的构造函数中调用了虚函数,"知道"它们将在适当时由B或A处理.然后时间过去了,其他人决定他们需要定义C,并覆盖那里的一些虚函数.突然之间,B的构造函数最终会调用C中的代码,这可能导致相当令人惊讶的行为.

无论如何,避免构造函数中的虚函数可能是一个好主意,因为C#,C++和Java之间的规则如此不同.你的程序员可能不知道会发生什么!

  • 格雷格比奇的答案,虽然不幸的是没有像我的回答那样高,但我觉得这是更好的答案.它肯定有一些我没有花时间包含的更有价值的解释性细节. (48认同)
  • @JoãoPortelaC++实际上非常不同.构造函数(和析构函数!)中的虚方法调用是使用当前构造的类型(和vtable)解析的,而不是像Java和C#那样的最派生类型.[这是相关的FAQ条目](http://www.parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.5). (9认同)
  • 实际上Java中的规则是相同的. (3认同)

Ily*_*kov 84

已经描述了警告的原因,但是如何修复警告?你必须密封班级或虚拟成员.

  class B
  {
    protected virtual void Foo() { }
  }

  class A : B
  {
    public A()
    {
      Foo(); // warning here
    }
  }
Run Code Online (Sandbox Code Playgroud)

你可以密封A级:

  sealed class A : B
  {
    public A()
    {
      Foo(); // no warning
    }
  }
Run Code Online (Sandbox Code Playgroud)

或者你可以密封方法Foo:

  class A : B
  {
    public A()
    {
      Foo(); // no warning
    }

    protected sealed override void Foo()
    {
      base.Foo();
    }
  }
Run Code Online (Sandbox Code Playgroud)


Ale*_*man 17

在C#中,基类的构造函数派生类的构造函数之前运行,因此派生类可能在可能被覆盖的虚拟成员中使用的任何实例字段都尚未初始化.

请注意,这只是一个警告,让您注意并确保它是正确的.这种情况有实际的用例,您只需要记录虚拟成员的行为,它不能使用在调用它的构造函数下面的派生类中声明的任何实例字段.


Jos*_*off 11

上面有为什么你写得很好的答案不会想这样做.这里有一个反例,其中也许你想这样做(翻译成从C#Ruby的实用面向对象的设计由三迪梅斯第126页).

请注意,GetDependency()不接触任何实例变量.如果静态方法可以是虚拟的,那么它将是静态的.

(公平地说,通过依赖注入容器或对象初始化器可能有更聪明的方法...)

public class MyClass
{
    private IDependency _myDependency;

    public MyClass(IDependency someValue = null)
    {
        _myDependency = someValue ?? GetDependency();
    }

    // If this were static, it could not be overridden
    // as static methods cannot be virtual in C#.
    protected virtual IDependency GetDependency() 
    {
        return new SomeDependency();
    }
}

public class MySubClass : MyClass
{
    protected override IDependency GetDependency()
    {
        return new SomeOtherDependency();
    }
}

public interface IDependency  { }
public class SomeDependency : IDependency { }
public class SomeOtherDependency : IDependency { }
Run Code Online (Sandbox Code Playgroud)


Dav*_*rre 6

是的,在构造函数中调用虚方法通常很糟糕.

此时,对象可能尚未完全构建,并且方法所预期的不变量可能尚未成立.


Gus*_*ori 6

一个重要的缺失是,解决此问题的正确方法是什么?

正如Greg 解释的那样,这里的根本问题是基类构造函数会在派生类被构造之前调用虚拟成员。

以下代码取自MSDN 的构造函数设计指南,演示了这个问题。

public class BadBaseClass
{
    protected string state;

    public BadBaseClass()
    {
        this.state = "BadBaseClass";
        this.DisplayState();
    }

    public virtual void DisplayState()
    {
    }
}

public class DerivedFromBad : BadBaseClass
{
    public DerivedFromBad()
    {
        this.state = "DerivedFromBad";
    }

    public override void DisplayState()
    {   
        Console.WriteLine(this.state);
    }
}
Run Code Online (Sandbox Code Playgroud)

DerivedFromBad创建新实例时,基类构造函数调用DisplayState并显示,BadBaseClass因为派生构造函数尚未更新该字段。

public class Tester
{
    public static void Main()
    {
        var bad = new DerivedFromBad();
    }
}
Run Code Online (Sandbox Code Playgroud)

改进的实现从基类构造函数中删除了虚拟方法,并使用了一个Initialize方法。创建DerivedFromBetter显示预期的“DerivedFromBetter”的新实例

public class BetterBaseClass
{
    protected string state;

    public BetterBaseClass()
    {
        this.state = "BetterBaseClass";
        this.Initialize();
    }

    public void Initialize()
    {
        this.DisplayState();
    }

    public virtual void DisplayState()
    {
    }
}

public class DerivedFromBetter : BetterBaseClass
{
    public DerivedFromBetter()
    {
        this.state = "DerivedFromBetter";
    }

    public override void DisplayState()
    {
        Console.WriteLine(this.state);
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @GustavoMori 这不起作用。基类仍然在 DerivedFromBetter 构造函数运行之前调用 DisplayState,因此它输出“BetterBaseClass”。 (5认同)
  • 嗯,我认为 DerivedFromBetter 构造函数隐式调用了 BetterBaseClass 构造函数。上面的代码应该等价于 public DerivedFromBetter() : base(),所以初始化会被调用两次 (4认同)

180*_*ION 5

因为在构造函数完成执行之前,该对象未完全实例化.虚函数引用的任何成员都可能未初始化.在C++中,当您在构造函数中时,this仅引用您所在构造函数的静态类型,而不是正在创建的对象的实际动态类型.这意味着虚函数调用甚至可能不会达到您期望的范围.


xto*_*ofl 5

您的构造函数(稍后,在您的软件的扩展中)可以从覆盖虚方法的子类的构造函数中调用.现在不是子类的函数实现,但是将调用基类的实现.因此,在这里调用虚函数并没有多大意义.

但是,如果您的设计符合Liskov替换原则,则不会造成任何伤害.可能这就是它被容忍的原因 - 警告,而不是错误.


sup*_*cat 5

其他答案尚未解决的这个问题的一个重要方面是,基类在其构造函数中调用虚拟成员是安全的,如果这是派生类期望它做的事情.在这种情况下,派生类的设计者负责确保在构造完成之前运行的任何方法将在这种情况下表现得尽可能合理.例如,在C++/CLI中,构造函数包含在代码中,Dispose如果构造失败,它将调用部分构造的对象.Dispose在这种情况下调用通常是必要的,以防止资源泄漏,但Dispose必须准备方法,以确保它们运行的​​对象可能没有完全构建.