任务的协变和逆变
Covariance and contravariance on Tasks
鉴于以下代码片段,我完全不明白为什么我不可能实现什么:
接口:
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);
错误:
method return type must be output safe. invalid variance: the type parameter T must be invariantly valid on Task
请帮助我理解,为什么我不能将 <out T>
与 Task<T>
一起使用
在决定某些泛型接口中泛型类型参数的变化时,您必须考虑接口内泛型类型参数的所有使用。每次使用都可能引入一些关于方差的约束。这包括:
- 用作方法的输入参数 - 不允许协方差
- 用作方法中的 return 值 - 不允许逆变
- 作为其他泛型推导的一部分使用,例如
Task<T>
- 此类使用可能不允许协变、不允许逆变或两者兼而有之
我是用反逻辑来强调所有这些情况基本上都是在引入约束。分析后,您将知道是否有任何元素不允许协变,然后您的参数可能不会声明为 out
。相反,如果任何元素不允许逆变,则您的参数可能不会声明为 in
.
在您的特定情况下,第一个 Search
方法 returns IEnumerable<T>
,呈现逆变不适用。但是,SearchAsync returning Task<IEnumerable<T>>
,并且该用法引入了存在于 Task 类型中的约束 - 它是不变的,这意味着这次 out
也是不可能的。
结果是您的泛型接口必须在其泛型参数类型上保持不变才能满足其所有方法的签名。
Task<T>
不是协变的。差异只能应用于通用 interfaces(和委托,但这与此处无关)。
例如Task<IEnumerable<Dog>>
无法 分配给 Task<IEnumerable<Animal>>
。因此,您的接口也不能标记为协变。
您可能想看看 。
你可以欺骗和使用
IObservable<out T>
几乎与Task<T>
相同,但具有协变类型。它总是可以在您需要时转换为任务。您在代码的可读性方面损失了一点,因为假定(并强制)使用 Task<T>
您只能得到一个结果,但使用 IObservable<T>
可以得到很多结果。但是你可以做
IObservable<T> o = ...
var foo = await o;
和
一样
Task<T> t = ...
var foo = await t;
请注意,您需要包含 Rx library 中的 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
如果您可以使用 .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; }
}
可以找到有关协变异步的更多信息here。
鉴于以下代码片段,我完全不明白为什么我不可能实现什么:
接口:
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);
错误:
method return type must be output safe. invalid variance: the type parameter T must be invariantly valid on Task
请帮助我理解,为什么我不能将 <out T>
与 Task<T>
在决定某些泛型接口中泛型类型参数的变化时,您必须考虑接口内泛型类型参数的所有使用。每次使用都可能引入一些关于方差的约束。这包括:
- 用作方法的输入参数 - 不允许协方差
- 用作方法中的 return 值 - 不允许逆变
- 作为其他泛型推导的一部分使用,例如
Task<T>
- 此类使用可能不允许协变、不允许逆变或两者兼而有之
我是用反逻辑来强调所有这些情况基本上都是在引入约束。分析后,您将知道是否有任何元素不允许协变,然后您的参数可能不会声明为 out
。相反,如果任何元素不允许逆变,则您的参数可能不会声明为 in
.
在您的特定情况下,第一个 Search
方法 returns IEnumerable<T>
,呈现逆变不适用。但是,SearchAsync returning Task<IEnumerable<T>>
,并且该用法引入了存在于 Task 类型中的约束 - 它是不变的,这意味着这次 out
也是不可能的。
结果是您的泛型接口必须在其泛型参数类型上保持不变才能满足其所有方法的签名。
Task<T>
不是协变的。差异只能应用于通用 interfaces(和委托,但这与此处无关)。
例如Task<IEnumerable<Dog>>
无法 分配给 Task<IEnumerable<Animal>>
。因此,您的接口也不能标记为协变。
您可能想看看
你可以欺骗和使用
IObservable<out T>
几乎与Task<T>
相同,但具有协变类型。它总是可以在您需要时转换为任务。您在代码的可读性方面损失了一点,因为假定(并强制)使用 Task<T>
您只能得到一个结果,但使用 IObservable<T>
可以得到很多结果。但是你可以做
IObservable<T> o = ...
var foo = await o;
和
一样Task<T> t = ...
var foo = await t;
请注意,您需要包含 Rx library 中的 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
如果您可以使用 .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; }
}
可以找到有关协变异步的更多信息here。