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

发布于 2024-07-29 10:26:03 字数 1024 浏览 7 评论 0原文

我在图书馆有一个抽象类。 我试图尽可能轻松地正确实现此类的派生。 问题是我需要通过三步过程来初始化对象:获取文件,执行一些中间步骤,然后使用该文件。 第一步和最后一步是派生类特有的。 这是一个精简的示例。

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

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

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

I have an abstract class in a library. I'm trying to make it as easy as possible to properly implement a derivation of this class. The trouble is that I need to initialize the object in a three-step process: grab a file, do a few intermediate steps, and then work with the file. The first and last step are particular to the derived class. Here's a stripped-down example.

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

The trouble, of course, is the virtual method call in the constructor. I'm afraid that the consumer of the library will find themselves constrained when using the class if they can't count on the derived class being fully initialized.

I could pull the logic out of the constructor into a protected Initialize() method, but then the implementer might call Step1() and Step3() directly instead of calling Initialize(). The crux of the issue is that there would be no obvious error if Step2() is skipped; just terrible performance in certain situations.

I feel like either way there is a serious and non-obvious "gotcha" that future users of the library will have to work around. Is there some other design I should be using to achieve this kind of initialization?

I can provide more details if necessary; I was just trying to provide the simplest example that expressed the problem.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

枫以 2024-08-05 10:26:03

我会考虑创建一个抽象工厂,它负责使用用于初始化的模板方法

举个例子:

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 容器来处理这个责任......

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

虽然并不总是能够阻止您的类的用户搬起石头砸自己的脚,但是,您可以提供基础设施来帮助消费者在熟悉设计后正确使用您的代码。

I would consider creating an abstract factory that is responsible for instantiating and initializing instances of your derived classes using a template method for initialization.

As an example:

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

This factory code could be improved dramatically - especially if you are willing to use an IoC container to handle this responsibility...

If you don't have control over the derived classes, you may not be able to prevent them from offering a public constructor that can be called - but at least you can establish a usage pattern that consumers could adhere to.

It's not always possible to prevent users of you classes from shooting themselves in the foot - but, you can provide infrastructure to help consumers use your code correctly when they familiarize themselves with the design.

小红帽 2024-08-05 10:26:03

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

That's way too much to place in the constructor of any class, much less of a base class. I suggest you factor that out into a separate Initialize method.

时光是把杀猪刀 2024-08-05 10:26:03

在很多情况下,初始化工作涉及分配一些属性。 可以使这些属性本身抽象,并让派生类重写它们并返回一些值,而不是将该值传递给基构造函数进行设置。 当然,这个想法是否适用取决于你具体班级的性质。 不管怎样,构造函数中有这么多代码是很臭的。

In lots of cases, initialization stuff involves assigning some properties. It's possible to make those properties themselves abstract and have derived class override them and return some value instead of passing the value to the base constructor to set. Of course, whether this idea is applicable depends on the nature of your specific class. Anyway, having that much code in the constructor is smelly.

舂唻埖巳落 2024-08-05 10:26:03

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

public class Base
{
   private void Initialize()
   {
      // do whatever necessary to initialize
   }

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

At first sight, I would suggest to move this kind of logic to the methods relying on this initialization. Something like

public class Base
{
   private void Initialize()
   {
      // do whatever necessary to initialize
   }

   public void UseMe()
   {
      if (!_initialized) Initialize();
      // do work
   }
}
鱼忆七猫命九 2024-08-05 10:26:03

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

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

Since step 1 "grabs a file", it might be good to have Initialize(IBaseFile) and skip step 1. This way the consumer can get the file however they please - since it is abstract anyways. You can still offer a 'StepOneGetFile()' as abstract that returns the file, so they could implement it that way if they choose.

DerivedClass foo = DerivedClass();
foo.Initialize(StepOneGetFile('filepath'));
foo.DoWork();
是伱的 2024-08-05 10:26:03

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

将各个初始化函数设为私有。 可以是私有虚拟。 然后提供一个公共的非虚拟 Initialize() 函数,以正确的顺序调用它们。

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

Edit: I answered this for C++ for some reason. Sorry. For C# I recommend against a Create() method - use the constructor and make sure the objects stays in a valid state from the start. C# allows virtual calls from the constructor, and it's OK to use them if you carefully document their expected function and pre- and post-conditions. I inferred C++ the first time through because it doesn't allow virtual calls from the constructor.

Make the individual initialization functions private. The can be both private and virtual. Then offer a public, non-virtual Initialize() function that calls them in the correct order.

If you want to make sure everything happens as the object is created, make the constructor protected and use a static Create() function in your classes that calls Initialize() before returning the newly created object.

勿忘初心 2024-08-05 10:26:03

您可以使用以下技巧来确保以正确的顺序执行初始化。 据推测,您在基类中实现了一些其他依赖于初始化的方法 (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");
    }
}

You could employ the following trick to make sure that initialization is performed in the correct order. Presumably, you have some other methods (DoActualWork) implemented in the base class, that rely on the initialization.

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");
    }
}
愿得七秒忆 2024-08-05 10:26:03

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

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

I wouldn't do this. I generally find that doing any "real" work in a constructor ends up being a bad idea down the road.

At the minimum, have a separate method to load the data from a file. You could make an argument to take it a step further and have a separate object responsible for building one of your objects from file, separating the concerns of "loading from disk" and the in-memory operations on the object.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文