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

时间:2013-09-18 21:37:59

标签: c#

我有一个用于格式化电子邮件的类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个其他的tempaltes,但是90%的代码是可重用的。这就是为什么我选择创建BaseEmailTemplate(主题主题)。 BaseTemplate会创建主题和列表以及SendEmail期望读取的其他内容。

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

我不希望任何跟随我工作的人必须知道

 var newEmail = new NewEmailTemplate();
 newEmail.Init(topic);
每次使用都需要

。没有它,该对象将无法使用。我以为有很多警告?

5 个答案:

答案 0 :(得分:3)

<{3}}的

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

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

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

正如已经提到的,在这种情况下,派生方法似乎只依赖于作为参数传递的项目,并且可能没有问题地运行。

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

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

答案 1 :(得分:2)

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

在基类中:

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

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

答案 2 :(得分:1)

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

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

答案 3 :(得分: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;
}

答案 4 :(得分: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的可能性,并处理它为null的情况。但是我认为您还是应该这样做。有人可以将空主题显式传递给原始构造函数
  • 您实际上不需要公开公开Topic属性。也许您仍然打算这样做,但如果不是这样,那可能不是很理想。您可以删除吸气剂,但看起来有点奇怪
  • 如果您具有多个相互依赖的属性,则样板代码将增加。您可以尝试将所有这些分组到一个类中,以便只有一个设置器仍会触发抽象方法