任务的协变和逆变

15
在下面的代码片段中,我不太明白为什么我要实现的内容为什么是不可能的:

接口:

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 上不变地有效。

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

我认为操作的结果需要分配给任务,使其成为 <in T> - Jim
@Jim 但是结果类型是 iEnumerable<T>,而不仅仅是 T... 我本能地认为这应该可以工作。 - René Vogt
2
Task<T>是一个类,因此它是不变的。 - Lee
@RenéVogt 我也这么认为。Task本身不应该关心返回的内容是什么。 - lokusking
5个回答

19

Task<T>不支持协变性,只有针对泛型接口(和委托类型,但这与此处无关)才能应用协变性。

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

您可能想查看这个相关问题


22
他们可以给出任何说明,但我仍然觉得在引入“Task<T>”、“ITask”却没有“ITask<out T>”时出现了错误的决策。 - Toxantron

6
决定在某个通用接口中使用通用类型参数的方差时,您必须考虑接口内通用类型参数的所有用法。每个用法都可能引入有关方差的某些限制。这包括:
  • 作为方法的输入参数的用途-禁止协变
  • 作为方法返回值的用途-禁止逆变
  • 作为其他通用派生的一部分,例如Task<T> - 这种用法可能禁止协变、禁止逆变或两者皆有

我使用了消极逻辑来强调所有这些情况基本上都引入了限制。经过分析,您将知道任何元素是否已禁止协变,然后您的参数可能不声明为out。反之,如果任何元素禁止了逆变性,则您的参数可能无法声明为in

在您的特定情况下,第一个Search方法返回IEnumerable<T>,从而使逆变不适用。但是,SearchAsync然后返回Task<IEnumerable<T>>,而该用法引入了存在于Task类型中的约束-它是不变的,这意味着此时out也不可能。

结果是,为了满足其所有方法的签名,您的通用接口必须在其通用参数类型上是不变的。


2
我不完全理解为什么没有ITask<out T>接口。它将有助于解决这些问题。 - Toxantron
@Toxantron 因为需要有人来“设置”结果,所以它不能是协变的。我仍然不明白所有这些如何适用于此处,因为问题中的类型仍然是 IEnumerable<T> 而不是 T。为什么 T 的协变性在 Task<IEnumerable<T>> 中起任何作用呢? - René Vogt
1
@RenéVogt Task 对于 IEnumerable<T> 是不变的,这意味着 T 不能变化,尽管 IEnumerable<T> 本身对 T 是协变的。Search 方法返回普通的 IEnumerable<T>,并且不限制变异性。仅使用 Search 方法,整个接口可以对 T 是协变的。 - Zoran Horvat
@RenéVogt 当然可以。我经常设计这样的API。看看 List<T> 和 IEnumerable<out T>。你可以操作 Task<T>,但返回 ITask<out T>。 - Toxantron
@Toxantron,ITask<out T>是什么? - Zoran Horvat
@ZoranHorvat 很遗憾,由于某些模糊的原因,这种情况并不存在。他们认为现在无法改变。我希望有人能创建 ITask<out T>,每当我们遇到使用 Task<T> 的 API 时,我们就可以将其替换掉。 - Toxantron

1
如果您可以使用.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; }
  }

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


0
你可以作弊并且使用。
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甚至没有实现IObservable - IluTov
你可以等待一个IObservable。 - bradgonesurfing
好的,我猜你不是在谈论 System.IObservable 对吧 ;) - IluTov
如此提到,您需要在 System.Reactive.Linq 中使用扩展方法才能使其正常工作。https://dev59.com/24Xca4cB1Zd3GeqPPvSz#27415346 - IluTov
当然。没有System.Reactive. Linq,就没有人会使用IObservable。 - bradgonesurfing

0

就像其他人之前提到的那样,在 Task<TResult> 中,TResult 不是协变的。不过您可以使用 ContinueWith 创建一个新的 Task 实例:

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

await objectTask; // 42

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接