【问题标题】:Covariance and contravariance on Tasks任务的协变和逆变
【发布时间】:2016-11-03 13:04:05
【问题描述】:

鉴于以下 sn-p,我很不明白 为什么 我要实现的目标是不可能的:

界面:

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&lt;IEntity&gt; 中只是定义了简单的通用CRUD

我在此行收到错误:Task&lt;IEnumerable&lt;T&gt;&gt; SearchAsync(string pattern);

错误:

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

请帮助我理解,为什么我不能将&lt;out T&gt;Task&lt;T&gt; 一起使用

【问题讨论】:

  • 我认为需要将操作的结果分配给任务,使其成为&lt;in T&gt;
  • @Jim 但结果类型是iEnumerable&lt;T&gt;,而不仅仅是T....我直觉上认为这应该可行。
  • Task&lt;T&gt; 是一个类,所以它是不变的。
  • @RenéVogt 我也这么认为。 Task 本身不应该关心,什么回来了。

标签: c# async-await task covariance contravariance


【解决方案1】:

Task&lt;T&gt; 不是协变的。差异只能应用于通用接口(和委托,但这与这里无关)。

例如Task&lt;IEnumerable&lt;Dog&gt;&gt;不能分配给Task&lt;IEnumerable&lt;Animal&gt;&gt;。因此,您的接口也不能被标记为协变。

您可能想查看this related question

【讨论】:

  • 他们可以给出他们想要的所有解释。我仍然觉得有人在介绍Task&lt;T&gt;ITask 时打错了电话,但没有ITask&lt;out T&gt;
【解决方案2】:

在决定某个泛型接口中泛型类型参数的变化时,您必须考虑接口内泛型类型参数的所有用途。每次使用都可能引入一些关于方差的限制。这包括:

  • 用作方法的输入参数 - 不允许协方差
  • 用作方法的返回值 - 不允许逆变
  • 用作其他泛型派生的一部分,例如 Task&lt;T&gt; - 此类使用可能不允许协变、不允许逆变,或两者兼而有之

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

在您的特定情况下,第一个 Search 方法返回 IEnumerable&lt;T&gt;,导致逆变不适用。但是,SearchAsync 正在返回 Task&lt;IEnumerable&lt;T&gt;&gt;,并且这种用法引入了 Task 类型中存在的约束 - 它是不变的,这意味着这次 out 也是不可能的。

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

【讨论】:

  • 我不完全明白为什么没有 ITask 接口。这将有助于解决这些问题。
  • @Toxantron 因为有人必须设置结果,所以它不能是协变的。我仍然没有看到这一切在这里如何应用,因为有问题的类型仍然是IEnumerable&lt;T&gt; 而不是T。为什么T 的方差在Task&lt;IEnumerable&lt;T&gt;&gt; 中起任何作用?
  • @RenéVogt TaskIEnumerable&lt;T&gt; 上是不变的,这意味着 T 不能改变,尽管 IEnumerable&lt;T&gt; 本身在 T 上是协变的。 Search 方法返回普通的 IEnumerable&lt;T&gt; 并且不限制方差。仅使用Search 方法,整个接口可以在T 上是协变的。
  • @RenéVogt 确定可以。我一直在设计这样的 API。查看 List 和 IEnumerable。您对 Task 进行操作,但返回 ITask.
  • @Toxantron 什么是 ITask
【解决方案3】:

正如其他人之前提到的,Task&lt;TResult&gt; 中的TResult 不是协变的。但是,您可以做的是使用 ContinueWith 创建一个新的 Task 实例:

var intTask = Task.FromResult(42);
var objectTask = intTask.ContinueWith(t => (object) t.Result);

await objectTask; // 42

【讨论】:

    【解决方案4】:

    你可以作弊和使用

    IObservable<out T>
    

    几乎Task&lt;T&gt; 相同,但具有协变类型。当您需要它时,它总是可以转换为任务。您会在代码的可读性上失去一点点,因为假设(并强制执行)Task&lt;T&gt; 您只会得到一个结果,但使用 IObservable&lt;T&gt; 您可以获得很多结果。但是你可以做

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

    和原来一样

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

    请注意,您需要包含 Rx library 中的 System.Reactive.Linq 命名空间才能使这项工作正常进行。它将向IObservable&lt;&gt; 添加一个扩展方法,使其可以等待。

    【讨论】:

    • 另外,Task 甚至没有实现 IObservable
    • 你可以等待一个 IObservable。 stackoverflow.com/questions/27415098/…
    • 好吧,我猜你说的不是System.IObservable ;)
    • 正如这里提到的,您需要System.Reactive.Linq 中的扩展方法才能使其工作。 stackoverflow.com/a/27415346/1320374
    • 当然。没有 System.Reactive,没有人会使用 IObservable。林克。
    猜你喜欢
    • 1970-01-01
    • 2015-02-09
    • 2012-04-06
    • 1970-01-01
    • 1970-01-01
    • 2012-12-03
    • 2014-03-06
    • 2015-01-16
    • 1970-01-01
    相关资源
    最近更新 更多