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

Virtual member call in a constructor

提问人:JasonS 提问时间:9/23/2008 最后编辑:CodeNotFoundJasonS 更新时间:9/11/2020 访问量:219606

问:

我收到来自 ReSharper 的警告,内容是关于从我的对象构造函数调用虚拟成员。

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

C# 构造 警告 Resharper 虚拟函数

评论

28赞 Rosdi Kasim 4/17/2013
@m.埃德蒙森,说真的......您的评论应该是这里的答案。虽然格雷格的解释是正确的,但直到我读了你的博客,我才明白。
6赞 SpeziFish 12/21/2017
您现在可以在这里找到 @m.edmondson 的文章:codeproject.com/Articles/802375/......

答:

6赞 1800 INFORMATION 9/23/2008 #1

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

8赞 David Pierre 9/23/2008 #2

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

在这一点上,对象可能还没有完全构造,方法所期望的不变量可能还不成立。

800赞 Matt Howells 9/23/2008 #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!?!
    }
}

答案是,实际上会抛出一个,因为是空的。对象的基构造函数在它自己的构造函数之前调用。通过调用对象的构造函数,引入了继承对象在完全初始化之前执行代码的可能性。NullReferenceExceptionfoovirtual

评论

56赞 kuradac 6/28/2018
这比上面的答案更清楚。示例代码胜过千言万语。
2赞 apple apple 10/29/2020
我认为就地初始化(喜欢)会更清楚地表明确实被初始化了。(而不是一些未初始化的状态)。fooprivate string foo="INI";foo
0赞 Nyerguds 3/8/2022
展示危险的好例子。但是,为了演示这种情况的安全变体,如果只是在不访问任何局部变量的情况下执行,则没有问题。DoSomething()Console.WriteLine("hello");
1赞 Yuval Peled 9/23/2008 #4

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

评论

0赞 qbeuek 9/23/2008
在 C++ 中,不禁止在构造函数中调用虚函数。
0赞 David Pierre 9/23/2008
同样的论点也适用于 C++,如果你不需要访问成员,你不在乎它们没有初始化......
3赞 qbeuek 9/26/2008
不。当您在 C++ 的构造函数中调用虚拟方法时,它不会调用最深的覆盖实现,而是调用与当前类型关联的版本。它是虚拟调用的,但就像在当前类的某个类型上一样 - 您无权访问派生类的方法和成员。
24赞 Alex Lyman 9/23/2008 #5

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

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

1283赞 Greg Beech 9/23/2008 #6

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

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

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

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

评论

180赞 Paul Pacurar 2/15/2011
格雷格,请告诉我,当类具有 VIRTUAL 成员 [即在派生类中覆盖]时,为什么有人会有一个 SEALED 类(不能继承)?
126赞 Øyvind 2/16/2011
如果要确保派生类无法进一步派生,则完全可以对其进行密封。
63赞 ljs 2/26/2011
@Paul - 关键是已经完成了类的虚拟成员的派生,因此将该类标记为您希望的完全派生。
9赞 Dave Cousineau 10/8/2011
@Greg 如果虚拟方法的行为与实例变量无关,这不行吗?似乎我们应该能够声明虚拟方法不会修改实例变量?(静态?例如,如果您希望有一个可以重写的虚拟方法,以实例化更派生的类型。这对我来说似乎是安全的,并且不值得发出此警告。
11赞 Revolutionair 2/26/2013
@PaulPacurar - 如果要在最派生的类中调用虚拟方法,则在知道它不会导致问题的情况下,仍会收到警告。在这种情况下,您可以通过密封该类与系统共享您的知识。
5赞 xtofl 9/23/2008 #7

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

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

170赞 Lloyd 9/23/2008 #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 之间的规则非常不同。您的程序员可能不知道会发生什么!

评论

49赞 Lloyd 9/24/2008
格雷格·比奇(Greg Beech)的答案,虽然不幸的是没有我的答案那么高,但我觉得是更好的答案。它当然还有一些更有价值的解释性细节,我没有花时间包括在内。
4赞 OlegYch 11/18/2011
实际上,Java 中的规则是一样的。
10赞 Jacek Sieka 5/28/2012
实际上,@JoãoPortela C++非常不同。构造函数(和析构函数)中的虚拟方法调用是使用当前正在构造的类型(和 vtable)解析的,而不是像 Java 和 C# 那样使用最派生的类型。 这是相关的常见问题解答条目
1赞 João Portela 5/28/2012
@JacekSieka你是绝对正确的。自从我用 C++ 编码以来已经有一段时间了,我不知何故混淆了这一切。我应该删除评论以避免混淆其他人吗?
0赞 supercat 3/25/2015
C# 与 Java 和 VB.NET 有很大的不同;在 C# 中,在声明时初始化的字段将在基构造函数调用之前处理其初始化;这样做的目的是允许派生类对象可从构造函数使用,但不幸的是,这种能力仅适用于其初始化不受任何派生类参数控制的派生类功能。
91赞 Ilya Ryzhenkov 9/23/2008 #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();
    }
  }

评论

0赞 Daniel Dušek 1/17/2022
或者可以在类的构造函数中显式说:那么基类的基类将始终在 的构造函数中被调用。AA() { base.Foo(); }Foo()BA
5赞 supercat 10/26/2012 #10

这个问题的一个重要方面是,如果基类期望它这样做,那么基类从其构造函数中调用虚拟成员是安全的。在这种情况下,派生类的设计者负责确保在构造完成之前运行的任何方法在特定情况下的行为都尽可能合理。例如,在 C++/CLI 中,构造函数包装在代码中,如果构造失败,这些代码将调用部分构造的对象。在这种情况下调用通常是必要的,以防止资源泄漏,但必须为运行它们所基于的对象可能尚未完全构造的可能性做好准备。DisposeDisposeDispose

13赞 Josh Kodroff 12/28/2012 #11

上面有很好的答案,说明为什么你不想这样做。这里有一个反例,也许你会想这样做(从 Sandi Metz 的 Ruby 中的 Practical Object-Oriented Design 翻译成 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 { }

评论

0赞 Ian Ringrose 3/17/2015
我会考虑为此使用工厂方法。
0赞 supercat 3/25/2015
我希望 .NET Framework 能够将大多数无用的 作为默认成员 ,将该 vtable 插槽用于一个方法,当构造函数返回到客户端代码时,当构造函数抛出时,或者当发现对象被放弃时,该方法将被调用。大多数需要从基类构造函数调用虚拟方法的方案最好使用两阶段构造来处理,但两阶段构造应表现为实现细节,而不是要求客户端调用第二阶段。FinalizeObjectManageLifetime(LifetimeStatus)
0赞 vgru 3/8/2016
尽管如此,此代码仍可能出现问题,就像此线程中显示的任何其他情况一样; 不能保证在调用构造函数之前可以安全地调用。此外,默认实例化默认依赖项并不是你所说的“纯 DI”。GetDependencyMySubClass
0赞 Nachbars Lumpi 10/31/2016
该示例执行“依赖项外射”。;-)对我来说,这是从构造函数调用虚拟方法的另一个很好的反例。SomeDependency 不再在 MySubClass 派生中实例化,从而导致依赖于 SomeDependency 的每个 MyClass 功能的行为中断。
-3赞 adityap 5/23/2014 #12

我发现的另一件有趣的事情是,ReSharper错误可以通过执行以下操作来“满足”,这对我来说是愚蠢的。但是,正如前面许多人提到的,在构造函数中调用虚拟属性/方法仍然不是一个好主意。

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

评论

0赞 alzaimar 1/5/2015
您不应该找到解决方法,而应该解决实际问题。
2赞 adityap 1/10/2015
我同意@alzaimar!我试图为面临类似问题并且不想实施上述解决方案的人留下选择,这可能是由于一些限制。有了这个(正如我在上面的解决方法中提到的),我试图指出的另一件事是,如果可能的话,ReSharper也需要能够将此解决方法标记为错误。然而,它目前没有,这可能会导致两件事 - 他们忘记了这个场景,或者他们想故意将其排除在外,用于一些现在无法想到的有效用例。
0赞 OwnageIsMagic 10/13/2020
@adityap 要禁止使用警告,请使用警告,禁止 jetbrains.com/help/resharper/...
5赞 Gustavo Mori 8/15/2015 #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);
    }
}

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

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

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

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

评论

4赞 monkeyhouse 12/23/2015
嗯,我认为 DerivedFromBetter 构造函数调用了 BetterBaseClass 构造函数隐含性。上面的代码应该等同于 public DerivedFromBetter() : base(),因此 intialize 将被调用两次
0赞 Sven Vranckx 3/29/2018
可以在 BetterBaseClass 类中定义一个受保护的构造函数,该构造函数具有一个附加参数,该参数确定是否在基构造函数中调用。然后,派生的构造函数将调用以避免调用 Initialize 两次bool initializeInitializebase(false)
0赞 Gustavo Mori 4/3/2018
@user1778606:当然可以!我已经根据你的观察解决了这个问题。谢谢!
7赞 user1969177 2/2/2019
@GustavoMori 这不起作用。在 DerivedFromBetter 构造函数运行之前,基类仍调用 DisplayState,因此它输出“BetterBaseClass”。
1赞 Jim Ma 10/15/2015 #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());
    }
}

评论

0赞 Phil1970 4/18/2017
我几乎从不这样做,因为如果你想单步执行构造函数,它会使调试变得有些困难。
3赞 Biniam Eyakem 8/29/2016 #15

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

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

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

此处的子类重写父属性。如果此属性未标记为 virtual,编译器将警告该属性隐藏父类上的属性,并建议添加“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"
    }
} 

评论

0赞 hyankov 4/18/2019
没有“父”和“子”类,而是“base”和“derived”。
3赞 typhon04 10/6/2017 #16

谨防盲目听从雷夏普的建议,让班级封存! 如果它是 EF Code First 中的模型,它将删除 virtual 关键字,这将禁用其关系的延迟加载。

    public **virtual** User User{ get; set; }
-2赞 Web Dev 12/14/2017 #17

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

评论

0赞 Stefan Bormann 8/30/2018
这会使警告消失,但不能解决问题。当您添加更多派生类时,您会遇到与其他人解释的相同的问题。
1赞 pasx 12/11/2019 #18

我认为,如果您想让子类能够设置或覆盖父构造函数将立即使用的属性,那么忽略警告可能是合法的:

internal class Parent
{
    public Parent()
    {
        Console.WriteLine("Parent ctor");
        Console.WriteLine(Something);
    }

    protected virtual string Something { get; } = "Parent";
}

internal class Child : Parent
{
    public Child()
    {
        Console.WriteLine("Child ctor");
        Console.WriteLine(Something);
    }

    protected override string Something { get; } = "Child";
}

这里的风险是子类从其构造函数设置属性,在这种情况下,值的更改将在调用基类构造函数之后发生。

我的用例是,我希望子类提供特定值或实用程序类(例如转换器),并且我不想在基上调用初始化方法。

实例化子类时,上述的输出为:

Parent ctor
Child
Child ctor
Child