基础泛型接口上的扩展方法

5

我正在实现一种流畅的构建器模式,需要在静态扩展方法中接受可枚举对象,并迭代其内容,同时对可枚举对象的内容应用函数对象。示例如下(引用的不是实际代码,仅为说明):

public static IValidator<IEnumerable<T>> Each<T>(
    this IValidator<IEnumerable<T>> enumerable, 
    Func<T, bool> action)
{
    foreach (T value in enumerable)
        action(value);

    return validator;
}

这对于可枚举类型来说效果非常好,但是对于继承的类型/接口则无法正常工作。比如说:

IValidator<IEnumerable<Guid>> validator = ...;

IEnumerable<Guid> guids = ...;
validator.Each(guids, guid => guid != Guid.Empty);   // ok

IList<Guid> guids = ...;
validator.Each(guids, guid => guid != Guid.Empty);   // doesn't compile (see below)

例外情况如下:

IValidator<IList<Guid>>不包含“Each”的定义,而且找不到接受类型为IValidator<IList<Guid>>的第一个参数的任何扩展方法“Each”(是否缺少使用指令或程序集引用?)

我的问题涉及IValidator<T>的继承链,更具体地说,涉及其泛型类型参数T。为什么类型IValidator<IEnumerable<T>>不能赋值给IValidator<IList<T>>?在我看来,没有任何情况会使IList<T>不是IEnumerable<T>(假设它们有相同的T)。
将泛型参数限制为T : IEnumerable<R>可以解决问题,但这需要两个类型参数(TR),如果可能的话,我想避免这种情况。
有什么想法?更好的解决方案吗?谢谢。

3
我认为这可能是一个协变和逆变的问题。让我看看能否找到一些示例代码,以确定是否属于这种情况。 - Jason Evans
1
即使您解决了当前的问题,该方法有什么意义呢?您没有使用传递的函数对象的布尔结果,因此,除非函数对象改变了对象,否则净结果是没有任何变化的;如果它确实改变了对象,那么...好吧...它不应该这样做,因为 Linq 是为序列的查询而设计的,而不是为序列的变异。 - Servy
@JasonEvans - 是的,看起来是这样。IEnumerable<T>是协变的,而IList<T>不是。虽然我认为问题可能在于IValidator本身不是协变的。你有克服这个问题的想法吗? - lsoliveira
1
好的,我现在没有时间编写代码。我的建议是,在使用 IValidator<T> 时尝试使用 IValidator<out T>IValidator<in T>,我忘记了哪一个。你也可以直接使用 var guids = validator.Each.....,这可能会有所帮助,但不会解决你的设计问题。 - Jason Evans
@JasonEvans - 谢谢。将接口标记为 out T 使其具有协变性是关键(这是安全的,因为 IValidator<T> 中类型为 T 的成员没有任何变异)。 - lsoliveira
显示剩余4条评论
2个回答

6

这是由于您的IValidator<T>接口定义所致。我猜测它大致类似于:

public interface IValidator<T>

你真正想要的是:
public interface IValidator<out T>

这将使您的界面 covariant,这意味着您可以将 IValidator<T2> 的实现分配给 IValidator<T>,假设 T2 派生自 T

在这种情况下,IList<T> 派生自 IEnumerable<T>,因此您应该能够将 T 声明为协变。但是,这取决于 IValidator<T> 上的方法以及它们如何公开。

换句话说,如果 IValidator<T> 上有将 T 实例作为参数传递给接口上任何方法的方法,则无法将接口声明为协变。

如果是这种情况,则您应该能够使用以下定义的 Each

public static IValidator<T> Each<T, TValue>(
    this IValidator<T> enumerable, 
    Func<TValue, bool> action) where T : IEnumerable<TValue>
{
    foreach (TValue value in enumerable)
        action(value);

    return validator;
}

这将表示T应继承自IEnumerable<TValue>

在@JasonEvans上面的评论之后,我想起我完全忘记了协变性/逆变性。你说得对。使接口具有协变性解决了这个问题。感谢您的意见。 - lsoliveira

2

两个答案:

  1. 使用 where T: IEnumerable<T> 是完全合法的,可以解决你的问题,而且不需要第二个泛型类型。

  2. IValidator<IEnumerable<T>> 不能赋值给 IValidator<IList<T>>,因为你的接口不是协变的。从 v4 开始,C# 支持接口协变,但你必须在接口定义中显式地请求它。要这样做,请参见out Generic Modifier

协变允许你在进行赋值等操作时将更具体的类型替换为期望更抽象的泛型参数类型。

请注意,你需要确保你的接口真正是安全的协变。这就是为什么在 C# 4 之前不可能实现的原因,因为它并不总是适用;有些接口是安全的协变,有些则是安全的逆变(你只能替换期望类型的更抽象类型)。这完全取决于你正在做什么。


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