Skrócona składnia do rzutowania z listy <X> na listę <Y>?

237

Wiem, że możliwe jest rzutowanie listy elementów z jednego typu na inny (biorąc pod uwagę, że twój obiekt ma metodę publicznego jawnego operatora jawnego, aby wykonać rzutowanie) w następujący sposób:

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

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

Ale czy nie można rzucić całej listy jednocześnie? Na przykład,

ListOfY = (List<Y>)ListOfX;
Jimbo
źródło
@Oded: Próbowałem to trochę wyjaśnić. Nie martw się, nie jesteś, rozumiem :)
BoltClock
1
Zakładając, że X pochodzi od Y, a Z pochodzi od Y, zastanów się, co by się stało, gdybyś dodał Z do swojej Listy <Y>, która tak naprawdę jest Listą <X>.
Richard Friend,

Odpowiedzi:

497

Jeśli Xnaprawdę można go rzucić Y, powinieneś być w stanie użyć

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

Kilka rzeczy, o których należy pamiętać (H / T dla komentujących!)

Jamiec
źródło
12
Zdobądź kolejną złotą odznakę. To było całkiem przydatne.
ouflak
6
Musi zawierać następujący wiersz, aby kompilator rozpoznał te metody rozszerzenia: using System.Linq;
hypehuman
8
Pamiętaj też, że chociaż rzutuje to każdy element na liście, sama lista nie jest rzutowana; raczej tworzona jest nowa lista z pożądanym typem.
hypehuman
4
Należy również pamiętać, że Cast<T>metoda nie obsługuje niestandardowych operatorów konwersji. Dlaczego pomocnik Linq Cast nie działa z niejawnym operatorem rzutowania .
clD
Nie działa dla obiektu, który ma jawną metodę operatora (framework 4.0)
Adrian
100

Bezpośrednie rzucanie var ListOfY = (List<Y>)ListOfXnie jest możliwe, ponieważ wymagałoby ko / kontrawariancji tego List<T>typu, a po prostu nie można tego zagwarantować w każdym przypadku. Przeczytaj dalej, aby zobaczyć rozwiązania tego problemu z rzutowaniem.

Chociaż normalne wydaje się być pisanie takiego kodu:

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

ponieważ możemy zagwarantować, że każdy ssak będzie zwierzęciem, jest to oczywiście błąd:

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

ponieważ nie każde zwierzę jest ssakiem.

Jednak używając C # 3 i nowszych, możesz użyć

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

co nieco ułatwia casting. Jest to syntaktycznie równoważne z dodawanym kodem jeden po drugim, ponieważ używa jawnego rzutowania do rzutowania każdego Mammalz listy na Animali nie powiedzie się, jeśli rzutowanie nie powiedzie się.

Jeśli chcesz mieć większą kontrolę nad procesem rzutowania / konwersji, możesz użyć ConvertAllmetody List<T>klasy, która może użyć dostarczonego wyrażenia do konwersji elementów. Ma dodaną korzyść, którą zwraca Listzamiast IEnumerable, więc nie .ToList()jest to konieczne.

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
źródło
2
Nie mogę uwierzyć, że nigdy nie dawałem +1 tej odpowiedzi. Jest o wiele lepszy niż mój powyżej.
Jamiec
6
@Jamiec Nie dałem +1, ponieważ zaczyna od „Nie, to niemożliwe”, jednocześnie grzebiąc odpowiedź, której szuka wielu, którzy znajdą to pytanie. Technicznie odpowiedział jednak dokładniej na pytanie PO.
Dan Bechard,
13

Aby dodać do punktu Sweko:

Powód, dla którego obsada

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

nie jest możliwe, ponieważ List<T>jest niezmienny w typie T, a zatem nie ma znaczenia, czy Xpochodzi od Y) - dzieje się tak, ponieważ List<T>jest zdefiniowany jako:

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

(Uwaga: w tej deklaracji wpisz Ttutaj nie ma żadnych dodatkowych modyfikatorów wariancji)

Jeśli jednak zmienne kolekcje nie są wymagane w twoim projekcie, możliwy jest upcast do wielu niezmiennych kolekcji , np. Pod warunkiem, że Giraffepochodzi z Animal:

IEnumerable<Animal> animals = giraffes;

Jest tak, ponieważ IEnumerable<T>obsługuje kowariancję w T- ma to sens, biorąc pod uwagę, że IEnumerableoznacza to, że kolekcji nie można zmienić, ponieważ nie obsługuje ona metod dodawania lub usuwania elementów z kolekcji. Zwróć uwagę na outsłowo kluczowe w deklaracji IEnumerable<T>:

public interface IEnumerable<out T> : IEnumerable

( Oto dalsze wyjaśnienie powodu, dla którego zmienne kolekcje, takie jak, Listnie mogą obsługiwać covariance, podczas gdy niezmienne iteratory i kolekcje mogą.)

Przesyłam z .Cast<T>()

Jak wspomnieli inni, .Cast<T>()można zastosować do kolekcji, aby rzutować nową kolekcję elementów InvalidCastExceptionrzutowanych na T, jednak spowoduje to rzutowanie, jeśli rzutowanie na jeden lub więcej elementów nie jest możliwe (co byłoby tym samym zachowaniem, co wykonywanie jawnych obsadzone w foreachpętli OP ).

Filtrowanie i przesyłanie za pomocą OfType<T>()

Jeśli lista wejściowa zawiera elementy różnych, niekompatybilnych typów, potencjału InvalidCastExceptionmożna uniknąć, używając .OfType<T>()zamiast .Cast<T>(). ( .OfType<>()sprawdza, czy element można przekonwertować na typ docelowy, przed próbą konwersji i odfiltrowuje typy niezgodne).

dla każdego

Należy również pamiętać, że jeśli PO zamiast tego napisał: (należy zwrócić uwagę na wyraźneY y w foreach)

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

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

że próba odlewania również zostanie podjęta. Jeśli jednak rzut nie jest możliwy, InvalidCastExceptionnastąpi wynik.

Przykłady

Na przykład, biorąc pod uwagę prostą hierarchię klas (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) { }
}

Podczas pracy z kolekcją typów mieszanych:

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

Natomiast:

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

odfiltrowuje tylko słonie - tzn. zebry są eliminowane.

Re: Domniemani operatorzy rzutowania

Bez dynamiki operatory konwersji zdefiniowane przez użytkownika są używane tylko w czasie kompilacji *, więc nawet jeśli operator konwersji między powiedzmy, że Zebra i Elephant został udostępniony, powyższe zachowanie podejścia do konwersji nie zmieni się.

Jeśli dodamy operator konwersji, aby przekonwertować Zebrę na słonia:

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

Zamiast tego, biorąc pod uwagę powyższy operator konwersji, kompilator będzie mógł zmienić typ poniższej tablicy z Animal[]na Elephant[], biorąc pod uwagę, że Zebry można teraz przekształcić w jednorodną kolekcję słoni:

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

Korzystanie z niejawnych operatorów konwersji w czasie wykonywania

* Jak wspomniał Eric, do operatora konwersji można jednak uzyskać dostęp w czasie wykonywania, korzystając z 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
źródło
Hej, właśnie wypróbowałem przykład „Korzystanie z foreach () do filtrowania typów” przy użyciu: var list = new List <object> () {1, „a”, 2, „b”, 3, „c”, 4, „ d "}; foreach (int i in list) Console.WriteLine (i); a kiedy go uruchamiam, pojawia się komunikat „Określona obsada jest nieprawidłowa”. Czy coś brakuje? Nie sądziłem, że Foreach działa w ten sposób i dlatego próbowałem.
Brent Rittenhouse
Poza tym nie jest to rzecz referencyjna vs. typ wartości. Właśnie wypróbowałem to z podstawową klasą „Rzecz” i dwiema pochodnymi klasami: „Osoba” i „Zwierzę”. Kiedy robię to samo, otrzymuję: „Nie można rzucić obiektu typu„ Zwierzę ”, aby wpisać„ Osoba ”. Więc zdecydowanie iteruje przez każdy element. JEŚLI miałbym zrobić OfType na liście, to zadziałałoby. ForEach prawdopodobnie byłby bardzo wolny, gdyby musiał to sprawdzić, chyba że kompilator to zoptymalizuje.
Brent Rittenhouse
Dzięki Brent - zjechałem z kursu. foreachnie filtruje, ale użycie bardziej pochodnego typu jako zmiennej iteracyjnej zmusi kompilator do próby rzutowania, co zakończy się niepowodzeniem w przypadku pierwszego elementu, który nie jest zgodny.
StuartLC,
7

Możesz użyć List<Y>.ConvertAll<T>([Converter from Y to T]);

Andrey
źródło
3

Nie jest to do końca odpowiedź na to pytanie, ale może być przydatna dla niektórych: jak powiedział @SWeko, dzięki kowariancji i kontrawariancji List<X>nie można w nią wrzucić List<Y>, ale List<X>można ją wrzucić IEnumerable<Y>, a nawet z ukrytą obsadą.

Przykład:

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

ale

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

Dużą zaletą jest to, że nie tworzy nowej listy w pamięci.

Xav987
źródło
1
Lubię to, ponieważ jeśli masz dużą listę źródeł, na początku nie ma wydajności. Zamiast tego jest mała niezauważalna obsada dla każdego wpisu przetwarzanego przez odbiorcę. Również nie gromadzi się ogromna pamięć. idealny do przetwarzania strumieni.
Johan Franzén,
-2
dynamic data = List<x> val;  
List<y> val2 = ((IEnumerable)data).Cast<y>().ToList();
użytkownik2361134
źródło