【问题标题】:How to Avoid Calling Viritual Methods from a Base Constructor如何避免从基构造函数调用虚拟方法
【发布时间】:2010-11-12 10:07:43
【问题描述】:

我在库中有一个抽象类。我试图尽可能简单地正确实现这个类的派生。麻烦的是我需要在一个三步过程中初始化对象:获取一个文件,执行几个中间步骤,然后使用该文件。第一步和最后一步是派生类特有的。这是一个精简的示例。

abstract class Base
{
    // grabs a resource file specified by the implementing class
    protected abstract void InitilaizationStep1();

    // performs some simple-but-subtle boilerplate stuff
    private void InitilaizationStep2() { return; }

    // works with the resource file
    protected abstract void InitilaizationStep3();

    protected Base()
    {
        InitilaizationStep1();
        InitilaizationStep2();
        InitilaizationStep3();
    }
}

问题当然在于构造函数中的虚方法调用。如果不能指望派生类被完全初始化,恐怕图书馆的使用者在使用该类时会发现自己受到限制。

我可以将构造函数中的逻辑提取到受保护的Initialize() 方法中,但随后实现者可能会直接调用Step1()Step3(),而不是调用Initialize()。问题的症结在于,如果跳过Step2(),就不会出现明显的错误;只是在某些情况下表现糟糕。

我觉得无论哪种方式,图书馆的未来用户都必须解决一个严重且不明显的“问题”。我应该使用其他一些设计来实现这种初始化吗?

如有需要,我可以提供更多详细信息;我只是想提供一个表达问题的最简单的例子。

【问题讨论】:

    标签: c# inheritance constructor virtual-functions


    【解决方案1】:

    这对于任何类的构造函数来说都太多了,更不用说基类了。我建议您将其分解为单独的 Initialize 方法。

    【讨论】:

    • 我同意。另一种方法是解决此对象在创建过程中的依赖关系。
    • 我喜欢您的解决方案,但要考虑的另一种可能性可能是将步骤 1 和 3 中的代码分解为一对传递给构造函数的接口。构造函数可以在适当的时候调用它们来完成它们的工作。简而言之,这很可能是组合胜过继承的地方之一。
    • 这并没有解决他对不按顺序调用步骤和简化派生类创建方法的担忧。
    • 首先,除非这个类层次结构中的某些东西使得 init 总是按照这个顺序执行三个步骤,否则它不应该由基类强加。其次,如果固有的,那么基类Initialize方法可以依次调用虚拟的Step1、Step2、Step3初始化方法:模板函数模式。
    • 顺序是固有的。基本上,在 Step1 中加载了一个文件,Step3 定义了从程序数据到文件的绑定。因为绑定过程很昂贵,所以 Step2 确保绑定是惰性的和缓存的。我当前的实现基本上是模板方法模式,但是 C# 中安全、惯用的 TMP 需要在基本构造函数或调用代码中显式调用 Initialize() 方法。前者比后者更可取,但在任何一种情况下失败的后果都可能是在代码被发送到野外很长时间后发生巨大的内存消耗。
    【解决方案2】:

    编辑:出于某种原因,我为 C++ 回答了这个问题。抱歉。 对于 C#,我建议不要使用 Create() 方法 - 使用构造函数并确保对象从一开始就处于有效状态。 C# 允许来自构造函数的虚拟调用,如果您仔细记录它们的预期功能和前置条件和后置条件,则可以使用它们。我第一次推断出 C++,因为它不允许来自构造函数的虚拟调用。

    制作单独的初始化函数private。可以是privatevirtual。然后提供一个公共的、非虚拟的 Initialize() 函数,以正确的顺序调用它们。

    如果您想确保在创建对象时一切都发生,请创建构造函数protected,并在您的类中使用静态Create() 函数,该函数调用Initialize(),然后返回新创建的对象。

    【讨论】:

    • 私有虚函数有什么意义 - 它不能被覆盖。
    • @John:对不起。 C# 允许来自构造函数的虚拟调用,如果您仔细记录它们的预期功能和前置条件和后置条件,则可以使用它们。我第一次推断出 C++,因为它不允许来自构造函数的虚拟调用。
    • 在我看来,Create() 方法类似于工厂模式。虽然它的限制有点多,但就 API 大小而言,它的占用空间要小得多,并且可能满足我在这种情况下的需求。您确实建议反对它;我忽略了一些关键限制吗?这是我以前使用过的一种模式(对于不太重要的代码),它似乎工作得很好。说服我这是一个坏主意,否则我会认真考虑。 :)
    【解决方案3】:

    在很多情况下,初始化都涉及到分配一些属性。可以使这些属性本身abstract 并让派生类覆盖它们并返回一些值,而不是将值传递给基构造函数进行设置。当然,这个想法是否适用取决于你的具体班级的性质。反正构造函数里有这么多代码很臭。

    【讨论】:

    • 一个有趣的想法,但不幸的是不适用于我的情况。还是谢谢!
    【解决方案4】:

    乍一看,我建议将这种逻辑移至依赖此初始化的方法中。像

    public class Base
    {
       private void Initialize()
       {
          // do whatever necessary to initialize
       }
    
       public void UseMe()
       {
          if (!_initialized) Initialize();
          // do work
       }
    }
    

    【讨论】:

    【解决方案5】:

    我会考虑创建一个abstract factory,它负责使用template method 初始化和初始化派生类的实例。

    举个例子:

    public abstract class Widget
    {
        protected abstract void InitializeStep1();
        protected abstract void InitializeStep2();
        protected abstract void InitializeStep3();
    
        protected internal void Initialize()
        {
            InitializeStep1();
            InitializeStep2();
            InitializeStep3();
        }
    
        protected Widget() { }
    }
    
    public static class WidgetFactory
    {
        public static CreateWidget<T>() where T : Widget, new()
        {
            T newWidget = new T();
            newWidget.Initialize();
            return newWidget;
        }
    }
    
    // consumer code...
    var someWidget = WidgetFactory.CreateWidget<DerivedWidget>();
    

    这个工厂代码可以显着改进——特别是如果你愿意使用 IoC 容器来处理这个责任......

    如果您无法控制派生类,您可能无法阻止它们提供可调用的公共构造函数 - 但至少您可以建立消费者可以遵守的使用模式。

    阻止您的类的用户自爆并非总是可能的 - 但是,您可以提供基础设施来帮助消费者在熟悉设计时正确使用您的代码。

    【讨论】:

    • 他的问题是关于实现抽象类的清晰方法,而不是派生版本的创建。
    • 我不同意。他正在寻找派生类将执行结构化初始化的模式 - 他担心这些派生类可能无法以适当的顺序执行所有必要的初始化步骤。同时执行构造和初始化的工厂可以帮助解决这个问题。特别是因为在构造函数中调用虚方法的替代方法(通常)是一种不受欢迎的做法。
    • LBushkin 对我的需求是正确的。我不确定您的建议是抽象工厂的字面实现,但我确实喜欢泛型减轻实现者的负担,因为不需要 DerivedFactory 类。要求 Derived 的作者保护构造函数是相当合理的。我主要关心的是调用代码的简单性所付出的代价。此外,为了 API 的一致性,我可能希望将所有对象生成转移到工厂中。总体而言,这可能是有益的,但这是我必须考虑的事情。
    • quote "我正在努力让正确实现此类的派生变得尽可能简单"
    • 如果客户端只是通过调用默认构造函数直接创建一个 Widget 继承者的实例,它将如何提供帮助?如果客户不知道应该使用某种工厂怎么办?
    【解决方案6】:

    由于第 1 步“获取文件”,因此最好使用 Initialize(IBaseFile) 并跳过第 1 步。这样消费者可以随心所欲地获取文件 - 因为它无论如何都是抽象的。您仍然可以提供一个“StepOneGetFile()”作为返回文件的抽象,因此他们可以选择以这种方式实现它。

    DerivedClass foo = DerivedClass();
    foo.Initialize(StepOneGetFile('filepath'));
    foo.DoWork();
    

    【讨论】:

    • 我喜欢将单独的对象推入初始化方法的想法;它消除了由 Step1() 和 Step3() 方法创建的潜在漏洞。它给派生类增加了一点负担,但它使得只有一种方法可以使用基类。路更长,但更直。我想我更愿意根据自己的需要对 Initialize 进行保护,但原则是一样的。
    【解决方案7】:

    您可以使用以下技巧来确保以正确的顺序执行初始化。据推测,您在基类中实现了一些其他方法(DoActualWork),它们依赖于初始化。

    抽象类基 { 私人布尔_初始化; 受保护的抽象 void InitilaizationStep1(); 私人无效InitilaizationStep2(){返回; } 受保护的抽象 void InitilaizationStep3(); 受保护的初始化() { // 这里调用虚方法是安全的 初始化步骤1(); 初始化步骤2(); 初始化步骤3(); // 将对象标记为正确初始化 _initialized = true; } 公共无效 DoActualWork() { if (!_initialized) 初始化(); Console.WriteLine("我们现在肯定已经初始化了"); } }

    【讨论】:

    • 当我研究使用这种模式时,我发现我的班级有太多的入口点,无法使这种方法干净实用。特别是,接收绑定信息并以各种格式检索该信息有许多重载。反过来,这表明我可能想将这些职责重构为一个新类。无论如何,好主意,虽然可能有点hacky。当多个方法需要适当的初始化时,分解出通用代码不是带您走向工厂模式吗?无论如何,感谢您的建议和启发。
    【解决方案8】:

    我不会这样做。我通常发现在构造函数中做任何“真正的”工作最终都是一个坏主意。

    至少有一个单独的方法来从文件中加载数据。你可以提出一个论点,让它更进一步,让一个单独的对象负责从文件中构建你的一个对象,将“从磁盘加载”的关注点与对象的内存中操作分开。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-03-17
      • 2013-04-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-09-02
      相关资源
      最近更新 更多