虚拟件呼吁在一个构造
-
02-07-2019 - |
题
我得到一个警告,从ReSharper关于一个叫到一个虚拟的成员从我的物体构造。
为什么这个是什么不做?
解决方案
当构造用C#编写的对象时,会发生的情况是初始化程序按从最派生类到基类的顺序运行,然后构造函数按顺序从基类运行到最派生类(请参阅Eric Lippert的博客,了解详情为何 。
同样在.NET对象中,不会在构造时更改类型,而是从最派生类型开始,方法表用于最派生类型。这意味着虚方法调用始终在最派生类型上运行。
当你将这两个事实结合起来时,你会遇到这样的问题:如果你在构造函数中进行虚方法调用,并且它不是其继承层次结构中派生最多的类型,那么它将在一个构造函数的类上调用尚未运行,因此可能没有适当的状态来调用该方法。
当然,如果您将类标记为已密封以确保它是继承层次结构中派生类型最多的类型,则可以缓解此问题 - 在这种情况下,调用虚方法是完全安全的。
其他提示
为了回答您的问题,请考虑以下问题:当 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
,您可以介绍继承对象在完全初始化之前执行代码的可能性。
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();
}
}
在C#中,基类'构造函数在派生类'构造函数之前运行,因此派生类可能在可能被覆盖的虚拟成员中使用的任何实例字段都尚未初始化。
请注意,这只是一个警告,让您注意并确保它是正确的。这个场景有实际的用例,您只需要记录虚拟成员的行为,它不能使用在构造函数调用它的下面的派生类中声明的任何实例字段。 / p>
上面有很好的答案,为什么不想想要这样做。这是一个反例,也许你 希望这样做(从 Ruby中实用的面向对象设计,Sandi Metz,第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 { }
是的,在构造函数中调用虚方法通常很糟糕。
此时,objet可能尚未完全构建,方法所预期的不变量可能尚未成熟。
因为在构造函数完成执行之前,该对象未完全实例化。虚函数引用的任何成员都可能未初始化。在C ++中,当您在构造函数中时, this
仅引用您所在构造函数的静态类型,而不是正在创建的对象的实际动态类型。这意味着虚函数调用可能甚至不会达到您期望的范围。
您的构造函数(稍后,在您的软件的扩展中)可以从覆盖虚方法的子类的构造函数中调用。现在不是子类的函数实现,但是将调用基类的实现。因此,在这里调用虚函数并没有多大意义。
但是,如果您的设计符合Liskov替换原则,则不会造成任何伤害。可能这就是它被容忍的原因 - 警告,而不是错误。
一个重要方面,这个问题,其他的答案尚未解决的是,它是安全的一个基类叫虚拟的成员在其构造 如果那是什么生类期望它做的事.在这种情况下,所设计的源类是负责确保任何方法是运行之前建设完成,将表现为理智,因为他们可以的情况下。例如,在C++/CLI的构造是包裹在代码,将电话 Dispose
在部分-建造的目的,如果建设失败。叫 Dispose
在这种情况往往是必要的,以防止泄漏资源,但是 Dispose
方法必须准备为这种可能性,目的在它们的运行可能没有完全建成。
警告提醒您虚拟成员可能会在派生类上被覆盖。在这种情况下,父类对虚拟成员所做的任何操作都将通过覆盖子类来撤消或更改。为了清晰起见,请看一下小例子
下面的父类尝试在其构造函数上为虚拟成员设置值。这将触发重新锐化警告,让我们看看代码:
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"
}
}
小心谨慎地遵循Resharper的建议,让课堂密封! 如果它是EF Code First中的模型,它将删除虚拟关键字,这将禁用其关系的延迟加载。
public **virtual** User User{ get; set; }
一个重要缺点是,什么是正确的方法来解决这个问题呢?
作为 格雷格解释, 根里的问题是一个基类的构造将调用虚拟件之前得出类已经建成。
以下代码,从 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);
}
}
在这种特定情况下,C ++和C#之间存在差异。 在C ++中,对象未初始化,因此在构造函数中调用虚拟函数是不安全的。 在C#中创建类对象时,其所有成员都初始化为零。可以在构造函数中调用虚函数,但是如果您可以访问仍为零的成员。如果您不需要访问成员,则在C#中调用虚函数是非常安全的。
只是添加我的想法。如果在定义私有字段时始终初始化私有字段,则应避免此问题。至少下面的代码就像一个魅力:
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());
}
}
我发现另一个有趣的事情是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";
}
}
我只是将一个Initialize()方法添加到基类中,然后从派生构造函数中调用它。在执行了所有构造函数之后,该方法将调用任何虚拟/抽象方法/属性:)