¿Sintaxis más corta para pasar de una Lista <X> a una Lista <Y>?

237

Sé que es posible emitir una lista de elementos de un tipo a otro (dado que su objeto tiene un método de operador explícito estático público para realizar la conversión) uno a la vez de la siguiente manera:

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

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

Pero, ¿no es posible emitir la lista completa al mismo tiempo? Por ejemplo,

ListOfY = (List<Y>)ListOfX;
Jimbo
fuente
@Oded: Intenté aclararlo un poco. No te preocupes, no lo estás, lo entiendo :)
BoltClock
1
Suponiendo que X se deriva de Y, y Z se deriva de Y, piense qué sucedería si agregara Z a su Lista <Y>, que en realidad es una Lista <X>.
Richard Friend

Respuestas:

497

Si Xrealmente se te puede lanzar Y, deberías poder usar

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

Algunas cosas a tener en cuenta (¡H / T a los comentaristas!)

Jamiec
fuente
12
Tener otra insignia de oro. Esto fue bastante útil.
ouflak
66
Debe incluir la siguiente línea para que el compilador reconozca esos métodos de extensión: using System.Linq;
hypehuman
8
También tenga en cuenta que aunque esto arroja cada elemento de la lista, la lista en sí no se lanza; más bien se crea una nueva lista con el tipo deseado.
hipehumano
44
También tenga en cuenta que el Cast<T>método no admite operadores de conversión personalizados. ¿Por qué el Linq Cast Helper no funciona con el operador de transmisión implícito ?
clD
No funciona para un objeto que tiene un método de operador explícito (marco 4.0)
Adrian
100

El lanzamiento directo var ListOfY = (List<Y>)ListOfXno es posible porque requeriría co / contravarianza delList<T> tipo, y eso no puede garantizarse en todos los casos. Siga leyendo para ver las soluciones a este problema de transmisión.

Si bien parece normal poder escribir código como este:

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

porque podemos garantizar que cada mamífero será un animal, esto obviamente es un error:

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

ya que no todos los animales son mamíferos.

Sin embargo, usando C # 3 y superior, puede usar

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

eso facilita un poco el lanzamiento. Esto es sintácticamente equivalente a su código de adición uno por uno, ya que utiliza un reparto explícito para convertir cada uno Mammalen la lista en un Animal, y fallará si el reparto no tiene éxito.

Si desea tener más control sobre el proceso de conversión / conversión, puede usar el ConvertAllmétodo de la List<T>clase, que puede usar una expresión proporcionada para convertir los elementos. Tiene el beneficio añadido de que devuelve un List, en lugar de IEnumerable, por lo que no .ToList()es necesario.

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
SWeko
fuente
2
No puedo creer que nunca haya hecho +1 en esta respuesta hasta ahora. Es mucho mejor que el mío de arriba.
Jamiec
66
@Jamiec No hice +1 porque comienza con "No, no es posible", mientras entierra la respuesta que muchos que encuentran esta pregunta están buscando. Técnicamente, respondió la pregunta del OP más a fondo sin embargo.
Dan Bechard
13

Para agregar al punto de Sweko:

La razón por la cual el elenco

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

no es posible porque List<T>es invariante en el Tipo T y, por lo tanto, no importa si se Xderiva de Y), esto se debe a que List<T>se define como:

public class List<T> : IList<T>, ICollection<T>, IEnumerable<T> ... // Other interfaces

(Tenga en cuenta que en esta declaración, escriba Taquí no tiene modificadores de varianza adicionales)

Sin embargo, si no se requieren colecciones mutables en su diseño, es posible una transmisión ascendente en muchas de las colecciones inmutables , por ejemplo, siempre que se Giraffederive de Animal:

IEnumerable<Animal> animals = giraffes;

Esto se debe a que IEnumerable<T>admite la covarianza T: esto tiene sentido dado que IEnumerableimplica que la colección no se puede cambiar, ya que no admite métodos para Agregar o quitar elementos de la colección. Tenga outen cuenta la palabra clave en la declaración de IEnumerable<T>:

public interface IEnumerable<out T> : IEnumerable

( Aquí hay una explicación más detallada de la razón por la cual las colecciones mutables como Listno pueden soportar covariance, mientras que los iteradores y colecciones inmutables sí pueden).

Fundición con .Cast<T>()

Como otros han mencionado, .Cast<T>()se puede aplicar a una colección para proyectar una nueva colección de elementos lanzados a T, sin embargo, hacerlo arrojará un resultado InvalidCastExceptionsi no es posible lanzar uno o más elementos (lo que sería el mismo comportamiento que hacer lo explícito). emitido en el foreachbucle del OP ).

Filtrado y Fundición con OfType<T>()

Si la lista de entrada contiene elementos de tipos diferentes e incompatibles, InvalidCastExceptionse puede evitar el potencial utilizando en .OfType<T>()lugar de .Cast<T>(). ( .OfType<>()comprueba si un elemento se puede convertir al tipo de destino, antes de intentar la conversión, y filtra los tipos incompatibles).

para cada

También tenga en cuenta que si el OP había escrito esto en su lugar: (tenga en cuenta lo explícitoY y en el foreach)

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

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

que también se intentará el casting. Sin embargo, si no es posible lanzar, se InvalidCastExceptionobtendrá un resultado.

Ejemplos

Por ejemplo, dada la jerarquía de clases simple (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) { }
}

Al trabajar con una colección de tipos mixtos:

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();

Mientras:

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

filtra solo los elefantes, es decir, se eliminan las cebras.

Re: operadores de reparto implícito

Sin dinámica, los operadores de conversión definidos por el usuario solo se usan en tiempo de compilación *, por lo que incluso si un operador de conversión entre, por ejemplo, Zebra y Elephant estuviera disponible, el comportamiento del tiempo de ejecución anterior de los enfoques de conversión no cambiaría.

Si agregamos un operador de conversión para convertir una cebra en un elefante:

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

En cambio, dado el operador de conversión anterior, el compilador podrá cambiar el tipo de la matriz a continuación de Animal[]a Elephant[], dado que las cebras ahora se pueden convertir en una colección homogénea de elefantes:

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

Uso de operadores de conversión implícita en tiempo de ejecución

* Como mencionó Eric, se puede acceder al operador de conversión en tiempo de ejecución recurriendo a 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
StuartLC
fuente
Hola, acabo de probar el ejemplo "Usando foreach () para el filtrado de tipos" usando: var list = new List <object> () {1, "a", 2, "b", 3, "c", 4, " d "}; foreach (int i en la lista) Console.WriteLine (i); y cuando lo ejecuto aparece "El reparto especificado no es válido". ¿Me estoy perdiendo de algo? No pensé que foreach funcionara de esta manera, por eso lo estaba intentando.
Brent Rittenhouse
Además, no es una referencia frente a un tipo de valor. Acabo de probarlo con una clase base de 'Cosa' y dos clases derivadas: 'Persona' y 'Animal'. Cuando hago lo mismo con él me sale: "No se puede lanzar un objeto de tipo 'Animal' para escribir 'Persona'". Entonces definitivamente está iterando a través de cada elemento. Si tuviera que hacer un OfType en la lista, entonces funcionaría. ForEach probablemente sería realmente lento si tuviera que verificar esto, a menos que el compilador lo optimice.
Brent Rittenhouse
Gracias Brent, estaba fuera de curso allí. foreachno se filtra, pero usar un tipo más derivado como la variable de iteración obligará al compilador a intentar un Cast, que fallará en el primer elemento que no cumpla.
StuartLC
7

Puedes usar List<Y>.ConvertAll<T>([Converter from Y to T]);

Andrey
fuente
3

Esta no es exactamente la respuesta a esta pregunta, pero puede ser útil para algunos: como dijo @SWeko, gracias a la covarianza y la contravarianza, List<X>no se puede incorporar List<Y>, pero List<X>se puede convertir enIEnumerable<Y> , , e incluso con conversión implícita.

Ejemplo:

List<Y> ListOfY = new List<Y>();
List<X> ListOfX = (List<X>)ListOfY; // Compile error

pero

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

La gran ventaja es que no crea una nueva lista en la memoria.

Xav987
fuente
1
Me gusta esto porque si tienes una gran lista de fuentes, no hay un impacto en el rendimiento al principio. En cambio, hay un pequeño reparto no notable para cada entrada que el receptor procesa. Además, no se acumula gran memoria. Perfecto para procesar flujos.
Johan Franzén
-2
dynamic data = List<x> val;  
List<y> val2 = ((IEnumerable)data).Cast<y>().ToList();
usuario2361134
fuente