提问人:JasonS 提问时间:9/23/2008 最后编辑:CodeNotFoundJasonS 更新时间:9/11/2020 访问量:219606
构造函数中的虚拟成员调用
Virtual member call in a constructor
答:
因为在构造函数完成执行之前,对象不会完全实例化。虚拟函数引用的任何成员都不得初始化。在 C++ 中,当您处于构造函数中时,仅引用您所在的构造函数的静态类型,而不是正在创建的对象的实际动态类型。这意味着虚拟函数调用甚至可能无法到达您期望的位置。this
是的,在构造函数中调用虚拟方法通常很糟糕。
在这一点上,对象可能还没有完全构造,方法所期望的不变量可能还不成立。
为了回答您的问题,请考虑以下问题:当对象实例化时,下面的代码将打印出什么?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
virtual
评论
foo
private string foo="INI";
foo
DoSomething()
Console.WriteLine("hello");
在这种特定情况下,C++ 和 C# 之间存在差异。 在 C++ 中,对象未初始化,因此在构造函数中调用 virutal 函数是不安全的。 在 C# 中,当创建类对象时,其所有成员都初始化为零。可以在构造函数中调用虚拟函数,但可能会访问仍为零的成员。如果不需要访问成员,则在 C# 中调用虚拟函数是非常安全的。
评论
在 C# 中,基类的构造函数在派生类的构造函数之前运行,因此派生类可能在可能被重写的虚拟成员中使用的任何实例字段尚未初始化。
请注意,这只是一个警告,让您注意并确保一切正常。此方案有实际用例,您只需要记录虚拟成员的行为,即它不能使用在调用它的构造函数所在的派生类中声明的任何实例字段。
构造用 C# 编写的对象时,发生的情况是初始值设定项按从最派生类到基类的顺序运行,然后构造函数按从基类到最派生类的顺序运行(有关为什么会这样的详细信息,请参阅 Eric Lippert 的博客)。
此外,在 .NET 中,对象在构造时不会更改类型,而是从最派生的类型开始,方法表用于最派生的类型。这意味着虚拟方法调用始终在最派生的类型上运行。
当您将这两个事实结合起来时,您就会遇到一个问题,即如果在构造函数中进行虚拟方法调用,并且它不是其继承层次结构中派生最多的类型,则它将在构造函数尚未运行的类上调用,因此可能不处于调用该方法的合适状态。
当然,如果将类标记为密封,以确保它是继承层次结构中最派生的类型,则此问题会得到缓解 - 在这种情况下,调用 virtual 方法是完全安全的。
评论
构造函数可以(稍后在软件的扩展中)从重写 virtual 方法的子类的构造函数调用。现在不是子类的函数实现,而是基类的实现将被调用。因此,在这里调用虚拟函数实际上没有意义。
但是,如果您的设计满足 Liskov 替代原则,则不会造成任何损害。可能这就是它被容忍的原因 - 警告,而不是错误。
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 之间的规则非常不同。您的程序员可能不知道会发生什么!
评论
警告的原因已经描述过,但您将如何修复警告?您必须密封类或虚拟成员。
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();
}
}
评论
A
A() { base.Foo(); }
Foo()
B
A
这个问题的一个重要方面是,如果基类期望它这样做,那么基类从其构造函数中调用虚拟成员是安全的。在这种情况下,派生类的设计者负责确保在构造完成之前运行的任何方法在特定情况下的行为都尽可能合理。例如,在 C++/CLI 中,构造函数包装在代码中,如果构造失败,这些代码将调用部分构造的对象。在这种情况下调用通常是必要的,以防止资源泄漏,但必须为运行它们所基于的对象可能尚未完全构造的可能性做好准备。Dispose
Dispose
Dispose
上面有很好的答案,说明为什么你不想这样做。这里有一个反例,也许你会想这样做(从 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 { }
评论
Finalize
Object
ManageLifetime(LifetimeStatus)
GetDependency
MySubClass
我发现的另一件有趣的事情是,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";
}
}
评论
一个重要的缺失点是,解决此问题的正确方法是什么?
正如 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();
}
}
改进的实现从基类构造函数中删除虚拟方法,并使用方法。创建一个新实例 显示预期的“DerivedFromBetter”Initialize
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);
}
}
评论
bool initialize
Initialize
base(false)
只是为了补充我的想法。如果在定义私有字段时始终对其进行初始化,则应避免此问题。至少下面的代码就像一个魅力:
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());
}
}
评论
该警告提醒您,虚拟成员可能会在派生类上被覆盖。在这种情况下,父类对虚拟成员所做的任何操作都将通过重写子类来撤消或更改。为了清楚起见,请看小例子打击
下面的父类尝试在其构造函数上将值设置为虚拟成员。这将触发 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"
}
}
评论
谨防盲目听从雷夏普的建议,让班级封存! 如果它是 EF Code First 中的模型,它将删除 virtual 关键字,这将禁用其关系的延迟加载。
public **virtual** User User{ get; set; }
我只需将 Initialize() 方法添加到基类中,然后从派生构造函数中调用该方法。该方法将在所有构造函数执行后调用任何虚拟/抽象方法/属性:)
评论
我认为,如果您想让子类能够设置或覆盖父构造函数将立即使用的属性,那么忽略警告可能是合法的:
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
评论