C#中的虚方法或事件

时间:2009-12-16 20:26:55

标签: c# design-patterns

我目前正在编写一个小型C#库,以简化实施小型物理模拟/实验。

主要组件是SimulationForm,它在内部运行定时器循环并隐藏用户的样板代码。实验本身只能通过三种方法来定义:

  1. Init()(初始化所有内容)
  2. Render(Graphics g)(渲染当前模拟状态)
  3. Move(double dt)(移动实验dt秒)
  4. 我只是想知道让用户实现这些功能的更好选择是什么:

    1)由继承表格覆盖的虚拟方法

    protected virtual void Init() {} 
    ...
    

    2)活动

    public event EventHandler<MoveSimulationEventArgs> Move = ...
    ...
    

    修改:请注意,方法不应该是 abstract 。实际上,还有更多, 没有实现。由于许多模拟不需要它们,因此将它们放在外面通常很方便。

    关于这是一个“正常形式”的很酷的事情是你可以写

    partial class frmMyExperiment : SimulationForm {
    }
    

    并且您完全能够与设计器以及所有继承的控件和设置/属性进行交互。我不想通过采用完全不同的方法来失去这些功能。

8 个答案:

答案 0 :(得分:17)

在这种情况下,我更喜欢使用虚拟方法。

此外,如果方法必需才能运作,您可以将班级设为abstract class。这在可用性方面是最好的,因为它会导致编译器强制使用。如果用户试图在不实现方法的情况下使用您的类/表单,编译器会抱怨。

事件在这里有点不合适,因为这不是你想要的不仅仅是单个实现(事件允许多个订阅者)的东西,从概念上讲,它更像是对象的功能,而不是由对象引起的通知。

答案 1 :(得分:15)

有趣的是,我不得不在最近工作的设计中做出类似的选择。一种方法并不严格优于另一种方法。

在你的例子中,没有额外的信息,我可能会选择虚拟方法 - 特别是因为我的直觉让我相信你会使用继承来模拟不同类型的实验,而你不需要拥有多个订阅者的能力(事件允许)。

以下是关于在这些模式之间进行选择的一些一般性观察:

活动很棒:

  1. 当您不需要调用者返回任何信息时,以及何时需要可扩展性而不需要子类化。
  2. 如果您希望允许呼叫者拥有多个可以收听并响应该事件的订阅者。
  3. 因为他们自然会将自己安排到template method模式中 - 这有助于避免引入fragile base class problem
  4. 事件的最大问题是管理订阅者的生命周期可能会变得棘手,并且当订阅者订阅时间超过必要时,您可能会引入泄漏甚至功能缺陷。第二个最大的问题是,允许多个订阅者可以创建一个混乱的实现,其中各个订阅者互相踩踏 - 或者表现出顺序依赖性。

    虚拟方法运作良好:

    1. 当你只希望继承者能够改变一个类的行为时。
    2. 当您需要从呼叫中返回信息时(哪个事件不容易支持)
    3. 当您只想要一个订阅者到特定扩展点时
    4. 当您希望衍生品的衍生品能够覆盖行为时。
    5. 虚拟方法的最大问题是您可以轻松地将脆弱的基类问题引入到您的实现中。虚方法本质上是与派生类的契约,您必须清楚地记录它们,以便继承者可以提供有意义的实现。

      虚拟方法的第二大问题是它可以引入深度或广泛的继承树,以便定制在特定情况下如何自定义类的行为。这可能没问题,但如果问题域中没有明确的is-a关系,我通常会尝试避免继承。

      您可以考虑使用另一种解决方案:strategy pattern让您的类支持对象(或委托)的分配,以定义渲染,初始化,移动等应该如何表现。您的基类可以提供默认实现,但允许外部使用者更改行为。虽然与事件类似,但优点是您可以将值返回给调用代码,并且您只能强制执行一个订阅者。

答案 2 :(得分:5)

决定一点帮助:

是否要通知其他(多个)对象?然后使用事件。

您想要实现不同的实现/使用多态吗?然后使用虚拟/抽象方法。

在某些情况下,即使是两种方式组合也是一个很好的解决方案。

答案 3 :(得分:3)

还有另一个选项,从函数式编程中借用:将扩展点公开为Func或Action类型的属性,并让实验为代表提供代理。

例如,在您的模拟跑步者中,您将拥有:

public class SimulationForm
{
    public Action Init { get; set; }
    public Action<Graphics> Render { get; set; }
    public Action<double> Move { get; set; }

    //...
}

在你的模拟中你会有:

public class Simulation1
{
    private SimulationForm form;

    public void Simulation1()
    {
        form = new SimulationForm();
        form.Init = Init;
        form.Render = Render;
        form.Move = Move;
    }

    private void Init()
    {
        // Do Init code here
    }

    private void Render(Graphics g)
    {
        // Do Rendering code here
    }

    private void Move(double dt)
    {
        // Do Move code here
    }
}

编辑:有关我在某些私有代码中使用的这种技术的非常愚蠢的例子(对于私人游戏项目),请查看my delegate-powered-collection blog post

答案 4 :(得分:2)

两者都不是。

你应该使用一个实现定义所需函数的接口的类。

答案 5 :(得分:1)

为什么界面不适合这个?

答案 6 :(得分:0)

使用虚方法允许基类的特化 - 内部细节。

从概念上讲,除了返回方法之外,您还可以使用事件来表示类的“输出”。

答案 7 :(得分:0)

我喜欢LBuskin的总结。关于脆弱的基类问题,有一种情况我认为受保护事件可能是最佳实践的竞争者:

  1. 你有真正用于内部课程使用的事件(而不是像OP的Render那样的策略),例如Initializing或EnabledChanging,
  2. 您有可能拥有大型继承层次结构(深度和/或广度),需要对事件采取多个级别(多个非公共消费者! )
  3. 你不想依赖每个派生类来记住调用base.OnSomethingHappened()。 (避免脆弱的基类问题。)
  4. 订单处理事件无关紧要(否则请使用base.OnSomethingHappened()链来控制订单。)
  5. 该事件适用于某种内部状态更改,对外部类没有意义(否则使用公共事件)。例如,公共Initializing事件通常是在发生任何内部初始化之前发生的可取消事件。受保护的Initializing事件不应作为可取消事件向类的用户公开,因为该类可能已经部分初始化。
  6. 这避免了使用“派生类应调用基本方法”来记录虚拟受保护方法的需要,并希望您或其他派生类的人始终阅读文档并遵循它。它还避免了不确定性,想知道是否应该在重写方法的开头或中间或结尾调用对base方法的调用。

    注意:如果您认为附加到其自身事件的基类是愚蠢的,那么就没有什么能阻止您在基类中使用非虚方法。

    在OP的情况下,我认为Init和可能的Move事件可能符合上述标准,具体取决于具体情况。通常情况下,它不太可能有多个Render方法,所以一个抽象方法(或空虚拟方法,如果这个类允许你运行模拟而不绘制任何东西)不需要调用任何基本Render方法的方法似乎很可能在这里。如果外部组件可能知道如何将类绘制到图形目标,可能具有替代视觉样式(即2D与3D),则Render的策略(公共委托)将是有用的。对于Render方法似乎不太可能,但也许可能适用于其他未提及的函数,尤其是应用算法的方法(趋势线平均方法等)。