任务的协方差和逆变性

本文关键字:方差 任务 | 更新日期: 2023-09-27 17:54:11

给出下面的代码片段,我不太明白为什么我要实现的是不可能的:

接口:

public interface IEntityRepository<out T> : IRepository<IEntity> {
    void RequeryDataBase();
    IEnumerable<T> Search(string pattern);
    Task<IEnumerable<T>> SearchAsync(string pattern);
    SearchContext Context { get; }
    string BaseTableName { get; }
  }

IRepository<IEntity>中只是简单的通用CRUD定义。

我在这行得到一个错误:Task<IEnumerable<T>> SearchAsync(string pattern);

错误:

方法返回类型必须是输出安全的。无效方差:类型参数T必须在Task

上始终有效。

请帮助我理解,为什么我不能使用<out T>Task<T>

任务的协方差和逆变性

Task<T>不是协变的。方差只能应用于泛型接口(和委托,但这与这里无关)。

Task<IEnumerable<Dog>> 不能赋值给Task<IEnumerable<Animal>>。因此,你的接口也不能被标记为协变。

你可能想看看这个相关的问题

在决定泛型接口中泛型类型参数的方差时,必须考虑接口中泛型类型参数的所有使用。每次使用都可能引入一些关于方差的约束。这包括:

  • 用作方法的输入参数-禁止协方差
  • 用作方法的返回值-禁止逆向
  • 作为其他通用衍生的一部分使用,如Task<T> -此类使用可能不允许协方差,不允许逆变,或两者兼有

我使用否定逻辑来强调所有这些情况基本上都是引入约束。在分析之后,您将知道是否有任何元素不允许协方差,然后您的参数可能不会被声明为out。相反,如果任何元素具有不允许的逆变性,则可能不会将参数声明为in

在您的特殊情况下,第一个Search方法返回IEnumerable<T>,使得逆变不适用。但是,SearchAsync返回Task<IEnumerable<T>>,并且这种用法引入了Task类型中存在的约束—它是不变的,这意味着这次out也不可能。

结果是你的泛型接口必须在它的泛型参数类型上是不变的,以便满足它所有方法的签名。

如果你可以使用。net Core或。net 5及以后的版本,解决方案是使用IAsyncEnumerable<T>而不是Task<IEnumerable<T>>

协变接口看起来像:

public interface IEntityRepository<out T> : IRepository<IEntity> {
    void RequeryDataBase();
    IEnumerable<T> Search(string pattern);
    IAsyncEnumerable<T> SearchAsync(string pattern);
    SearchContext Context { get; }
    string BaseTableName { get; }
  }

关于协变异步的更多信息可以在这里找到

你可以使用

IObservable<out T>

几乎与Task<T>相同,但具有协变类型。当你需要的时候,它总是可以被转换成一个任务。您在代码的可读性上损失了一点,因为Task<T>假定(并强制)您只能获得一个结果,而IObservable<T>可以获得多个结果。但是你可以做

IObservable<T> o = ...
var foo = await o;

相同
Task<T> t = ...
var foo = await t;

注意,您需要从Rx库中包含System.Reactive.Linq名称空间才能使此工作。它将为IObservable<>添加一个扩展方法,使其可等待。

如前所述,Task<TResult>中的TResult不是协变的。你可以用ContinueWith:

创建一个新的Task实例
var intTask = Task.FromResult(42);
var objectTask = intTask.ContinueWith(t => (object) t.Result);
await objectTask; // 42