Pregunta

Si bien podemos heredar de la clase / interfaz base, ¿por qué no podemos declarar una List < > usando 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?

¿Fue útil?

Solución

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

Otros consejos

Antes que nada, 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 entonces "¿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?

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 desea que el compilador lo proteja del bloqueo al hacer que la asignación sea ilegal en primer lugar?

Elegimos este último.

Este tipo de conversión se denomina "covariante" conversión. En C # 4, le permitiremos realizar conversiones covariantes en interfaces y delegados cuando se sabe que la conversión siempre es 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).

Para citar la gran explicación de Eric

  

¿Qué 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 de tiempo de ejecución en lugar de un error de compilación? Normalmente usaría Cast < > o ConvertAll < > pero entonces tendrás 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 de contenedor (+ 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);
    }
}

En cuanto a por qué no funciona, podría ser útil comprender covarianza y 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 de Tipo "B", pero el tipo de elemento de la Lista Derivada es C.

Ahora, suponga que realmente solo queremos hacer una función genérica que funcione 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>());
}

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

En lugar de -

Lista < A > listOfA = nueva Lista < C > (); // Error del compilador

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

IEnumerable < A > listOfA = nueva Lista < C > (); // error del compilador - ¡no más! :)

Solo puede 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. El motivo es:

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

¿Qué pasa ahora? 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.

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

Porque C # no permite ese tipo de conversión de herencia por el momento .

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

En mi caso, tenía una clase NodeBase con un diccionario Children , 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 , por lo que obviamente quería evitar las costosas copias / iteraciones. Por lo tanto, utilicé el código de Bigjim para transmitir el Dictionary < cualquiera que sea el tipo específico > a un Dictionary < NodeBase > genérico:

// 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, finalmente encontré limitaciones no relacionadas y terminé creando un método abstracto FindChild () 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 IEnumerable para 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 emitir toda la colección, 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.

También puede usar el paquete System.Runtime.CompilerServices.Unsafe NuGet para crear una referencia a la misma 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 instancias Hammer existentes en la lista utilizando la variable tools . Agregar instancias de Tool a la lista genera una excepción ArrayTypeMismatchException porque tools hace referencia a la misma variable que hammers .

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top