Tablice – rzutowanie

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.

One thought on “Tablice – rzutowanie”

  1. 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ć.

Leave a Reply

Your email address will not be published.