首页 > 解决方案 > 无法将孩子投射到父类

问题描述

我有一个类定义,如:

 public abstract class Index<TKey> : IComparer<Computer>, IEnumerable<TKey>
        where TKey:IComparable

然后另一个类将其实现为:

public class ScreenSizeIndex : Index<double>

然后我想像这样消费

Index<IComparable> index = new ScreenSizeIndex(); 

然后编译器抱怨:

CS0266 无法将类型“System.IComparable”隐式转换为“double”。存在显式转换(您是否缺少演员表?)

double shold 可以分配给 IComparable,所以 Index 的实现应该可以分配给 Index,那么为什么编译器会抱怨这个 assigmnet

标签: c#genericsinheritance.net-core

解决方案


这个问题的答案可能看起来不是很直观,但确实很简单。具有不同泛型参数的泛型类型(大多数情况下)在 C# 中被认为是完全不同的类型,无论其中一个类型参数是否可以分配给另一个。您可以尝试对任何泛型类型执行此操作,例如将 a 分配List<double>给 aList<IComparable>或 a List<object>,只要您分配的类型参数和您分配的类型参数不完全相同,它就永远不会起作用。

以下代码可以正常工作,因为ScreenSizeIndex根据Index<double>定义。

Index<double> index = new ScreenSizeIndex();

编辑:

我想我也许应该对这个话题有更多的了解。

为什么它不起作用(不变性)

想象一下下面的泛型类:

public class Foo<T> {
    public T GetBar() {
        // return some value of type T
    }
    public void SetBar(T value) {
        // do something with the value of type T
    }

}

现在,如果你有 a Foo<string>,你可以绝对肯定它GetBar()总是会返回 a string。现在你可能会想,好吧,因为 astring也总是 a IComparable,所以 aFoo<string>很容易被视为 aFoo<IComparable>因为返回的值仍然具有正确的类型,所以这个

Foo<IComparable> foo = new Foo<string>();
IComparable bar = foo.GetBar();

应该是可能的。

是的,如果我们只谈论T作为方法的返回类型,那么这种思路是正确的。但是如果我们调用SetBar()where 函数参数的类型T呢?好吧,如果您可以将 aFoo<string>存储为 a Foo<IComparable>,则类型的变量将Foo<IComparable>允许您像这样传递任何内容:IComparableSetBar()

Foo<IComparable> foo = new Foo<string>();
foo.SetBar(4.2f);

编译器将允许这样做,因为float实现IComparable,这就是 aFoo<IComparable>可以预期的全部,但是,当尝试将 a 传递给实际存储的对象floatSetBar(string)方法时,方法调用将在运行时失败。Foo<string>

因此,编译器根本不允许这些情况发生。


协方差和逆变

这种特定行为称为不变性,适用于大多数泛型类型。唯一可能的例外是泛型接口委托,如果由相应的接口或委托指定,它们可以完全按照您尝试实现的方式运行。

我们一直在谈论的行为称为协方差。定义泛型接口时,将关键字out放在泛型类型参数前面会使其协变。例如,您可以在 interface 的声明中看到这一点IEnumerable<out T>。如果我们对此进行测试,我们可以看到它实际上工作得很好:

IEnumerable<IComparable> a = new List<string>();

然而,这有两个条件:

  1. 用关键字定义的泛型类型参数out只能用作方法返回类型或只读属性、只读字段或静态字段的类型(如果我理解正确的话,未来的版本也将允许协变类型参数用作静态方法的参数类型)。
  2. 协方差仅适用于引用类型。值类型从不支持协方差(这就是我用作string示例类型而不是 的原因double)。编译器不允许尝试将 an 分配给 an IEnumerable<double>IEnumerable<IComparable>

另外,请注意协方差仅以一种方式起作用,因此您可以将 an 分配IEnumerable<string>给 an IEnumerable<IComparable>,但不能反过来。那将是逆变

逆变与协方差正好相反。这意味着您可以将泛型类型的实例分配给类型参数更多派生的变量。就像协变类型参数一样,逆变类型参数是使用关键字声明的,在这种情况下是in. 这方面的一个例子是Action<in T> delegate.

Action<object> x = Console.WriteLine;
Action<string> y = x;

同样,逆变类型参数有两个条件:

  1. 用关键字定义的泛型类型参数in只能用作方法参数、只写属性或静态字段的类型(同样,如果我理解正确的话,未来的版本也将允许使用逆变类型参数作为静态方法的返回类型)。另外,请注意,当对方法参数使用逆变类型参数时,该方法参数必须是按值的,因此不能使用in,outref关键字(是的,方法参数的inandout关键字的含义与类型参数完全不同)。
  2. 与协变一样,逆变仅适用于引用类型,因此您不能将 an 分配Action<IComparable>给 an Action<double>

同样,逆变只能以一种方式起作用,因此将 an 分配Action<string>给 anAction<object>也不会起作用。

逆变经常使人感到困惑,但如果你仔细想想它是有道理的。看上面的例子:

Action<object> x = Console.WriteLine;
Action<string> y = x;

这没有问题,因为底层证券Action<object>仍然接受任何object. 所以调用y("Test")会很好,因为所有string的 s 都是objects。然而,反过来,我们就会遇到麻烦,就像我在Foo<T>上面的界面中描述的那样。


我希望此编辑对任何寻找类似于原始问题中描述的行为的人有所帮助。


推荐阅读