提问



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


为什么这不该做?

最佳参考


构造用C#编写的对象时,会发生的情况是初始化程序按从最派生类到基类的顺序运行,然后构造函数按顺序从基类运行到最派生类(参见Eric Lippert的博客)详情为何如此)。[30]


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


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


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

其它参考1


为了回答你的问题,请考虑这个问题:当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,因为foo为空。 在它自己的构造函数之前调用一个对象的基础构造函数。通过在对象的构造函数中调用virtual,你引入了继承对象在它们被执行之前执行代码的可能性。完全初始化。

其它参考2


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


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


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


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

其它参考3


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


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

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


你可以密封A级:


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


或者你可以密封方法Foo:


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

    protected sealed override void Foo()
    {
      base.Foo();
    }
  }

其它参考4


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


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

其它参考5


上面有很好的答案,为什么你不想想要这样做。这里是一个反例,也许你 希望这样做(翻译成C#来自Sandi Metz的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 { }

其它参考6


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


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

其它参考7


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


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

其它参考8


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

其它参考9


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

其它参考10


该警告提醒您虚拟成员可能会在派生类上被覆盖。在这种情况下,父类对虚拟成员所做的任何操作都将通过覆盖子类来撤消或更改。为了清晰起见,请看一下小例子


下面的父类尝试在其构造函数上为虚拟成员设置值。这将触发重新锐化警告,让我们看看代码:


public class Parent
{
    public virtual object Obj{get;set;}
    public Parent()
    {
        // Re-sharper warning: this is open to change from 
        // inheriting class overriding virtual member
        this.Obj = new Object();
    }
}


这里的子类覆盖父属性。如果此属性未标记为虚拟,则编译器将警告该属性隐藏父类的属性,并建议您添加new关键字(如果它是故意的)。


public class Child: Parent
{
    public Child():base()
    {
        this.Obj = "Something";
    }
    public override object Obj{get;set;}
}


最后对使用的影响,下面的例子的输出放弃了父类构造函数设置的初始值。
这就是Re-sharper试图警告你的内容,在Parent类构造函数上设置的值被子类构造函数覆盖,后者在父类构造函数之后被调用的。


public class Program
{
    public static void Main()
    {
        var child = new Child();
        // anything that is done on parent virtual member is destroyed
        Console.WriteLine(child.Obj);
        // Output: "Something"
    }
} 

其它参考11


谨防盲目跟随Resharper的建议,让课堂密封!
如果它是EF Code First中的模型,它将删除虚拟关键字,这将禁用其关系的延迟加载。


    public **virtual** User User{ get; set; }

其它参考12


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


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


以下代码取自MSDN的构造函数设计指南,演示了此问题。[33]


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


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


public class Tester
{
    public static void Main()
    {
        var bad = new DerivedFromBad();
    }
}


改进的实现从基类构造函数中删除虚方法,并使用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);
    }
}

其它参考13


在这种特定情况下,C ++和C#之间存在差异。
在C ++中,对象未初始化,因此在构造函数中调用虚拟函数是不安全的。
在C#中创建类对象时,其所有成员都初始化为零。可以在构造函数中调用虚函数,但是如果你可能会访问仍然为零的成员。如果你不需要访问成员,那么在C#中调用虚函数是相当安全的。

其它参考14


只是为了添加我的想法。如果在定义私有字段时始终初始化私有字段,则应避免此问题。至少下面的代码就像一个魅力:


class Parent
{
    public Parent()
    {
        DoSomething();
    }
    protected virtual void DoSomething()
    {
    }
}

class Child : Parent
{
    private string foo = "HELLO";
    public Child() { /*Originally foo initialized here. Removed.*/ }
    protected override void DoSomething()
    {
        Console.WriteLine(foo.ToLower());
    }
}

其它参考15


我发现另一个有趣的事情是ReSharper错误可以通过做类似下面的事情来满足,这对我来说是愚蠢的(然而,正如之前许多人所提到的,在ctor中调用虚拟道具/方法仍然不是一个好主意。


public class ConfigManager
{

   public virtual int MyPropOne { get; private set; }
   public virtual string MyPropTwo { get; private set; }

   public ConfigManager()
   {
    Setup();
   }

   private void Setup()
   {
    MyPropOne = 1;
    MyPropTwo = "test";
   }


}

其它参考16


我只是将一个Initialize()方法添加到基类,然后从派生构造函数调用它。在所有构造函数都已执行后,该方法将调用任何虚拟/抽象方法/属性:)