3

我有一个类 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);
        }

当然这会导致这里讨论的错误:Virtual member call in a constructor

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

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

我的意思是每个人似乎都在说的事情是使用虚拟的意外后果似乎是我的确切意图..或者也许我太深并且专注于单一?

更新-澄清

我理解为什么构造函数中的虚拟成员不好,我很欣赏有关该主题的答案,尽管我的问题不是“为什么这不好?” “好吧,这很糟糕,但我看不出有什么能更好地满足我的需要,那我该怎么办?”

这是目前的实施方式

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

            //Logging.....
        }

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

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

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

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

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

4

5 回答 5

3

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

在基类中:

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...

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

于 2013-09-19T12:34:11.647 回答
3

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

这意味着Null Reference Exception当您尝试从基对象构造函数运行派生方法时,如果它访问由派生类初始化的项,则您可能会收到 a,因为派生对象尚未运行其构造函数。

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

如前所述,在这种情况下,派生方法似乎只依赖于作为参数传递的项目,并且可以毫无问题地运行。

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

如果方法是从除基类构造函数之外的任何其他上下文调用的,这将不是问题。

于 2013-09-18T22:06:10.467 回答
1

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

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

于 2013-09-18T22:16:22.630 回答
0

@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;
}
于 2018-05-11T22:18:43.817 回答
0

这个答案主要是为了完整性,以防最近有人偶然发现这个问题(比如我)。

为了在保持简单的同时避免使用单独的Init方法,对于代码用户来说可能感觉更自然(IMO)的一件事是Topic作为基类的属性:

// This:
var newEmail = new NewEmailTemplate { Topic = topic };

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

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

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 仍然触发抽象方法
于 2019-02-22T16:07:06.267 回答