委托与接口-还有更多可用的说明吗?


23

在阅读了《 何时使用委托代替接口》(《 C#编程指南》)一文之后,我需要一些帮助来理解以下给定的要点,我发现这些要点不太清楚(对我而言)。有任何示例或详细说明吗?

在以下情况下使用委托:

  • 使用事件设计模式。
  • 期望封装静态方法。
  • 需要容易的组成。
  • 一个类可能需要该方法的多个实现。

在以下情况下使用界面:

  • 可以调用一组相关方法。
  • 一个类仅需要该方法的一个实现。

我的问题是

  1. 事件设计模式意味着什么?
  2. 如果使用委托,组成如何变得容易?
  3. 如果有一组相关的方法可以被调用,那么使用接口-它有什么好处?
  4. 如果一个类仅需要该方法的一个实现,请使用接口-这样做有何好处?

Answers:


11

事件设计模式意味着什么?

他们最有可能引用观察者模式的实现,该模式是C#中的核心语言构造,公开为“ 事件 ”。通过将委托挂接到事件可以监听事件。正如Yam Marcovic所指出的那样,它EventHandler是事件的常规基本委托类型,但是可以使用任何委托类型。

如果使用委托,组成如何变得容易?

这可能只是指代代表提供的灵活性。您可以轻松地“组成”某些行为。借助lambdas,语法非常简洁。考虑以下示例。

class Bunny
{
    Func<bool> _canHop;

    public Bunny( Func<bool> canHop )
    {
        _canHop = canHop;
    }

    public void Hop()
    {
        if ( _canHop() )  Console.WriteLine( "Hop!" );
    }
}

Bunny captiveBunny = new Bunny( () => IsBunnyReleased );
Bunny lazyBunny = new Bunny( () => !IsLazyDay );
Bunny captiveLazyBunny = new Bunny( () => IsBunnyReleased && !IsLazyDay );

对接口进行类似的操作将需要您使用策略模式或使用(抽象的)基Bunny类,从中扩展更特定的兔子。

如果有一组相关的方法可以被调用,那么使用interface-它有什么好处?

同样,我将使用兔子来演示它会变得更容易。

interface IAnimal
{
    void Jump();
    void Eat();
    void Poo();
}

class Bunny : IAnimal { ... }
class Chick : IAnimal { ... }

// Using the interface.
IAnimal bunny = new Bunny();
bunny.Jump();  bunny.Eat();  bunny.Poo();
IAnimal chick = new Chick();
chick.Jump();  chick.Eat();  chick.Poo();

// Without the interface.
Action bunnyJump = () => bunny.Jump();
Action bunnyEat = () => bunny.Eat();
Action bunnyPoo = () => bunny.Poo();
bunnyJump(); bunnyEat(); bunnyPoo();
Action chickJump = () => chick.Jump();
Action chickEat = () => chick.Eat();
...

如果一个类仅需要该方法的一个实现,请使用接口-这样做有何好处?

为此,请再次考虑兔子的第一个示例。如果只需要一种实现-不需要自定义组成,则可以将此行为公开为接口。您将不必构造lambda,只需使用接口即可。

结论

代表们提供了更多的灵活性,而界面则可以帮助您建立牢固的合同。因此,我发现提到的最后一点是:“一个类可能需要该方法的多个实现”。,是目前最相关的一种。

使用委托的另一个原因是,您只想公开无法从中调整源文件的类的一部分。

作为这种情况的示例(最大的灵活性,无需修改源),只需传递两个委托,就可以对任何可能的集合考虑二进制搜索算法的这种实现


12

委托类似于单个方法签名的接口,而不必像常规接口那样显式实现。您可以在运行时构建它。

接口只是表示某些合同的语言结构-“我在此保证我提供以下方法和属性”。

另外,我也不完全同意委托代理在用作观察者/订阅者模式的解决方案时最有用。但是,它是“ java详细程度问题”的理想解决方案。

对于您的问题:

1&2)

如果要使用Java创建事件系统,通常使用接口来传播事件,例如:

interface KeyboardListener
{
    void KeyDown(int key);
    void KeyUp(int key)
    void KeyPress(int key);
    .... and so on
}

这意味着您的类将必须显式实现所有这些方法,并为所有这些方法提供存根,即使您只是想实现KeyPress(int key)

在C#中,这些事件将被表示为委托列表,并由c#中的“ event”关键字隐藏,每个单个事件对应一个。这意味着您可以轻松地订阅所需的内容,而不会用公共的“ Key”方法等给班级增加负担。

+1分3-5。

另外:

当您要提供例如“地图”功能时,委托非常有用,该功能接受一个列表并将每个元素投影到一个新列表中,其中元素数量相同但有所不同。本质上,IEnumerable.Select(...)。

IEnumerable.Select采用一个Func<TSource, TDest>,这是一个包装函数的委托,该函数采用TSource元素并将该元素转换为TDest元素。

在Java中,必须使用接口来实现。通常没有自然的地方可以实现这样的接口。如果一个类包含要以某种方式转换的列表,则实现接口“ ListTransformer”并不是很自然,尤其是因为可能存在两个不同的列表,应以不同的方式转换。

当然,您可以使用类似概念的匿名类(在Java中)。


考虑编辑帖子以实际回答他的问题
Yam Marcovic

@YamMarcovic你是对的,我随意地随意游说,而不是直接回答他的问题。不过,我认为这可以解释所涉及的机制。另外,当我回答问题时,他的问题形式不太正确。:p
最多

自然。对我来说也是如此,它本身也具有价值。这就是为什么我只建议它,却不抱怨。:)
Yam Marcovic

3

1)事件模式,经典的就是观察者模式,这是一个很好的Microsoft链接 Microsoft Talk Observer

您链接到的文章的imo编写得不太好,并使事情变得比必要的更为复杂。静态业务是常识,您不能使用静态成员定义接口,因此,如果要在静态方法上进行多态行为,则可以使用委托。

上面的链接讨论了代表以及为什么有代表组合的好处,因此这可能对您的查询有所帮助。


3

首先,好问题。我钦佩您对实用程序的关注,而不是一味地接受“最佳实践”。+1。

我之前已经阅读过该指南。您必须记住一些有关它的内容-它只是一个指南,主要供那些知道如何编程但对C#的工作方式不太熟悉的C#新手使用。它不只是规则页面,而是描述通常已经完成的事情的页面。由于他们已经在任何地方都采用这种方法,因此保持一致可能是一个好主意。

我会回答你的问题。

首先,我假设您已经知道接口是什么。对于委托,可以说它是一个结构,其中包含指向方法的类型化指针,以及指向表示该this方法参数的对象的可选指针。对于静态方法,后一个指针为null。
也有一些组播委托,它们与委托一样,但是可能会为其分配多个结构(这意味着对组播委托的Invoke的单个调用将调用其分配的调用列表中的所有方法)。

事件设计模式意味着什么?

它们的意思是在C#中使用事件(该事件具有特殊的关键字,可以巧妙地实现这种极其有用的模式)。多播委托推动了C#中的事件。

当您定义事件时,例如在此示例中:

class MyClass {
  // Note: EventHandler is just a multicast delegate,
  // that returns void and accepts (object sender, EventArgs e)!
  public event EventHandler MyEvent;

  public void DoSomethingThatTriggersMyEvent() {
    // ... some code
    var handler = MyEvent;
    if (handler != null)
      handler(this, EventArgs.Empty);
    // ... some other code
  }
}

编译器实际上将其转换为以下代码:

class MyClass {
  private EventHandler MyEvent = null;

  public void add_MyEvent(EventHandler value) {
    MyEvent += value;
  }

  public void remove_MyEvent(EventHandler value) {
    MyEvent -= value;
  }

  public void DoSomethingThatTriggersMyEvent() {
    // ... some code
    var handler = MyEvent;
    if (handler != null)
      handler(this, EventArgs.Empty);
    // ... some other code
  }
}

然后,您通过以下方式订阅活动

MyClass instance = new MyClass();
instance.MyEvent += SomeMethodInMyClass;

编译成

MyClass instance = new MyClass();
instance.add_MyEvent(new EventHandler(SomeMethodInMyClass));

因此,这在C#(或一般.NET)中会发生。

如果使用委托,组成如何变得容易?

这很容易证明:

假设您有一个类,该类依赖于要传递给它的一组操作。您可以将这些操作封装在接口中:

interface RequiredMethods {
  void DoX();
  int DoY();
};

任何想要将动作传递给您的类的人都必须首先实现该接口。或者,您可以通过以下课程来简化他们的生活

sealed class RequiredMethods {
  public Action DoX;
  public Func<int> DoY();
}

这样,调用者只需在运行时创建RequiredMethods实例并将方法绑定到委托即可。通常这比较容易。

在正确的情况下,这种处事方式非常有益。考虑一下-当您真正关心的是将实现传递给您时,为什么要依赖接口?

有一组相关方法时使用接口的好处

使用接口是有益的,因为接口通常需要显式的编译时实现。这意味着您将创建一个新类。
而且,如果您在一个程序包中有一组相关的方法,那么使该程序包可被代码的其他部分重用是有益的。因此,如果他们可以简单地实例化一个类而不是构建一组委托,那将更加容易。

如果一类仅需要一个实现,则使用接口的好处

如前所述,接口是在编译时实现的-这意味着它们比调用委托(这本身就是间接级别)要高效。

“一个实现”可能意味着一个存在一个明确定义的位置的实现。
否则,实现可能来自程序中恰好符合方法签名的任何地方。这样可以提供更大的灵活性,因为方法只需要符合预期的签名,而不必属于显式实现特定接口的类。但是这种灵活性可能要付出一定的代价,并且实际上破坏了Liskov Substitution原则,因为大多数时候您都希望明确,因为这样可以最大程度地减少发生事故的机会。就像静态打字一样。

该术语在这里也可以指多播委托。接口声明的方法只能在实现类中实现一次。但是代表可以累积多个方法,这些方法将被顺序调用。

因此,总的来说,该指南似乎还不足以提供信息,仅能按其本身的功能运行-而不是规则手册。一些建议实际上听起来有点矛盾。由您决定何时才是合适的方法。该指南似乎只给我们提供了一条一般的道路。

希望您的问题得到满意的答复。再一次,对这个问题表示敬意。


2

如果我对.NET的记忆仍然成立,则委托基本上就是一个函数ptr或functor。它为函数调用增加了一个间接层,以便可以替换函数而不必更改调用代码。接口做的是同一件事,除了接口将多个功能打包在一起,并且实现者必须一起实现它们。

从广义上讲,事件模式是一种响应其他地方的事件(例如Windows消息)的模式。事件集通常是开放式的,它们可以以任何顺序出现,并且不一定彼此相关。代表为此很好地工作,因为每个事件都可以调用一个函数,而无需引用一系列可能也包含许多不相关函数的实现对象。另外(这是我的.NET内存模糊的地方),我认为可以将多个委托附加到一个事件。

合成,虽然我不太熟悉该术语,但基本上是将一个对象设计为具有多个子部分,即将工作传递到的子对象。代表们可以让孩子们以更加临时的方式进行混合和匹配,以免界面过于夸张或导致过多的耦合以及随之而来的刚性和脆弱性。

相关方法的接口的好处在于,这些方法可以共享实现对象的状态。委托函数不能如此干净地共享甚至包含状态。

如果一个类只需要一个实现,那么一个接口更合适,因为在实现整个集合的任何一个类中,只能执行一个实现,并且您可以从实现类(状态,封装等)中受益。如果实现可能由于运行时状态而改变,则代表可以更好地工作,因为可以将它们替换为其他实现而不会影响其他方法。例如,如果有三个委托,每个委托都有两个可能的实现,则您将需要八个不同的类来实现三个方法的接口,以解决所有可能的状态组合。


1

“事件”设计模式(更好的称为观察者模式)使您可以将具有相同签名的多个方法附加到委托。您真的不能通过界面来做到这一点。

我完全不相信委托对于组合而言比接口更容易。这是一个很奇怪的说法。我不知道他的意思是因为您可以将匿名方法附加到委托。


1

我可以提供的最大澄清:

  1. 委托定义函数签名-匹配函数将接受哪些参数以及返回什么参数。
  2. 接口处理整套功能,事件,属性和字段。

所以:

  1. 当您要泛化具有相同签名的某些功能时,请使用委托。
  2. 当您想要概括类的某些行为或质量时,请使用接口。

1

您对事件的疑问已经很好地涵盖了。的确,一个接口可以定义多个方法(但实际上不是必须的),而函数类型只能对单个函数施加约束。

但是,真正的区别是:

  • 通过结构子类型将功能值与功能类型匹配(即与所需结构的隐式兼容性)。这意味着,如果函数值具有与函数类型兼容的签名,则它是该类型的有效值。
  • 实例通过名义子类型(即,名称的显式使用)与接口进行匹配。这意味着,如果实例是显式实现给定接口的类的成员,则该实例是该接口的有效值。但是,如果对象仅具有接口要求的所有成员,并且所有成员都具有兼容的签名,但没有显式实现接口,则它不是接口的有效值。

当然可以,但这意味着什么呢?

让我们来看这个例子(因为我的C#不太好,所以代码在haXe中):

class Collection<T> {
    /* a lot of code we don't care about now */
    public function filter(predicate:T->Bool):Collection<T> { 
         //build a new collection with all elements e, such that predicate(e) == true
    }
    public function remove(e:T):Bool {
         //removes an element from this collection, if contained and returns true, false otherwise
    }
}

现在,过滤方法可以轻松方便地传递一小部分逻辑,而无需知道集合的内部组织,而集合并不依赖于所提供的逻辑。大。除了一个问题:
收集确实取决于逻辑。该集合从本质上假设:传入的函数旨在根据条件测试值并返回测试成功。请注意,并非所有将一个值作为参数并返回布尔值的函数实际上只是谓词。例如,我们集合的remove方法就是这样的功能。
假设我们打过电话c.filter(c.remove)。结果将是包含cwhile 所有元素的集合c本身变成空的。这很不幸,因为我们自然希望c自己是不变的。

该示例非常结构化。但是关键问题是,c.filter以某个函数值作为参数调用的代码无法知道该参数是否合适(即最终将保存不变式)。创建函数值的代码可能会或可能不会知道,它将被解释为谓词。

现在让我们进行更改:

interface Predicate<T> {
    function test(value:T):Bool;
}
class Collection<T> {
    /* a lot of code we don't care about now */
    public function filter(predicate:Predicate<T>):Collection<T> { 
         //build a new collection with all elements e, such that predicate.test(e) == true
    }
    public function remove(e:T):Bool {
         //removes an element from this collection, if contained and returns true, false otherwise
    }
}

有什么变化?发生的变化是,无论现在赋予什么价值,filter都已明确签署了谓词合同。当然,恶意的或极其愚蠢的程序员会创建接口的实现,这些实现不是没有副作用的,因此也不是谓词。
但是,不再可能发生的事情是,有人将数据/代码的逻辑单元捆绑在一起,由于其外部结构,该数据单元/代码被错误地解释为谓词。

因此,仅用几句话就可以重新表述上面的内容:

  • 接口意味着使用名义类型并因此使用显式关系
  • 代表的意思是使用结构化类型,从而使用隐式关系

显式关系的优点是可以确定它们之间的关系。缺点是,它们需要显式的开销。相反,隐式关系(在我们的例子中,一个函数的签名与所需的签名匹配)的缺点是,您不能真正确保您可以100%地以这种方式使用事物。好处是,您可以建立关系而没有所有开销。您可以快速将它们放在一起,因为它们的结构允许这样做。这就是简单的组合的意思。
这是一个有点像乐高:你可以简单地插入星球大战LEGO人物到LEGO海盗船,只是因为外部结构允许它。现在您可能会觉得那是完全错误的,或者可能正是您想要的。没有人会阻止你。


1
值得一提的是,“显式实现接口”(在“名义子类型”下)与接口成员的显式或隐式实现不同。正如您所说,在C#中,类必须始终显式声明其实现的接口。但是接口成员可以显式(例如int IList.Count { get { ... } })或隐式(public int Count { get { ... } })实现。这种区别与本讨论无关,但值得一提,以免引起读者困惑。
phoog 2011年

@phoog:是的,谢谢你的修正。我什至不知道第一个可能。但是,是的,语言实际执行接口实现的方式差异很大。例如,在Objective-C中,它只有在未实施的情况下才会给您警告。
back2dos

1
  1. 事件设计模式暗含涉及发布和订阅机制的结构。事件由来源发布,每个订阅者都获得它自己的已发布项目副本,并对其自身的行为负责。这是一种松散耦合的机制,因为发布者甚至不必知道订阅者在那里。更不用说拥有订户不是强制性的了(也可以没有)
  2. 如果与接口相比使用委托,则组合“更容易”。接口将类实例定义为“处理程序种类”对象,其中,组合构造实例看起来像“具有处理程序”,因此实例的外观较少受委托限制,并且变得更加灵活。
  3. 从下面的评论中,我发现我需要改进我的文章,请参阅以下内容以供参考:http : //bytes.com/topic/c-sharp/answers/252309-interface-vs-delegate

1
3.为什么代表需要更多的选拔权?为什么更紧密的联系会带来好处?4.您不必使用事件来使用委托,对于委托,它就像持有一个方法-您知道它是返回类型,而它是参数的类型。另外,为什么在接口中声明的方法比附加到委托的方法更容易处理错误?
Yam Marcovic

如果只是代表的规格,那您是对的。但是,在事件处理的情况下(至少这是我所引用的内容),您总是需要继承某种eventArgs来充当自定义类型的容器,因此,除了能够安全地测试值之外,您始终会必须在处理这些值之前先拆开您的类型。
Carlo Kuip

至于为什么我认为在接口中定义的方法将使其更易于处理错误的原因,是使编译器能够检查从该方法引发的异常类型。我知道这并不令人信服,但也许应该说是偏好?
Carlo Kuip

1
首先,我们谈论的是委托与接口,而不是事件与接口。其次,使事件本身基于EventHandler委托只是一种约定,绝不是强制性的。但是再次,在这里谈论事件有点不对了。至于您的第二条评论-C#中未检查异常。您对Java感到困惑(顺便说一句,它甚至没有委托)。
Yam Marcovic

在该主题上找到了一个解释重要差异的线索
Carlo
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.