首页 文章

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

提问于
浏览
1188

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

为什么这不该做?

17 回答

  • 1

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

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

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

    作为Greg explained,这里的根本问题是基类构造函数在构造派生类之前调用虚拟成员 .

    以下代码取自MSDN's constructor design guidelines,演示了此问题 .

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

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

  • 523

    我发现另一个有趣的事情是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";
       }
    

    }

  • 3

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

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

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

    最后对使用的影响,下面的例子的输出放弃了父类构造函数设置的初始值 . And this is what Re-sharper attempts to to warn you ,在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"
        }
    }
    
  • 155

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

    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();
        }
      }
    
  • 5

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

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

  • 83

    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之间的规则是如此不同 . 你的程序员可能不知道会发生什么!

  • 1

    上面有很好的答案,为什么你不想这样做 . 这是一个反例,也许你想要这样做(由Sandi Metz翻译成C#,#12687181,第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 { }
    
  • 4

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

  • 5

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

    请注意,这只是一个让你注意并确保它是正确的 warning . 对于这种情况有实际的用例,你只需要 document the behavior 的虚拟成员,它不能使用在下面派生类中声明的构造函数所声明的任何实例字段 .

  • 11

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

  • 2

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

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

  • 3

    构造用C#编写的对象时,会发生的情况是初始化程序按从最派生类到基类的顺序运行,然后构造函数按顺序从基类运行到最派生类(see Eric Lippert's blog for details as to why this is) .

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

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

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

  • 16

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

  • 5

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

    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());
        }
    }
    
  • -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!?!
        }
    }
    

    答案是,实际上会抛出 NullReferenceException ,因为 foo 为空 . An object's base constructor is called before its own constructor . 通过在对象的构造函数中调用 virtual ,您将介绍继承对象在完全初始化之前执行代码的可能性 .

相关问题