如何避免从基础构造函数调用虚方法

时间:2009-07-20 19:04:02

标签: c# inheritance constructor virtual-method

我在库中有一个抽象类。我正在尝试尽可能简单地正确实现此类的派生。问题是我需要在三个步骤中初始化对象:获取文件,执行一些中间步骤,然后使用该文件。第一步和最后一步特别适用于派生类。这是一个精简的例子。

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()则不会出现明显的错误;在某些情况下表现糟糕。

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

如有必要,我可以提供更多细节;我只是想提供表达问题的最简单的例子。

8 个答案:

答案 0 :(得分:10)

我会考虑创建一个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容器来处理这个责任......

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

并不总是可以阻止您的课程用户自己动手 - 但是,您可以提供基础设施来帮助消费者在熟悉设计时正确使用您的代码。

答案 1 :(得分:3)

这太过于放置在任何类的构造函数中,更不用说基类了。我建议您将其分解为单独的Initialize方法。

答案 2 :(得分:1)

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

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

如果要确保在创建对象时发生所有事情,请在返回新创建的对象之前创建构造函数protected并在调用Create()的类中使用静态Initialize()函数对象

答案 3 :(得分:1)

在很多情况下,初始化内容涉及分配一些属性。可以自己创建这些属性abstract并使派生类覆盖它们并返回一些值,而不是将值传递给要设置的基础构造函数。当然,这个想法是否适用取决于您特定班级的性质。无论如何,在构造函数中有那么多代码是臭的。

答案 4 :(得分:1)

乍一看,我建议将这种逻辑移到依赖于这种初始化的方法上。像

这样的东西
public class Base
{
   private void Initialize()
   {
      // do whatever necessary to initialize
   }

   public void UseMe()
   {
      if (!_initialized) Initialize();
      // do work
   }
}

答案 5 :(得分:1)

由于步骤1“抓取文件”,最好使用Initialize(IBaseFile)并跳过步骤1.这样消费者可以随意获取文件 - 因为它仍然是抽象的。你仍然可以提供一个'StepOneGetFile()'作为返回文件的抽象,因此他们可以选择实现它。

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

答案 6 :(得分:1)

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

abstract class Base
{
    private bool _initialized;

    protected abstract void InitilaizationStep1();
    private void InitilaizationStep2() { return; }
    protected abstract void InitilaizationStep3();

    protected Initialize()
    {
        // it is safe to call virtual methods here
        InitilaizationStep1();
        InitilaizationStep2();
        InitilaizationStep3();

        // mark the object as initialized correctly
        _initialized = true;
    }

    public void DoActualWork()
    {
        if (!_initialized) Initialize();
        Console.WriteLine("We are certainly initialized now");
    }
}

答案 7 :(得分:0)

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

至少要有一个单独的方法来加载文件中的数据。您可以创建一个参数,使其更进一步,并有一个单独的对象负责从文件构建您的一个对象,分离“从磁盘加载”和对象的内存中操作的关注。