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

问题描述 投票:3回答:5

我有一个类,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创建了诸如Subject和List之类的东西以及SendEmail期望读取的其他东西。

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

我宁愿不要求任何跟随我工作的人必须知道这一点

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

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

c#
5个回答
3
投票

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

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

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

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

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

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


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

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


1
投票

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

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


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

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的情况。但无论如何,我认为你应该这样做;有人可以显式地将null主题传递给原始构造函数
  • 你是公开暴露Topic财产而不是真的需要。也许你打算这样做但是,如果没有,它可能不是很理想。你可以删除吸气剂,但这可能看起来有点奇怪
  • 如果您有多个相互依赖的属性,则样板代码会增加。您可以尝试将所有这些组合成一个类,这样只有一个setter仍会触发抽象方法
© www.soinside.com 2019 - 2024. All rights reserved.