从List<X>到List<Y>的转换是否有更短的语法?

287

我知道将一个类型的项目列表转换为另一个类型是可能的(假设你的对象有一个公共静态显式操作符方法来进行转换),可以按照以下方式逐个进行转换:

List<Y> ListOfY = new List<Y>();

foreach(X x in ListOfX)
    ListOfY.Add((Y)x);

但是难道不可能一次性将整个列表转换类型吗?例如,

ListOfY = (List<Y>)ListOfX;

1
假设X派生自Y,而Z也派生自Y,那么如果您将Z添加到List<Y>中,而该列表实际上是List<X>,会发生什么呢? - Richard Friend
7个回答

622

如果X确实可以转换为Y,那么您应该能够使用

List<Y> listOfY = listOfX.Cast<Y>().ToList();

需要注意的一些事情(感谢评论者!)

  • 您必须包含using System.Linq;才能获取此扩展方法。
  • 这个方法会将列表中的每个项强制转换 - 而不是列表本身。调用ToList()将创建一个新的List<Y>
  • 此方法不支持自定义转换运算符。(请参见为什么 Linq Cast<> 助手与隐式转换运算符不兼容?
  • 此方法不适用于具有显式转换运算符方法的对象(框架 4.0)。

15
再获得一枚金徽章。这非常有用。 - ouflak
2
我不得不谷歌一下 H/T,顺便说一句,我从未在我的生活中见过这个。 - Clonkex
是的,“H/T”指的是“Hat tipping”,或者基本上是感谢。 - Onat Korucu

116

直接强制类型转换 var ListOfY = (List<Y>)ListOfX 是不可能的,因为它要求 List<T> 类型具有 协变性和逆变性,但这在每种情况下都无法保证。请继续阅读以了解此强制转换问题的解决方案。

尽管写下类似以下代码看起来很正常:

List<Animal> animals = (List<Animal>) mammalList;

因为我们可以保证每只哺乳动物都是动物,这显然是错误的:

List<Mammal> mammals = (List<Mammal>) animalList;

由于并非所有动物都是哺乳动物,因此会出现这种情况。

然而,使用C# 3及以上版本,您可以使用

IEnumerable<Animal> animals = mammalList.Cast<Animal>();

这种方法稍微简化了类型转换。它在语法上等同于逐个添加的代码,因为它使用显式转换将列表中的每个Mammal转换为Animal,如果转换失败会导致失败。

如果您需要更多对类型转换过程的控制,可以使用List<T>类的ConvertAll方法,该方法可以使用提供的表达式来转换列表项。 它还有一个额外的好处,就是返回一个List,而不是IEnumerable,因此不需要使用.ToList()

List<object> o = new List<object>();
o.Add("one");
o.Add("two");
o.Add(3);

IEnumerable<string> s1 = o.Cast<string>(); //fails on the 3rd item
List<string> s2 = o.ConvertAll(x => x.ToString()); //succeeds

3
直到现在我才相信我从未为这个答案点赞。它比我上面写的好多了。 - Jamiec
8
@Jamiec,我没有点赞是因为他以“不,这不可能”开头,而且还埋藏了许多人寻找这个问题的答案。从技术上讲,他对OP的问题回答得更全面,但他并没有直接回答问题。 - Dan Bechard

16

继续说 Sweko 的观点:

这就是为什么要进行强制类型转换的原因

var listOfX = new List<X>();
ListOf<Y> ys = (List<Y>)listOfX; // Compile error: Cannot implicitly cast X to Y

由于 List<T> 在类型 T 上是不变的,因此无论 X 是否派生自 Y,都不可能实现。这是因为 List<T> 的定义如下:
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T> ... // Other interfaces

(请注意,在此声明中,这里的类型 T 没有其他变体修饰符)
然而,如果您的设计不需要可变集合,则可以在许多不可变集合上进行向上转换,例如,前提是Giraffe派生自Animal
IEnumerable<Animal> animals = giraffes;

这是因为 IEnumerable<T> 支持在 T 上的协变 - 这很有意义,因为 IEnumerable 意味着集合不能被更改,因为它没有支持添加或删除元素的方法。请注意在 IEnumerable<T> 声明中的 out 关键字:
public interface IEnumerable<out T> : IEnumerable

(这里有进一步的解释,为什么可变集合如List不支持covariance,而不可变迭代器和集合可以。)

使用.Cast<T>()进行强制转换

正如其他人所提到的那样,可以将.Cast<T>()应用于集合以投影出一个新的元素类型为T的集合,但是如果其中一个或多个元素的强制转换不可能,则会抛出InvalidCastException(这与在OP的foreach循环中进行显式转换的行为相同)。

使用OfType<T>()进行筛选和强制转换

如果输入列表包含不同或不兼容类型的元素,则可以使用 .OfType<T>() 代替 .Cast<T>() 来避免潜在的 InvalidCastException. (.OfType<>() 在尝试转换之前检查元素是否可以转换为目标类型,并过滤掉不兼容的类型。)
还要注意,如果 OP 改成这样写: (请注意 foreach 中的显式 Y y)
List<Y> ListOfY = new List<Y>();

foreach(Y y in ListOfX)
{
    ListOfY.Add(y);
}

如果无法进行强制类型转换,将尝试进行转换。但是,如果无法进行转换,则会导致InvalidCastException

示例

例如,给定简单的(C#6)类层次结构:

public abstract class Animal
{
    public string Name { get;  }
    protected Animal(string name) { Name = name; }
}

public class Elephant :  Animal
{
    public Elephant(string name) : base(name){}
}

public class Zebra : Animal
{
    public Zebra(string name)  : base(name) { }
}

当处理混合类型的集合时:

var mixedAnimals = new Animal[]
{
    new Zebra("Zed"),
    new Elephant("Ellie")
};

foreach(Animal animal in mixedAnimals)
{
     // Fails for Zed - `InvalidCastException - cannot cast from Zebra to Elephant`
     castedAnimals.Add((Elephant)animal);
}

var castedAnimals = mixedAnimals.Cast<Elephant>()
    // Also fails for Zed with `InvalidCastException
    .ToList();

鉴于:

var castedAnimals = mixedAnimals.OfType<Elephant>()
    .ToList();
// Ellie

筛选出只有大象 - 即斑马被消除。

关于隐式转换运算符

没有动态性,用户定义的转换运算符仅在编译时*使用,因此即使提供了斑马和大象之间的转换运算符,上述转换方法的运行时行为也不会改变。

如果我们添加一个将斑马转换为大象的转换运算符:

public class Zebra : Animal
{
    public Zebra(string name) : base(name) { }
    public static implicit operator Elephant(Zebra z)
    {
        return new Elephant(z.Name);
    }
}

相反,鉴于上述的转换运算符,编译器将能够将下面的数组从Animal[]类型更改为Elephant[]类型,因为现在斑马可以被转换为一个同质的大象集合:

var compilerInferredAnimals = new []
{
    new Zebra("Zed"),
    new Elephant("Ellie")
};

在运行时使用隐式转换运算符
正如Eric所提到的,可以通过使用dynamic,在运行时访问转换运算符:
var mixedAnimals = new Animal[] // i.e. Polymorphic collection
{
    new Zebra("Zed"),
    new Elephant("Ellie")
};

foreach (dynamic animal in mixedAnimals)
{
    castedAnimals.Add(animal);
}
// Returns Zed, Ellie

嘿,我刚刚尝试了“使用foreach()进行类型过滤”的示例,使用如下代码:var list = new List<object>() { 1, "a", 2, "b", 3, "c", 4, "d" };foreach (int i in list) Console.WriteLine(i);但当我运行它时,我收到“指定的强制转换无效。”消息。我有什么遗漏的吗?我不认为foreach会这样工作,这就是为什么我在尝试它。 - Brent Rittenhouse
此外,这不是引用类型与值类型的问题。我刚试了一下,使用一个基类'Thing'和两个派生类:'Person'和'Animal'。当我对它做同样的事情时,我得到了:"无法将类型为'Animal'的对象强制转换为类型'Person'。"所以它肯定是在遍历每个元素。如果我在列表上执行OfType操作,那么它就可以工作了。如果ForEach必须检查这个,那么它可能会非常慢,除非编译器进行了优化。 - Brent Rittenhouse
谢谢Brent - 我走了弯路。foreach不会过滤,但是使用更具体的类型作为迭代变量将强制编译器尝试进行转换,这将在第一个不符合条件的元素上失败。 - StuartLC

10
您可以使用List<Y>.ConvertAll<T>([将Y转换为T的转换器]);

6
这并不完全回答这个问题,但对于一些人可能会有用:正如@SWeko所说,由于协变性和逆变性,List<X>不能转换为List<Y>,但List<X>可以转换为IEnumerable<Y>,甚至可以进行隐式转换。
示例:
List<Y> ListOfY = new List<Y>();
List<X> ListOfX = (List<X>)ListOfY; // Compile error

但是

List<Y> ListOfY = new List<Y>();
IEnumerable<X> EnumerableOfX = ListOfY;  // No issue

最大的优势是它不会在内存中创建新的列表。


1
我喜欢这个的原因是,如果你有一个大的源列表,在开始时不会有性能损失。相反,每个被接收器处理的条目都有一个小的、不可察觉的转换。而且没有巨大的内存积累。非常适合处理流。 - Johan Franzén

1
如果 X 派生自 Y,您也可以使用 ToList<T> 方法代替 Cast<T>
listOfX.ToList<Y>()

-1
dynamic data = List<x> val;  
List<y> val2 = ((IEnumerable)data).Cast<y>().ToList();

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