委托中的协方差,任何例子

本文关键字:任何例 方差 | 更新日期: 2023-09-27 18:07:01

我正在阅读这篇msdn的文章,逆变的例子(键盘和鼠标事件)是伟大的和有用的,而协方差的例子(哺乳动物和狗)看起来不是这样。

键盘和鼠标事件是伟大的,因为你可以使用一个处理多个情况;但我想不出将一个返回派生类型的处理程序分配给一个返回基类型的处理程序有什么好处,更不用说它看起来不太常见的委托关心返回类型?

谁能提供一个更实际的协方差委托的例子?

委托中的协方差,任何例子

下面是我实现服务定位器的"真实世界"示例。我想创建一个类,在这个类中我可以注册"工厂"委托,这些委托知道如何生成某种类型的实例,然后解析某种类型的实例。如下所示:

interface ISomeService { ... }
class SomeService : ISomeService { ... }
class IocContainer
{
    private readonly Dictionary<Type, Func<object>> _factories = new Dictionary<Type, Func<object>>();
    public void Register<T>(Func<T> factory)
        where T: class 
    {
        _factories[typeof(T)] = factory;
    }
    // Note: this is C#6, refactor if not using VS2015
    public T Resolve<T>() => (T)_factories[typeof(T)]();
}
class Program
{
    static void Main(string[] args)
    {
        var container = new IocContainer();
        container.Register<ISomeService>(() => new SomeService());
        // ...
        var service = container.Resolve<ISomeService>();
    }
}

现在,我在做container.Register<ISomeService>(() => new SomeService()), Func委托的协方差在两个层面上发挥作用:

  1. 我可以通过Func<SomeService>,它可以在Func<ISomeService>预期没有问题的地方分配,因为SomeServiceISomeService
  2. Register方法中,Func<T>可以在需要Func<object>的地方被分配,这没有问题,因为任何引用类型都是object

如果Func<SomeService>不能赋值给Func<ISomeService>,或者Func<ISomeService>不能赋值给Func<object>(通过协方差),这个例子就行不通了。

你是对的,这是不常见的事件返回一个值,这是一种惯例(实际上它不仅仅是惯例:见额外阅读#2)。然而,委托不仅仅是针对事件的,基本上它们是。net版本的近半个世纪以来的C风格"指向函数的指针"(C术语)。

在控制流的概念中,事件、委托和监听器(java)都是非常神秘的,它们只是简单的回调,所以它们有返回值是完全合理的。

所以从callback的角度来看:比如说我想加工动物。我想使用一个函数指针(我的意思是:委托或lambda)做这个处理的部分。就叫它FeedAnimal吧。我想有另一个骨架方法来调用这个饲料方法,我们叫它CareAnimal。我想插件饲料算法到护理算法运行时变量模式,所以护理将有一个委托参数:饲料。喂食后,喂食方法返回一个动物。

现在的重点是:提要对Dog和Cat将有不同的实现,一个返回Dog,另一个返回Cat....Care()方法接受一个委托参数,该参数返回Animal。

[Extra reading #1]:这种多态实现是not OOP多态实现。在OOP中,你可以通过虚方法重载实现类似的目的。

[额外阅读#2]:关于委托(和事件)的真正令人不安的事情是它们是多播委托,我的意思是一个单独的委托(默认是多播委托)可以包含多个方法入口点。当它被调用时,所有包含的方法在而不是指定的顺序的循环中被调用。然而,如果签名不是无效的,将会有一个返回值。当然,这是令人困惑的,所以我们可以放心地说:如果我们使用委托(或事件)的多播特性,那么除了void return之外,它没有任何意义。事件通常是多播的,这来自发布者/订阅者DP的寓言:许多订阅者(处理程序)可以订阅(+=)到发布者的发布,而无需了解彼此的任何信息。

好吧,如果你看一下Func<T, TResult>委托的声明。

public delegate TResult Func<in T, out TResult>(
    T arg
)

可以看到,输入参数的类型是逆变的,但结果或返回值的类型是协变的。

熟悉的Linq扩展Select,有一个接受这个委托的重载。

另外,注意Select的返回类型是IEnumerable<T>,这是一个协变接口,即

public IEnumerable<out T>
{
    '' ...
}

现在考虑类型,

abstract class Mammal
{
}

class Dog : Mammal
{
}

我可以声明一个委托的实例。

var doItToMammals = new Func<Mammal, Mammal>(mammal => mammal);

我可以把这个委托传递给Select而不产生任何变化。

IEnumerable<Mammal> mammals = new List<Mammal>().Select(doItToMammals);

现在,因为函数的输入是逆变的,我可以做

IEnumerable<Mammal> mammals = new List<Dog>().Select(doItToMammals);

现在关键在于,因为结果是协变的,所以我可以写

IEnumerable<Dogs> dogs = new List<Dog>().Select<Dog, Dog>(doItToMammals);