Converter lista <classe derivada> em lista <classe base>

179

Embora possamos herdar da classe / interface base, por que não podemos declarar o List<> uso da mesma classe / interface?

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

Existe uma maneira de contornar?

Asad
fonte

Respostas:

230

A maneira de fazer isso funcionar é iterar sobre a lista e converter os elementos. Isso pode ser feito usando o ConvertAll:

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

Você também pode usar o Linq:

List<A> listOfA = new List<C>().Cast<A>().ToList();
Mark Byers
fonte
2
outra opção: Listar <A> listOfA = listOfC.ConvertAll (x => (A) x);
Ahaliav fox
6
Qual é o mais rápido? ConvertAllou Cast?
Martin Braun
24
Isso criará uma cópia da lista. Se você adicionar ou remover algo da nova lista, isso não será refletido na lista original. Em segundo lugar, há uma grande penalidade de desempenho e memória, pois ela cria uma nova lista com os objetos existentes. Veja minha resposta abaixo para uma solução sem esses problemas.
Bigjim
Resposta ao modiX: ConvertAll é um método na Lista <T>, portanto, ele funcionará apenas em uma lista genérica; não funcionará em IEnumerable ou IEnumerable <T>; O ConvertAll também pode fazer conversões personalizadas, não apenas a conversão, por exemplo, ConvertAll (polegadas => polegadas * 25.4). A conversão <A> é um método de extensão LINQ, portanto funciona em qualquer IEnumerable <T> (e também funciona em IEnumerable não genérico) e, como na maioria dos LINQ, ele usa execução adiada, ou seja, converte apenas o número de itens que são recuperado. Leia mais sobre isso aqui: codeblog.jonskeet.uk/2011/01/13/…
Edward
172

Antes de tudo, pare de usar nomes de classe impossíveis de entender como A, B, C. Use Animal, Mamífero, Girafa ou Comida, Fruta, Laranja ou algo onde os relacionamentos sejam claros.

Sua pergunta então é "por que não posso atribuir uma lista de girafas a uma variável do tipo de lista de animais, pois posso atribuir uma girafa a uma variável do tipo animal?"

A resposta é: suponha que você possa. O que poderia então dar errado?

Bem, você pode adicionar um tigre a uma lista de animais. Suponha que permitamos que você coloque uma lista de girafas em uma variável que contém uma lista de animais. Então você tenta adicionar um tigre a essa lista. O que acontece? Deseja que a lista de girafas contenha um tigre? Você quer um acidente? ou você deseja que o compilador o proteja da falha, tornando a atribuição ilegal em primeiro lugar?

Nós escolhemos o último.

Esse tipo de conversão é chamado de conversão "covariante". No C # 4, permitiremos que você faça conversões covariantes em interfaces e delegados quando se sabe que a conversão é sempre segura . Veja meus artigos de blog sobre covariância e contravariância para obter detalhes. (Haverá um novo sobre este tópico na segunda e na quinta-feira desta semana.)

Eric Lippert
fonte
3
Haveria algo inseguro sobre um IList <T> ou ICollection <T> implementando IList não genérico, mas implementando ter o Ilist / ICollection não genérico retorne True for IsReadOnly e ative NotSupportedException para quaisquer métodos ou propriedades que o modifiquem?
Super28
33
Embora essa resposta contenha um raciocínio bastante aceitável, ela não é realmente "verdadeira". A resposta simples é que o C # não suporta isso. A idéia de ter uma lista de animais que contenha girafas e tigres é perfeitamente válida. O único problema ocorre quando você deseja acessar as classes de nível superior. Na realidade, isso não é diferente de passar uma classe pai como parâmetro para uma função e tentar convertê-la em uma classe irmão diferente. Pode haver problemas técnicos na implementação do elenco, mas a explicação acima não fornece nenhuma razão para que isso seja uma má ideia.
Paul Coldrey
2
@EricLippert Por que podemos fazer essa conversão usando um em IEnumerablevez de um List? ou seja: List<Animal> listAnimals = listGiraffes as List<Animal>;não é possível, mas IEnumerable<Animal> eAnimals = listGiraffes as IEnumerable<Animal>funciona.
LINQ
3
@jbueno: Leia o último parágrafo da minha resposta. Sabe-se que a conversão é segura . Por quê? Porque é impossível transformar uma sequência de girafas em uma sequência de animais e depois colocar um tigre na sequência de animais . IEnumerable<T>e IEnumerator<T>são marcados como seguros para covariância, e o compilador verificou isso.
Eric Lippert
60

Para citar a grande explicação de Eric

O que acontece? Deseja que a lista de girafas contenha um tigre? Você quer um acidente? ou você deseja que o compilador o proteja da falha, tornando a atribuição ilegal em primeiro lugar? Nós escolhemos o último.

Mas e se você quiser escolher uma falha de tempo de execução em vez de um erro de compilação? Você normalmente usaria o Cast <> ou o ConvertAll <>, mas terá 2 problemas: Ele criará uma cópia da lista. Se você adicionar ou remover algo da nova lista, isso não será refletido na lista original. Em segundo lugar, há uma grande penalidade de desempenho e memória, pois ela cria uma nova lista com os objetos existentes.

Eu tive o mesmo problema e, portanto, criei uma classe de wrapper que pode converter uma lista genérica sem criar uma lista totalmente nova.

Na pergunta original, você poderia usar:

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

e aqui a classe wrapper (+ um método de extensão CastList para facilitar o uso)

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
fonte
1
Acabei de usá-lo como modelo de exibição MVC e obtive uma boa visão parcial do barbeador universal. Idéia incrível! Estou tão feliz que li isso.
Stefan Cebulak
5
Eu adicionaria apenas um "where TTo: TFrom" na declaração de classe, para que o compilador possa avisar sobre o uso incorreto. Criar um CastedList de tipos não relacionados é absurdo, e criar um "CastedList <TBase, TDerived>" seria inútil: você não pode adicionar objetos TBase regulares a ele, e qualquer TDerived obtido da lista original já pode ser usado como um TBase.
Wolfzoon 02/11/19
2
@PaulColdrey Infelizmente, é o começo de seis anos.
Wolfzoon 02/11/19
@ Wolfzoon: o padrão .Cast <T> () também não tem essa restrição. Queria tornar o comportamento igual ao .Cast, para que seja possível converter uma lista de Animais em uma lista de Tigres e tenha uma exceção quando ela contenha uma Girafa, por exemplo. Assim como seria com Elenco ...
Bigjim
Oi @Bigjim, estou tendo problemas para entender como usar a classe wrapper, para converter uma matriz existente de girafas em uma matriz de animais (classe base). Todas as dicas serão appriciated :)
Denis Vitez
28

Se você usar IEnumerable, ele funcionará (pelo menos no C # 4.0, eu não tentei versões anteriores). Este é apenas um elenco, é claro, ainda será uma lista.

Ao invés de -

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

No código original da pergunta, use -

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

PhistucK
fonte
Como usar o IEnumerable nesse caso exatamente?
Vladius
1
Em vez de List<A> listOfA = new List<C>(); // compiler Errorno código original da pergunta, digiteIEnumerable<A> listOfA = new List<C>(); // compiler error - no more! :)
PhistucK 14/15/15
O método que usa o IEnumerable <BaseClass> como parâmetro permitirá que a classe herdada seja passada como uma Lista. Portanto, há muito pouco a fazer além de alterar o tipo de parâmetro.
#
27

Na medida em que não funciona, pode ser útil entender covariância e contravariância .

Apenas para mostrar por que isso não deve funcionar, aqui está uma alteração no código que você forneceu:

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

     C FirstItem = DerivedList.First();
}

Isso deve funcionar? O primeiro item da lista é do tipo "B", mas o tipo do item DerivedList é C.

Agora, suponha que realmente queremos apenas criar uma função genérica que opere em uma lista de algum tipo que implemente A, mas não nos importamos com o tipo:

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

}

void Test()
{
     ThisWorks(new List<B>());
     ThisWorks(new List<C>());
}
Chris Pitman
fonte
"Isso deve funcionar?" - Eu sim e não. Não vejo razão para que você não consiga escrever o código e que ele falhe no momento da compilação, pois na verdade está fazendo uma conversão inválida no momento em que você acessa o FirstItem como tipo 'C'. Existem muitas maneiras análogas de expandir o C # que são suportadas. Se você realmente deseja obter essa funcionalidade por um bom motivo (e há muitos), a resposta do bigjim abaixo é impressionante.
Paul Coldrey
16

Você só pode transmitir para listas somente leitura. Por exemplo:

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

E você não pode fazer isso para listas que suportam salvar elementos. O motivo é:

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

E agora? Lembre-se de que listObject e listString são a mesma lista, portanto, listString agora tem um elemento de objeto - não deve ser possível e não é.

Wojciech Mikołajewicz
fonte
1
Essa foi a resposta mais compreensível para mim. Especialmente porque menciona que existe algo chamado IReadOnlyList, que funcionará porque garante que nenhum elemento adicional será adicionado.
bendtherules
É uma pena que você não possa fazer algo semelhante ao IReadOnlyDictionary <TKey, TValue>. Por que é que?
Alastair Maw
Esta é uma ótima sugestão. Uso List <> em todos os lugares por conveniência, mas na maioria das vezes eu quero que seja somente leitura. Boa sugestão, pois isso melhorará meu código de duas maneiras, permitindo essa conversão e melhorando onde eu especifico somente leitura.
Rick Love
1

Pessoalmente, gosto de criar bibliotecas com extensões para as classes

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

Como o C # não permite esse tipo de herançaconversão no momento .

Seda do meio-dia
fonte
6
Primeiro, isso é uma questão de conversibilidade, não de herança. Segundo, a covariância de tipos genéricos não funcionará nos tipos de classe, apenas nos tipos de interface e delegado.
Eric Lippert
5
Bem, mal posso discutir com você.
Noon Silk
0

Esta é uma extensão da brilhante resposta de BigJim .

No meu caso, eu tive uma NodeBaseaula com um Childrendicionário e precisava de uma maneira generosa de fazer pesquisas O (1) das crianças. Eu estava tentando retornar um campo de dicionário privado no getter de Children, então obviamente eu queria evitar cópias / iterações caras. Portanto, usei o código de Bigjim para converter o Dictionary<whatever specific type>em um 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>();
    }
    ...
}

Isso funcionou bem. No entanto, acabei encontrando limitações não relacionadas e acabei criando um FindChild()método abstrato na classe base que faria as pesquisas. Como se viu, isso eliminou a necessidade do dicionário fundido em primeiro lugar. (Consegui substituí-lo por um simples IEnumerablepara meus propósitos.)

Portanto, a pergunta que você pode fazer (especialmente se o desempenho é um problema que o impede de usar .Cast<>ou .ConvertAll<>) é:

"Preciso realmente converter a coleção inteira ou posso usar um método abstrato para manter o conhecimento especial necessário para executar a tarefa e, assim, evitar o acesso direto à coleção?"

Às vezes, a solução mais simples é a melhor.

Zach
fonte
0

Você também pode usar o System.Runtime.CompilerServices.Unsafepacote NuGet para criar uma referência ao mesmo List:

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

Dada a amostra acima, você pode acessar as Hammerinstâncias existentes na lista usando a toolsvariável Adicionar Toolinstâncias à lista gera uma ArrayTypeMismatchExceptionexceção porque faz toolsreferência à mesma variável que hammers.

Desenhou
fonte
0

Eu li todo esse tópico e só quero apontar o que me parece uma inconsistência.

O compilador impede que você faça a atribuição com Listas:

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

Mas o compilador está perfeitamente bem com matrizes:

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

O argumento sobre se a tarefa é conhecida como segura se desfaz aqui. A tarefa que fiz com a matriz não é segura . Para provar isso, se eu seguir com isso:

myAnimalsArray[1] = new Giraffe();

Recebo uma exceção de tempo de execução "ArrayTypeMismatchException". Como se explica isso? Se o compilador realmente quer me impedir de fazer algo estúpido, deveria ter me impedido de fazer a atribuição da matriz.

Rajeev Goel
fonte