Convierta List <DerivedClass> a List <BaseClass>

179

Si bien podemos heredar de la clase / interfaz base, ¿por qué no podemos declarar un List<> uso de la misma clase / interfaz?

interface A
{ }

class B : A
{ }

class C : B
{ }

class Test
{
    static void Main(string[] args)
    {
        A a = new C(); // OK
        List<A> listOfA = new List<C>(); // compiler Error
    }
}

¿Hay alguna forma de evitarlo?

Asad
fuente

Respuestas:

230

La forma de hacer que esto funcione es iterar sobre la lista y emitir los elementos. Esto se puede hacer usando ConvertAll:

List<A> listOfA = new List<C>().ConvertAll(x => (A)x);

También puedes usar Linq:

List<A> listOfA = new List<C>().Cast<A>().ToList();
Mark Byers
fuente
2
otra opción: List <A> listOfA = listOfC.ConvertAll (x => (A) x);
ahaliav fox
66
¿Cuál es más rápido? ConvertAllo Cast?
Martin Braun
24
Esto creará una copia de la lista. Si agrega o elimina algo en la nueva lista, esto no se reflejará en la lista original. Y en segundo lugar, hay una gran penalización de rendimiento y memoria, ya que crea una nueva lista con los objetos existentes. Vea mi respuesta a continuación para obtener una solución sin estos problemas.
Bigjim
Respuesta a modiX: ConvertAll es un método en la Lista <T>, por lo que solo funcionará en una lista genérica; no funcionará en IEnumerable o IEnumerable <T>; ConvertAll también puede realizar conversiones personalizadas, no solo conversión, por ejemplo, ConvertAll (pulgadas => pulgadas * 25.4). Cast <A> es un método de extensión LINQ, por lo que funciona en cualquier IEnumerable <T> (y también funciona para IEnumerable no genérico) y, como la mayoría de LINQ, utiliza ejecución diferida, es decir, solo convierte tantos elementos como sea posible recuperado Lea más sobre esto aquí: codeblog.jonskeet.uk/2011/01/13/…
Edward
172

En primer lugar, deje de usar nombres de clase imposibles de entender como A, B, C. Use Animal, Mamífero, Jirafa, o Comida, Fruta, Naranja o algo donde las relaciones sean claras.

Su pregunta es "¿por qué no puedo asignar una lista de jirafas a una variable de tipo lista de animales, ya que puedo asignar una jirafa a una variable de tipo animal?"

La respuesta es: suponga que podría. ¿Qué podría salir mal entonces?

Bueno, puedes agregar un tigre a una lista de animales. Supongamos que le permitimos poner una lista de jirafas en una variable que contiene una lista de animales. Luego intenta agregar un tigre a esa lista. ¿Lo que pasa? ¿Quieres que la lista de jirafas contenga un tigre? ¿Quieres un choque? ¿o quieres que el compilador te proteja del accidente al hacer que la asignación sea ilegal en primer lugar?

Elegimos este último.

Este tipo de conversión se denomina conversión "covariante". En C # 4, le permitiremos realizar conversiones covariantes en interfaces y delegados cuando se sabe que la conversión es siempre segura . Vea los artículos de mi blog sobre covarianza y contravarianza para más detalles. (Habrá uno nuevo sobre este tema tanto el lunes como el jueves de esta semana).

Eric Lippert
fuente
3
¿Habría algo inseguro sobre una IList <T> o ICollection <T> que implementa IList no genérico, pero que implementa que la Ilist / ICollection no genérica devuelva True para IsReadOnly y arroje NotSupportedException para cualquier método o propiedad que lo modifique?
supercat
33
Si bien esta respuesta contiene un razonamiento bastante aceptable, en realidad no es "verdadera". La respuesta simple es que C # no es compatible con esto. La idea de tener una lista de animales que contenga jirafas y tigres es perfectamente válida. El único problema se produce cuando desea acceder a las clases de nivel superior. En realidad, esto no es diferente a pasar una clase padre como parámetro a una función y luego tratar de convertirla en una clase hermana diferente. Puede haber problemas técnicos con la implementación del reparto, pero la explicación anterior no proporciona ninguna razón por la cual sería una mala idea.
Paul Coldrey
2
@EricLippert ¿Por qué podemos hacer esta conversión usando un en IEnumerablelugar de un List? es decir: List<Animal> listAnimals = listGiraffes as List<Animal>;no es posible, pero IEnumerable<Animal> eAnimals = listGiraffes as IEnumerable<Animal>funciona.
LINQ
3
@jbueno: Lee el último párrafo de mi respuesta. Se sabe que la conversión es segura . ¿Por qué? Porque es imposible convertir una secuencia de jirafas en una secuencia de animales y luego poner un tigre en la secuencia de animales . IEnumerable<T>y IEnumerator<T>ambos están marcados como seguros para la covarianza, y el compilador lo ha verificado.
Eric Lippert
60

Para citar la gran explicación de Eric

¿Lo que pasa? ¿Quieres que la lista de jirafas contenga un tigre? ¿Quieres un choque? ¿o quieres que el compilador te proteja del accidente al hacer que la asignación sea ilegal en primer lugar? Elegimos este último.

Pero, ¿qué sucede si desea elegir un bloqueo en tiempo de ejecución en lugar de un error de compilación? Normalmente usaría Cast <> o ConvertAll <> pero luego tendrá 2 problemas: creará una copia de la lista. Si agrega o elimina algo en la nueva lista, esto no se reflejará en la lista original. Y en segundo lugar, hay una gran penalización de rendimiento y memoria ya que crea una nueva lista con los objetos existentes.

Tuve el mismo problema y, por lo tanto, creé una clase de contenedor que puede emitir una lista genérica sin crear una lista completamente nueva.

En la pregunta original, podría usar:

class Test
{
    static void Main(string[] args)
    {
        A a = new C(); // OK
        IList<A> listOfA = new List<C>().CastList<C,A>(); // now ok!
    }
}

y aquí la clase wrapper (+ un método de extensión CastList para un uso fácil)

public class CastedList<TTo, TFrom> : IList<TTo>
{
    public IList<TFrom> BaseList;

    public CastedList(IList<TFrom> baseList)
    {
        BaseList = baseList;
    }

    // IEnumerable
    IEnumerator IEnumerable.GetEnumerator() { return BaseList.GetEnumerator(); }

    // IEnumerable<>
    public IEnumerator<TTo> GetEnumerator() { return new CastedEnumerator<TTo, TFrom>(BaseList.GetEnumerator()); }

    // ICollection
    public int Count { get { return BaseList.Count; } }
    public bool IsReadOnly { get { return BaseList.IsReadOnly; } }
    public void Add(TTo item) { BaseList.Add((TFrom)(object)item); }
    public void Clear() { BaseList.Clear(); }
    public bool Contains(TTo item) { return BaseList.Contains((TFrom)(object)item); }
    public void CopyTo(TTo[] array, int arrayIndex) { BaseList.CopyTo((TFrom[])(object)array, arrayIndex); }
    public bool Remove(TTo item) { return BaseList.Remove((TFrom)(object)item); }

    // IList
    public TTo this[int index]
    {
        get { return (TTo)(object)BaseList[index]; }
        set { BaseList[index] = (TFrom)(object)value; }
    }

    public int IndexOf(TTo item) { return BaseList.IndexOf((TFrom)(object)item); }
    public void Insert(int index, TTo item) { BaseList.Insert(index, (TFrom)(object)item); }
    public void RemoveAt(int index) { BaseList.RemoveAt(index); }
}

public class CastedEnumerator<TTo, TFrom> : IEnumerator<TTo>
{
    public IEnumerator<TFrom> BaseEnumerator;

    public CastedEnumerator(IEnumerator<TFrom> baseEnumerator)
    {
        BaseEnumerator = baseEnumerator;
    }

    // IDisposable
    public void Dispose() { BaseEnumerator.Dispose(); }

    // IEnumerator
    object IEnumerator.Current { get { return BaseEnumerator.Current; } }
    public bool MoveNext() { return BaseEnumerator.MoveNext(); }
    public void Reset() { BaseEnumerator.Reset(); }

    // IEnumerator<>
    public TTo Current { get { return (TTo)(object)BaseEnumerator.Current; } }
}

public static class ListExtensions
{
    public static IList<TTo> CastList<TFrom, TTo>(this IList<TFrom> list)
    {
        return new CastedList<TTo, TFrom>(list);
    }
}
Bigjim
fuente
1
Lo acabo de usar como modelo de vista MVC y obtuve una buena vista parcial de la maquinilla de afeitar universal. Idea increíble! Estoy tan feliz de haber leído esto.
Stefan Cebulak
55
Solo agregaría un "where TTo: TFrom" en la declaración de clase, para que el compilador pueda advertir contra el uso incorrecto. Hacer una CastedList de tipos no relacionados no tiene sentido de todos modos, y hacer una "CastedList <TBase, TDerived>" sería inútil: no puede agregarle objetos TBase normales, y cualquier TDerived que obtenga de la Lista original ya puede usarse como una TBasa
Wolfzoon el
2
@PaulColdrey Alas, la culpa es de una ventaja de seis años.
Wolfzoon el
@Wolfzoon: el estándar .Cast <T> () tampoco tiene esta restricción. Quería hacer que el comportamiento sea el mismo que .Cast para que sea posible lanzar una lista de animales a una lista de tigres, y tener una excepción cuando contendría una jirafa, por ejemplo. Tal como sería con Cast ...
Bigjim
Hola @Bigjim, tengo problemas para entender cómo usar tu clase de envoltura, para lanzar una matriz existente de jirafas en una matriz de animales (clase base). Cualquier consejo será apreciado :)
Denis Vitez
28

Si lo usa IEnumerable, funcionará (al menos en C # 4.0, no he probado versiones anteriores). Esto es solo un reparto, por supuesto, seguirá siendo una lista.

En vez de -

List<A> listOfA = new List<C>(); // compiler Error

En el código original de la pregunta, use -

IEnumerable<A> listOfA = new List<C>(); // compiler error - no more! :)

PhistucK
fuente
¿Cómo usar exactamente el IEnumerable en ese caso?
Vladius
1
En lugar de List<A> listOfA = new List<C>(); // compiler Erroren el código original de la pregunta, ingreseIEnumerable<A> listOfA = new List<C>(); // compiler error - no more! :)
PhistucK
El método que toma IEnumerable <BaseClass> como parámetro permitirá que la clase heredada se pase como una Lista. Entonces, hay muy poco que hacer excepto cambiar el tipo de parámetro.
beauXjames
27

En cuanto a por qué no funciona, podría ser útil comprender la covarianza y la contravarianza .

Solo para mostrar por qué esto no debería funcionar, aquí hay un cambio en el código que proporcionó:

void DoesThisWork()
{
     List<C> DerivedList = new List<C>();
     List<A> BaseList = DerivedList;
     BaseList.Add(new B());

     C FirstItem = DerivedList.First();
}

¿Debería funcionar esto? El primer elemento de la lista es del tipo "B", pero el tipo del elemento DerivedList es C.

Ahora, suponga que realmente solo queremos hacer una función genérica que opere en una lista de algún tipo que implemente A, pero no nos importa de qué tipo sea:

void ThisWorks<T>(List<T> GenericList) where T:A
{

}

void Test()
{
     ThisWorks(new List<B>());
     ThisWorks(new List<C>());
}
Chris Pitman
fuente
"¿Debería funcionar esto?" - Yo sí y no. No veo ninguna razón por la que no pueda escribir el código y hacer que falle en el momento de la compilación, ya que en realidad está haciendo una conversión no válida en el momento en que accede a FirstItem como tipo 'C'. Hay muchas formas análogas de hacer explotar C # que son compatibles. Si realmente desea lograr esta funcionalidad por una buena razón (y hay muchas), la respuesta de bigjim a continuación es increíble.
Paul Coldrey
16

Solo puedes enviar a listas de solo lectura. Por ejemplo:

IEnumerable<A> enumOfA = new List<C>();//This works
IReadOnlyCollection<A> ro_colOfA = new List<C>();//This works
IReadOnlyList<A> ro_listOfA = new List<C>();//This works

Y no puede hacerlo para listas que admiten elementos de guardado. La razón por la cual es:

List<string> listString=new List<string>();
List<object> listObject=(List<object>)listString;//Assume that this is possible
listObject.Add(new object());

¿Ahora que? Recuerde que listObject y listString son la misma lista en realidad, por lo que listString ahora tiene un elemento objeto: no debería ser posible y no lo es.

Wojciech Mikołajewicz
fuente
1
Esta fue la respuesta más comprensible para mí. Especialmente porque menciona que hay algo llamado IReadOnlyList, que funcionará porque garantiza que no se agregarán más elementos.
bendtherules
Es una pena que no puedas hacer algo similar para IReadOnlyDictionary <TKey, TValue>. ¿Porqué es eso?
Alastair Maw
Esta es una gran sugerencia. Uso List <> en todas partes por conveniencia, pero la mayoría de las veces quiero que sea de solo lectura. Buena sugerencia ya que esto mejorará mi código de 2 maneras al permitir este reparto y mejorar donde especifique solo lectura.
Rick Love el
1

Personalmente me gusta crear librerías con extensiones a las clases.

public static List<TTo> Cast<TFrom, TTo>(List<TFrom> fromlist)
  where TFrom : class 
  where TTo : class
{
  return fromlist.ConvertAll(x => x as TTo);
}
vikingfabian
fuente
0

Porque C # no permite ese tipo de herenciaconversión en este momento .

Seda del mediodía
fuente
66
Primero, esta es una cuestión de convertibilidad, no de herencia. En segundo lugar, la covarianza de los tipos genéricos no funcionará en los tipos de clase, solo en los tipos de interfaz y delegado.
Eric Lippert el
55
Bueno, casi no puedo discutir contigo.
Mediodía Seda
0

Esta es una extensión de la brillante respuesta de BigJim .

En mi caso, tuve una NodeBaseclase con un Childrendiccionario y necesitaba una forma genérica de hacer búsquedas O (1) de los niños. Intentaba devolver un campo de diccionario privado en el captador de Children, así que obviamente quería evitar las costosas copias / iteraciones. Por lo tanto, utilicé el código de Bigjim para convertirlo Dictionary<whatever specific type>en un genérico Dictionary<NodeBase>:

// Abstract parent class
public abstract class NodeBase
{
    public abstract IDictionary<string, NodeBase> Children { get; }
    ...
}

// Implementing child class
public class RealNode : NodeBase
{
    private Dictionary<string, RealNode> containedNodes;

    public override IDictionary<string, NodeBase> Children
    {
        // Using a modification of Bigjim's code to cast the Dictionary:
        return new IDictionary<string, NodeBase>().CastDictionary<string, RealNode, NodeBase>();
    }
    ...
}

Esto funcionó bien. Sin embargo, eventualmente encontré limitaciones no relacionadas y terminé creando un FindChild()método abstracto en la clase base que haría las búsquedas en su lugar. Al final resultó que esto eliminó la necesidad del diccionario fundido en primer lugar. (Pude reemplazarlo con un simple IEnumerablepara mis propósitos).

Entonces, la pregunta que puede hacer (especialmente si el rendimiento es un problema que le prohíbe usar .Cast<>o .ConvertAll<>) es:

"¿Realmente necesito lanzar la colección completa, o puedo usar un método abstracto para mantener el conocimiento especial necesario para realizar la tarea y así evitar el acceso directo a la colección?"

A veces la solución más simple es la mejor.

Zach
fuente
0

También puede usar el System.Runtime.CompilerServices.Unsafepaquete NuGet para crear una referencia al mismo List:

using System.Runtime.CompilerServices;
...
class Tool { }
class Hammer : Tool { }
...
var hammers = new List<Hammer>();
...
var tools = Unsafe.As<List<Tool>>(hammers);

Dado el ejemplo anterior, puede acceder a las Hammerinstancias existentes en la lista utilizando la toolsvariable. Agregar Toolinstancias a la lista genera una ArrayTypeMismatchExceptionexcepción porque hace toolsreferencia a la misma variable que hammers.

Dibujó
fuente
0

He leído todo este hilo, y solo quiero señalar lo que me parece una inconsistencia.

El compilador le impide realizar la tarea con Listas:

List<Tiger> myTigersList = new List<Tiger>() { new Tiger(), new Tiger(), new Tiger() };
List<Animal> myAnimalsList = myTigersList;    // Compiler error

Pero el compilador está perfectamente bien con las matrices:

Tiger[] myTigersArray = new Tiger[3] { new Tiger(), new Tiger(), new Tiger() };
Animal[] myAnimalsArray = myTigersArray;    // No problem

El argumento sobre si se sabe que la asignación es segura se desmorona aquí. La asignación que hice con la matriz no es segura . Para probar eso, si sigo con esto:

myAnimalsArray[1] = new Giraffe();

Recibo una excepción de tiempo de ejecución "ArrayTypeMismatchException". ¿Cómo se explica esto? Si el compilador realmente quiere evitar que haga algo estúpido, debería haberme impedido hacer la asignación de la matriz.

Rajeev Goel
fuente