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

博客讨论了从C#构造函数中调用虚拟成员时ReSharper发出警告的原因。构造函数执行期间,对象可能未完全初始化,虚拟函数调用可能不按预期行为。虽然在C++中这是一个严重问题,但在C#中,由于对象以完全初始化的最派生类型存在,因此虚拟方法调用可能安全,但仍然可能导致意外行为。建议避免在构造函数中使用虚拟成员,以确保代码的清晰性和可预测性。
摘要由CSDN通过智能技术生成

我从ReSharper得到警告,关于从对象构造函数调用虚拟成员的信息。

为什么这不是要做的事情?


#1楼

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


#2楼

是的,在构造函数中调用虚拟方法通常是不好的。

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


#3楼

为了回答您的问题,请考虑以下问题:实例化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为null。 对象的基础构造函数在其自己的构造函数之前被调用 。 通过在对象的构造函数中进行virtual调用,您将引入一种可能性,即继承对象将在代码完全初始化之前执行代码。


#4楼

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


#5楼

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

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


#6楼

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

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

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

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


#7楼

可以从覆盖虚拟方法的子类的构造函数中调用构造函数(此后,在软件的扩展中)。 现在,不是子类的函数实现,而是基类的实现将被调用。 因此,在这里调用虚函数实际上没有任何意义。

但是,如果您的设计满足Liskov替换原理,则不会造成任何损害。 这可能就是为什么它可以容忍-警告,而不是错误。


#8楼

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


#9楼

已经描述了警告的原因,但是您将如何解决警告? 您必须密封类或虚拟成员。

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

#10楼

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


#11楼

上面有为什么你希望这样做,写得很好的答案。 这是一个反例,您可能想这样做(Sandi Metz 从Ruby的《 实用面向对象设计》译成C#,第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 { }

#12楼

我发现的另一件有趣的事情是,通过执行以下对我来说很愚蠢的事情可以“满足” ReSharper错误(但是,正如前面提到的,在ctor中调用虚拟prop /方法仍然不是一个好主意。

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

}


#13楼

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

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

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

#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楼

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

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

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试图警告您的事情在父类构造函数上设置的值已公开,将由子类构造函数覆盖,该子类构造函数在父类构造函数之后立即调用

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

#16楼

谨防盲目听从Resharper的建议,使课程封闭! 如果它是EF Code First中的模型,它将删除virtual关键字,这将禁用对其关系的延迟加载。

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

#17楼

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值