构造函数中的虚拟成员,解决方法

本文关键字:解决 方法 虚拟成员 构造函数 | 更新日期: 2023-09-27 18:31:48

我有一个类,BaseEmailTemplate,它格式化电子邮件,我想创建一个可以推翻默认值的派生类型。最初是我的基本构造函数 -

public BaseEmailTemplate(Topic topic)
        {
                CreateAddresses(topic);
                CreateSubject(topic);
                CreateBody(topic);
        }
... (Body/Addresses)
protected virtual void CreateSubject(Topic topic)
    {
        Subject = string.Format("Base boring format: {0}", topic.Name);
    }

而在我派生的

public NewEmailTemplate(Topic topic) : Base (topic)
        {
            //Do other things
        }
protected override void CreateSubject(Topic topic)
        {
            Subject = string.Format("New Topic: {0} - {1})", topic.Id, topic.Name);
        }

当然,这会导致此处讨论的错误:构造函数中的虚拟成员调用

所以直言不讳 - 我不想在每个派生类型中调用相同的方法。另一方面,我需要能够更改任何/所有。我知道另一个基地有不同的地址子集,但正文和主题将是默认值。

必须调用所有三个方法,并且更改其中任何一个方法的功能需要在每个派生的基础上可用。

我的意思是,每个人似乎都在说使用虚拟的意外后果似乎是我的确切意图。或者也许我太深了,太专注了?

更新 - 澄清

理解为什么构造函数中的虚拟成员不好,我很欣赏关于该主题的答案,尽管我的问题不是"为什么这很糟糕?"而是"好吧,这很糟糕,但我看不出有什么更好的满足我的需求,所以我该怎么办?

这就是它目前的实施方式

 private void SendNewTopic(TopicDTO topicDto)
        {
            Topic topic = Mapper.Map<TopicDTO , Topic>(topicDto);
            var newEmail = new NewEmailTemplate(topic);
            SendEmail(newEmail);  //Preexisting Template Reader infrastructure
            //Logging.....
        }

我正在处理一个孩子和孙子。我进来的地方只有新的电子邮件模板,但我现在必须构建其他 4 个临时模板,但 90% 的代码是可重用的。这就是为什么我选择创建BaseEmailTemplate(主题主题)。BaseTemplate创建诸如主题和列表之类的内容以及SendEmail期望阅读的其他内容。

  NewEmailTemplate(Topic topic): BaseEmailTemplate(Topic topic): BaseTemplate, IEmailTempate

宁愿不必要求任何关注我工作的人必须知道

 var newEmail = new NewEmailTemplate();
 newEmail.Init(topic);

每次使用时都需要。没有它,该对象将无法使用。我以为有很多警告?

构造函数中的虚拟成员,解决方法

C# 规范的

[10.11] 告诉我们,对象构造函数按顺序运行,从基类排在第一位,到最后继承最多的类。 而规范的 [10.6.3] 告诉我们,它是在运行时执行的虚拟成员的最派生实现。

这意味着,如果基对象构造函数访问由派生

类初始化的项,则在尝试从基对象构造函数运行派生方法时可能会收到Null Reference Exception,因为派生对象尚未运行其构造函数。

实际上,Base 方法的构造函数运行 [10.11] 并尝试在构造函数完成并且可以运行派生构造函数之前CreateSubject()引用派生方法,从而使该方法存在问题。

如前所述,在这种情况下,派生方法似乎只依赖于作为参数传递的项目,并且很可能运行而不会出现问题。

请注意,这是一个警告,本身不是错误,而是指示运行时可能发生错误。

如果从除

基类构造函数之外的任何其他上下文调用方法,则不会有问题。

工厂方法和初始化函数是这种情况的有效解决方法。

在基类中:

private EmailTemplate()
{
   // private constructor to force the factory method to create the object
}
public static EmailTemplate CreateBaseTemplate(Topic topic)
{
    return (new BaseEmailTemplate()).Initialize(topic);
}
protected EmailTemplate Initialize(Topic topic)
{
   // ...call virtual functions here
   return this;
}

在派生类中:

public static EmailTemplate CreateDerivedTemplate(Topic topic)
{
    // You do have to copy/paste this initialize logic here, I'm afraid.
    return (new DerivedEmailTemplate()).Initialize(topic);
}
protected override CreateSubject...

创建对象的唯一公开方法是通过工厂方法,因此您不必担心最终用户忘记调用初始化。 当您想要创建更多派生类时,扩展起来并不那么简单,但对象本身应该非常有用。

解决方法

是使用构造函数初始化private readonly Topic _topic字段,然后将三个方法调用移动到派生类型可以在其构造函数中安全调用的protected void Initialize()方法,因为当该调用发生时,基构造函数已经执行。

可疑的部分是派生类型需要记住进行该Initialize()调用。

@Tanzelax:看起来不错,除了Initialize总是返回EmailTemplate。因此,静态工厂方法不会那么黯淡:

public static DerivedEmailTemplate CreateDerivedTemplate(Topic topic)
{
    // You do have to copy/paste this initialize logic here, I'm afraid.
    var result = new DerivedEmailTemplate();
    result.Initialize(topic);
    return result;
}

这个答案主要是为了完整性,以防万一这些天有人偶然发现了这个问题(像我一样)。

为了避免单独的Init方法,同时仍然保持简单,代码用户感觉更自然(IMO)的一件事是将Topic作为基类的属性:

// This:
var newEmail = new NewEmailTemplate { Topic = topic };
// Instead of this:
var newEmail = new NewEmailTemplate();
newEmail.Init(topic);

然后,属性 setter 可以负责调用抽象方法,例如:

public abstract class BaseEmailTemplate
{
    // No need for even a constructor
    private Topic topic;
    public Topic
    {
        get => topic;
        set
        {
            if (topic == value)
            {
                return;
            }
            topic = value;
            // Derived methods could also access the topic
            // as this.Topic instead of as an argument
            CreateAddresses(topic);
            CreateSubject(topic);
            CreateBody(topic);
        }
    }
    protected abstract void CreateAddresses(Topic topic);
    protected abstract void CreateSubject(Topic topic);
    protected abstract void CreateBody(Topic topic);
}

优点:

  • 电子邮件模板可以用直观的语法在一行中定义
  • 不涉及工厂方法或第三类
  • 派生类只需要担心重写抽象方法,而不需要考虑调用基构造函数(但您可能仍希望将其他变量作为构造函数参数传递)

缺点:

  • 你仍然需要考虑用户忘记定义Topic的可能性,并处理它为空的情况。但我认为无论如何你都应该这样做;有人可以显式地将空主题传递给原始构造函数
  • 您是在公开公开Topic财产,而实际上不需要。也许你打算这样做,但如果没有,它可能不是很理想。您可以删除吸气器,但这看起来可能有点奇怪
  • 如果有多个相互依赖的属性,则样板代码将增加。您可以尝试将所有这些分组到一个类中,以便只有一个 setter 仍然触发抽象方法