W dzisiejszym wpisie chciałbym przyjrzeć się trochę bardziej tablicom i interfejsom jakie implementują. Zaglądając do dokumentacji dowiemy się, że Array implementuje:
[SerializableAttribute] [ComVisibleAttribute(true)] public abstract class Array : ICloneable, IList, ICollection, IEnumerable, IStructuralComparable, IStructuralEquatable
IEnumerable nie powinno wydawać się dziwne ponieważ oczekujemy od tablic możliwości dostępu do elementów poprzez foreach:
int[] numbers = new[] { 5, 2, 52, 5 }; foreach(int number in numbers) { Console.WriteLine(number); }
IList z kolei może być zaskakujące ponieważ kojarzy się ona z modyfikowalną kolekcją danych. W rzeczywistości IList posiada właściwość IsReadOnly określającą czy daną listę można modyfikować. Nic zatem nie stoi na przeszkodzie aby zastosować następujące rzutowanie:
int[] numbers = new[] { 5, 2, 52, 5 }; IList list = numbers; list.Add(3); // exception here
Kod się skompiluje bez problemu ale w czasie runtime zostanie wyrzucony wyjątek(“Collection was of a fixed size”) ponieważ tablica jest zawsze listą tylko do odczytu. Rozważmy metodę o następującej sygnaturze:
private void DisplayElements(IList elements) { }
Z powyższych rozważań wynika, że następujące wywołania są prawidłowe:
int[] numbersArray = new[] { 5, 2, 52, 5 }; List<int> numbersGenericList=new List<int>(); ArrayList nonGenericList=new ArrayList(); DisplayElements(numbersArray); DisplayElements(numbersGenericList); DisplayElements(nonGenericList);
Na pewnym forum, jeden z użytkowników zasugerował, że IList to jedyny interfejs eksponujący dostęp przez indexer tzn.:
int number=numbersArray[5]
Osobiście myślę, że w .NET Framework powinien być dodatkowy interfejs ponieważ fakt, że tablica implementuję listę nie jest dla mnie oczywisty.
To nie koniec niespodzianek…. Okazuje się, że CLR dokonuje pewnych “trików” i w rzeczywistości jest więcej interfejsów. Zróbmy mały eksperyment:
SampleClass[] array=new SampleClass[4]; foreach(Type item in array.GetType().GetInterfaces()) { Console.WriteLine(item.ToString()); }
Kod wyświetlający wszystkie implementowane interfejsy zwróci:
System.ICloneable System.Collections.IList System.Collections.ICollection System.Collections.IEnumerable System.Collections.IStructuralComparable System.Collections.IStructuralEquatable System.Collections.Generic.IList`1[SampleClass] System.Collections.Generic.ICollection`1[SampleClass] System.Collections.Generic.IEnumerable`1[SampleClass]
Niespodzianką są generyczne implementacje. Zgodnie z MSDN każda tablica implementuje wyłącznie niegeneryczne wersje (tzn. IList, ICollection). CLR jednak dynamicznie implementuje te interfejsy jeśli do czynienia ma z tablicą jednowymiarową. Dla tablic wielowymiarowych tablica implementuje wyłącznie niegeneryczne wersje:
SampleClass[,] array=new SampleClass[4,5]; foreach(Type item in array.GetType().GetInterfaces()) { Debug.WriteLine(item.ToString()); } Output: System.ICloneable System.Collections.IList System.Collections.ICollection System.Collections.IEnumerable System.Collections.IStructuralComparable System.Collections.IStructuralEquatable
Na tym etapie wiemy, że tablice jednowymiarowe implementują zarówno generyczne jak i niegeneryczne interfejsy, z kolei wielowymiarowe, wyłącznie niegeneryczne. Przyjrzyjmy się dla odmiany, jak wygląda sprawa z typami prostymi:
int[] array=new int[5]; foreach(Type item in array.GetType().GetInterfaces()) { Debug.WriteLine(item.ToString()); } Output: System.ICloneable System.Collections.IList System.Collections.ICollection System.Collections.IEnumerable System.Collections.IStructuralComparable System.Collections.IStructuralEquatable System.Collections.Generic.IList`1[System.Int32] System.Collections.Generic.ICollection`1[System.Int32] System.Collections.Generic.IEnumerable`1[System.Int32]
Na pierwszy rzut oka wygląda to tak samo ale jednak w przypadku typu referencyjnego możliwe jest rzutowanie do typów bazowych:
// OK SampleClass[] array = new SampleClass[5]; IList<object> array1 = array; IList<SampleBaseClass> array2 = array; // BLAD - tylko tablica dynamicznie implementuje bazowe interfejsy List<SampleClass> list=new List<SampleClass>(); List<object> list1 = list;
W przypadku typów prostych jest to niemożliwe – powyższe rzutowania zawsze zakończą się błędem kompilacji. Z powyższych rozważań wynika, że tablica może zostać przekazana do metod o naprawdę różnorakich sygnaturach. Niestety wynika z tego jeszcze jedna przykra sprawa – użytkownik poprzez przekazanie tablicy do metody, która spodziewa się listy, może spowodować błąd jeśli taka metoda oczekuje modyfikowalnej kolekcji.
Wynika z tego też taki wniosek że przy korzystaniu z IList powinniśmy sprawdzać właściwość readonly gdy próbvujemy coś innego z nia robić.