DISCLAIMER:
Artykuł jest pisany na poziomie ameby tudzież ucznia podstawówki. Jeżeli masz choć blade pojęcie na temat .net czy c# zrezygnuj z czytania tego artykułu i zajmij się czymś ciekawszym.
Pewnego razu trafiło mi się na facebookach takie pytanie „dlaczego z listy konwertuje na IEnumerable a odwrotnie nie” -cytat jest skopiowany żywcem, autor wolałby pozostać anonimowy. Jako że nie ma głupich pytań i każdy kiedyś zaczynał, odpowiem koledze i innym ciekawym.
Nie za bardzo wiadomo, o co chodzi z tym konwertowaniem, pewnie kolega miał na myśli rzutowanie (dla osób lubiących anglicyzmy: castowanie). W przypadku rzutowania nasze dane zaczynają być interpretowane w nowy sposób. Rzutowanie jest bezpieczne, jeżeli rzutujemy z klasy pochodnej na bazową, tudzież z klasy na interfejs jaki ona implementuje, zaś w przypadku value type z typu w którym możemy mniej zapisać, do takiego, w którym możemy więcej. Należy zauważyć, że po takim zrzutowaniu nie będziemy mieli dostępu do pól, których ten nowy typ nie ma. No, chyba że zaczniemy wchodzić w refleksję i strzelać na ślepo czy typ pole posiada czy nie;)
A więc weźmy sobie opisaną sytuację: mamy listę i chcemy sobie ją przypisać (co ją oczywiście niejawnie zrzutuje) do typu IEnumerable (bo z kolekcji niegenerycznych już się używa tylko tam, gdzie trzeba zostać we wstecznej kompatybilności z prehistorią).
Czym tak naprawdę jest ten IEnumerable.
Po pierwsze jest to interfejs (a więc kontrakt między klasami, które umawiają się, że dostarczą coś na pewno). Także nie da się zrobić new IEnumerable(). Jedyne co ma ten interfejs, to funkcję GetEnumerator, która ma zwrócić IEnumerator, który to jedyne co ma, to informacja o aktualnym obiekcie (current), funkcję MoveNext i funkcję Reset. I to nie jest wiedza tajemna. To tylko F12 na typie i odczytanie metadanych:)
A teraz po ludzku. Z czym się kojarzy nazwa IEnumerable? No z Enumami. A enumy się tłumaczy jako typy wyliczeniowe, więc cały powyższy akapit możemy omówić tak. Jako że typy wyliczeniowe, to oznacza, że musimy umieć po danym typie wyliczyć. Po liście się da? Da. Po tablicy się da? No da, bo wszystko co ma indekser da się tak naprawdę wyliczyć bez większego wysiłku. Jak w wyliczance. Jak dziecko pokaże na jedno „ene”, na drugie „due” to teraz musi wiedzieć, że skoro to na które wskazało że due (current) to teraz musi wskazać na następne „rike” (i do tego użyje move next). Wie, na które ma pokazać (w tą samą stronę idzie). No i musi (z tym musi to różnie bywa) umieć wyliczyć od początku.
Lista ten „kontrakt” podpisała. Skąd o tym wiemy? Znowu F12 i jest:
public class List: IList , ICollection , IList, ICollection, IReadOnlyList , IReadOnlyCollection , IEnumerable , IEnumerable
A skoro tak, możemy bezpiecznie zrobić taki myk.
static void Main(string[] args) { ListlistObject = new List (); listObject.Add(5); listObject.Add(3); IEnumerable enumerateObject = listObject; foreach (var item in enumerateObject) { Console.WriteLine(item); } Console.ReadKey(); }
Dlaczego? Bo do użycia funkcji foreach jedyne czego potrzebujemy, to właśnie wiedzieć który element jest teraz i który ma być następny. Gdzie skończyć? To też wiemy, bo funkcja moveNext zwraca true, jeśli mamy następny element (moveNext powinien do current przypisać nowy element i zwrócić true, lub zwrócić false, jeśli elementy się skończyły). A co jeśli ktoś dopisze później? Funkcja ma wrócić i dokończyć foreacha z nowymi elementami? A no jak się kolekcja zmieniła (nie ważne jak), to dostajemy System.InvalidOperationException i na tym zabawa się kończy.
I tu mamy jedyną przewagę wolniejszego foreacha nad nieco szybszym forem. For wymaga jednak odwołania się po indeksie, tutaj możemy nie wiedzieć ile elementów ma nasza kolekcja, może ich mieć nawet nieskończenie wiele (możemy na przykład w środku dodać jakąś zmienną co się będzie inkrementować i przerwać foreacha jak przekroczy zadaną wartość, by nie pobierać w nieskończoność). Może ich też nie mieć wcale (wtedy wejdzie do foreacha tylko po to by z niego wyjść).
Także ten interfejs świetnie się nadaje do iterowania po obiektach, będących kolekcjami innych obiektów.
Czyli robi to co lista? No niekoniecznie. Tu trza elementy po kolei oglądać (kolejność zależy właśnie od moveNexta). W powyższej funkcji zrobimy
Console.WriteLine(listObject[1]);
Ale kompilator zaprotestuje, jeżeli zobaczy
Console.WriteLine(enumerateObject[1]);
Już VS nam to podkreśli z komunikatem podobnym do tego:
"CS0021 C# Cannot apply indexing with [] to an expression of type".
Mimo, że sam obiekt indekser posiada, nie dostaniemy się do niego tak łatwo. Oczywiście, z poziomu kodu można i zrzutować w 2 stronę i się dobrać do tego elementu. Wyświetlmy ów pierwszy element na ekran:
Console.WriteLine(((List < int > )(enumerateObject))[1]);
W tym kawałku kodu jednak dobieramy się do elementu z indeksem 1 listy, a nie obiektu typu IEnumerable.
A teraz wyobraź sobie, że ktoś do tej zmiennej przypisze inny typ, który również podpisał umowę IEnumerable
Dlaczego jednak niejawna konwersja nie jest możliwa?
Ponieważ nigdy nie wiemy obiekt jakiego typu kryje się pod zmienną typu interfejsowego. Oczywiście możemy sprawdzić to używając składni
enumerateObject.GetType();
i dobrać się do danych przy użyciu refleksji, jednak jeśli mamy większy projekt i spróbujemy gdzieś użyć przypadkiem, możemy natrafić na InvalidCastException. Aby się przed tym uchronić, programiści rzutują ręcznie dopiero po sprawdzeniu typu, korzystając z takiej konstrukcji
if (enumerateObject is List) { secondList = enumerateObject as List ; }
Podsumowując…
Nawiązując do pytania z góry. Zamiana listy na IEnumerable zawsze ma sens. Dlaczego? Ponieważ List zawsze można potraktować jako „dający się wyliczyć”, tudzież umiejący w wyliczanki. Dlaczego w drugą stronę nie bardzo? Ponieważ lista ma to co IEnumerable i jeszcze więcej. Tym jeszcze więcej jest chociażby możliwość dobrania się do dowolnego elementu po jego indeksie, wyszukiwanie elementu na parę sposobów, dodawanie czy usuwanie elementów (IEnumerable pozwala tylko taki element pobrać, jeden po drugim). W porównaniu do listy IEnumerable jest… głupi.
Acz czasami właśnie ogłupienie ma swój cel. Jeżeli w funkcji nie chcemy robić nic poza przejściem po kolekcji i jej obejrzeniem, warto właśnie wrzucić ją jako ów enumerator- będziemy pewni, że bez rzutowania nie uda nam się na przykład dodać elementów z innej listy, czy ją przypadkowo wyczyścić (- przecież nie ma ani add czy addRange, ani remove ani clear).
Jak ktoś chce sobie poeksperymentować, to załączam jeszcze pliczek z kodem do zabawy. Można go pobrać stąd. Myślę, że całość pozwoli nowym zrozumieć problem.
Generalnie jak macie jakiś ciekawy problem do rozkmin, to podeślijcie, chętnie odpłynę w tym kierunku;).
6 komentarzy
Pomocne 🙂
List a IEnumerable – Zagubiona wśród własnych myśli – Piatkosia’s blog
Dziękujemy za dodanie artykułu – Trackback z dotnetomaniak.pl
IEnumerable nie jest potrzebne do używania foreach na kolekcji 🙂
Nie jest potrzebne, ale może być użyte.
Generalnie foreacha można rozwinąć do
Kolekcje zwykle to IEnumerable już mają w którejś z klas bazowych.
Masz rację w jednym – nie zawsze ten enumerator być musi. A przykładem
tego są tablice;) Swoją drogą nie wiem czy coś jeszcze. Na pewno jak coś ma indekser to też można go tak potraktować.
Tam bowiem takim enumeratorem jest adres w pamięci (wystarczy wyliczyć
przesunięcie).
Myślę, że ten artykuł może ci się spodobać:
http://www.pzielinski.com/?p=2131
Foreach bez IEnumerable?? Niby jak? btw tablice implementuja IEnumerable..
Oo racja;)
public abstract class Array : ICloneable, IList, ICollection,
IEnumerable, IStructuralComparable, IStructuralEquatable
A ja myślałam, że jak jest opcode na newarr to są proste obiekty jeden po drugim:p
Człowiek się całe życie uczy.
Comments are closed.