13.3.4 【接口和委托的泛型可变性】复杂情况

1.  Converter<TInput, TOutput> :同时使用协变性和逆变性

            Converter<object, string> converter1 = x => x.ToString();
            Converter<string, string> converter2 = converter1;
            Converter<object, object> converter3 = converter1;
            Converter<string, object> converter4 = converter1;

代码清单13-15展示了委托类型 Converter<object, string> (一个接收对象,生成字符串的委托)的可变性转换。我们首先使用简单的Lambda表达式(调用 ToString )实现了委托。
我们恰巧从未真正调用该委托,因此完全可以使用空引用。但我发现如果可以定义一个在调用时发生的具体行为,将有助于理解可变性。接下来的两行代码相对简单,每次只需关注一种类型参数即可。 TInput 类型参数只用于输入,因此可以逆变地将 Converter<object,string> 当作 Converter<Button,string>来使用。换句话说,既然可以将任意对象的引用传递给转换器,那么自然可以传递一个Button引用。同样,TOutput类型参数只用于输出(返回类型),因此可以协变地使用它:如果转换器总是返回一个字符串引用,那么在你能够保证返回一个对象引用的地方,可以放心地使用该转换器。最后一行仅仅是对这种理念的一种合理延伸。它在同一个转换内同时使用了逆变性和协变性,得到的是一个只接受按钮并且返回对象引用的转换器。注意,如果不进行强制转换,则无法将其转换为原来的类型——我们已经基本上在每个点上都放松了要求,你不能再隐式地收紧了。

2. 疯狂的高阶函数

        delegate Func<T> FuncFunc<out T>();
        delegate void ActionAction<out T>(Action<T> action);
        delegate void ActionFunc<in T>(Func<T> func);
        delegate Action<T> FuncAction<in T>();

每一个声明都相当于将一个标准的委托嵌入到另一个之中。
例如, FuncAction<T> 等同于Func<Action<T>> ,它们都表示一个函数,返回以 T 为参数的 Action 。但它应该是协变的还是逆变的呢?
这个函数将返回与 T 相关的内容,似乎应该是协变的,但是它也传入了与 T 有关的内容,似乎又是逆变的。
答案是该委托对 T 是逆变的,因此声明时使用了 in 修饰符。
作为一个便捷的规则,可以认为内嵌的逆变性反转了之前的可变性,而协变性不会如此。
因此 Action<Action<T>> 对 T 来说是协变的, Action<Action<Action<T>>> 是逆变的。
相比之下,对于 Func<T> 的可变性来说,你可以编写 Func<Func<Func<... Func<T>...>>> ,嵌套任意多个级别,得到的仍然是协变性。
举一个使用接口的类似示例,假设可以使用比较器对序列进行比较。如果能够比较任意对象的两个序列,自然可以比较两个字符串序列,但反之则不然。
将其转换为代码(不必实现接口)如下:

            IComparer<IEnumerable<object>> objectComparer = null;
            IComparer<IEnumerable<string>> stringComparer = objectComparer;

这种转换是合法的:由于 IEnumerable<T> 的协变性,IEnumerable<string> 是比IEnumerable<object> 更“小”的类型;
而 IComparer<T> 的逆变性可以将“较大”类型的比较器转换为较小类型的比较器。
当然,我们在本节只使用了包含单个类型参数的委托和接口——它也完全可以应用于多个类型参数。
尽管这种类型的可变性让你痛不欲生,不过不必担心,你并不会频繁地使用它,而且用的时候编译器也会帮你大忙。
我只是想让你知道有这种可能。另一方面,你可能认为某些功能可以实现,但实际上它们并没有得到支持。

 

posted @ 2018-12-12 22:07  一只桔子2233  阅读(131)  评论(0编辑  收藏  举报