IEnumerable<T>转换为扩展方法问题

4
我有以下类和扩展类(以此为例):

我有以下类和扩展类(以此为例):

public class Person<T>
{
    public T Value { get; set; }
}

public static class PersonExt
{
    public static void Process<TResult>(this Person<IEnumerable<TResult>> p)
    {
        // Do something with .Any().
        Console.WriteLine(p.Value.Any());
    }
}

我原以为能够编写如下代码并使其工作,但实际上它并不能:

var x = new Person<List<String>>();
x.Process();

由于List在继承树中比IEnumerable低,所以这应该是有效的,对吗?当然,如果我新创建一个Person >,那么它就有效,因为那是直接类型。
我正在尝试使用一个可以应用于所有Person的扩展方法,只要T实现了IEnumerable,因为我需要使用Any()方法。
编辑:也许我对协变的理解有误?我知道IEnumerable应该转换为IEnumerable,但IList不能转换为IEnumerable?
编辑2:忘记提到我正在使用.net 4.0。

关于您的编辑,那些是不同的概念,IList<T> 实现了 ICollection<T> ,而 ICollection<T> 又实现了 IEnumerable<T> ,因此转换是被允许的,这种情况下它不需要是协变或逆变的。 - Nick Craver
我现在明白了协变性/逆变性的概念,因为我过去几分钟一直在阅读相关内容。那么,我写的代码为什么无效呢?从直觉上看,它应该是有效的... - TheCloudlessSky
1
如果你在使用 C# 4.0,我猜想它会工作,但是在 C# 3.0 及更低版本中则不会。 - Jon Limjap
我用C#4.0做这件事,但它没有起作用。 - TheCloudlessSky
3个回答

2
我知道`IEnumerable`可以转换为`IEnumerable`,但为什么`IList`无法转换为`IEnumerable`?
`IList`可以转换为`IEnumerable`。问题在于你试图将`Person>`转换为`Person>`,这是不合法的。例如,以下语句是完全有效的:
var x = new Person<IEnumerable<String>>();
x.Value = new string[0];

由于Value的类型是IEnumerable<String>,而字符串数组也是IEnumerable<String>。但是,您不能这样写:

var x = new Person<List<String>>();
x.Value = new string[0];

由于Value是List<String>类型,因此在所有可以使用Person<IEnumerable<String>>的地方都不能使用Person<List<String>>,因此它不是一个合法的转换。

请注意,如果向您的扩展方法添加第二个类型参数,则可以执行类似于所需操作的操作:

public static void Process<TResult, TList>(this Person<TList> p)
    where TList : IEnumerable<TResult>
{
    Console.WriteLine(p.Value.Any());
}

不幸的是,编译器无法推断两个类型参数,因此您需要像这样调用它:

var x = new Person<List<String>>();
x.Process<String, List<String>>();

如果您使用的是 C# 4.0 并且可以使用协变性,则可以为人员定义一个协变的接口:interface
public interface IPerson<out T>
{
    T Value { get; }
}

public class Person<T>
    : IPerson<T>
{
    public T Value { get; set; }
}

然后编写您的扩展方法:

public static void Process<TResult>(this IPerson<IEnumerable<TResult>> p)
{
    // Do something with .Any().
    Console.WriteLine(p.Value.Any());
}

由于 IPerson<T>.Value 是只读的,因此可以在任何使用 IPerson<IEnumerable<String>> 的地方使用 IPerson<List<String>>,并且转换是有效的。


是的,我在接口上只是缺少了 out 关键字。谢谢! - TheCloudlessSky

1

我不确定你是否完全掌握了泛型的正确使用方式。无论如何...

唯一不正确的是你的扩展方法声明,以及你尝试约束扩展方法的方式。

public static class ThingExtensions
{
    public static void Process<T>(this Thing<T> p)
        where T : IEnumerable<string>
    {
        // Do something with .Any().
        Console.WriteLine(p.Value.Any());
    }
}

我所做的就是将Person重命名为Thing,这样我们就不会被一个Person<List<string>>搞糊涂了。

public class Thing<T>
{
    public T Value { get; set; }
}

class ListOfString : List<string>
{ }

class Program
{
    static void Main(string[] args)
    {
        var x = new Thing<ListOfString>();
        x.Value = new ListOfString();
        x.Process();

        x.Value.Add("asd");
        x.Process();


        var x2 = new Thing<int>();
        // Error    1   The type 'int' cannot be used as type parameter 'T' 
        // in the generic type or method 
        // 'ThingExtensions.Process<T>(Thing<T>)'. 
        // There is no boxing conversion from 'int' to 
        // 'System.Collections.Generic.IEnumerable<string>'.    
        //x2.Process();

        Console.Read();
    }
}

如果更适用的话,您也可以将通用约束移动到 Thing<T> 中。


我应该提到这是一个使用.NET 3.5(VS 2008)的控制台应用程序。 - Robert Paulson
你也可以将 ListOfString 替换为 List<string> 或任何实现了 IEnumerable<string> 的东西。 - Robert Paulson

0
你提到了协变性,但实际上并没有使用它。你必须在泛型参数上指定inout。请注意,协变/逆变不适用于类类型;它们必须应用于接口。
因此,引入一个接口并使其具有协变性:
public interface IPerson<out T>
{
  T Value { get; }
}

public class Person<T> : IPerson<T>
{
  public T Value { get; set; }
}

public static class PersonExt
{
  public static void Process<TResult>(this IPerson<IEnumerable<TResult>> p)
  {
    // Do something with .Any(). 
    Console.WriteLine(p.Value.Any());
  }
}

允许此代码编译:

var x = new Person<List<String>>();  
x.Process();  

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