Indeksatory a kolekcje obiektów typu wartościowego

2012-08-10

W dzisiejszym temacie poruszę pewien problem na jaki można natknąć się podczas używania kolekcji obiektów typu wartościowego. Aby pokazać o co dokładnie chodzi zacznijmy od zdefiniowania przykładowej klasy opisującej prostokąt:

class Rect
{
    public int Height { get; set; }
    public int Width { get; set; }
}

Teraz tworzymy listę obiektów typu Rect zawierającą dwa elementy, po czym modyfikujemy właściwość Height pierwszego z nich:

List<Rect> rectangles = new List<Rect>
{
    new Rect {Height = 2, Width = 3},
    new Rect {Height = 5, Width = 6}
};

rectangles[0].Height = 3;

Powyższy kod jest jak najbardziej poprawny i działa bez problemów. Co się jednak stanie gdy zmienimy naszą klasę Rect na strukturę?:

struct Rect
{
    public int Height { get; set; }
    public int Width { get; set; }
}

Okazuje się, że przy próbie kompilacji otrzymamy błąd w linii rectangles[0].Height = 3:

Cannot modify the return value of ‚System.Collections.Generic.List<Rect>.this[int]’ because it is not a variable

Co ciekawe, jeżeli zamiast listy użyjemy tablicy wszystko będzie w porządku. Poniższy kod nie generuje już błędu niezależnie czy użyjemy klasy czy struktury jako elementów tablicy:

Rect[] rectangles =
{
    new Rect {Height = 2, Width = 3},
    new Rect {Height = 5, Width = 6}
};

rectangles[0].Height = 3;

Aby wyjaśnić przyczynę takiego zachowania stwórzmy następującą klasę:

class MyCollection<T>
{
    public T[] Elements;

    public T this[int index]
    {
        get
        {
            return Elements[index];
        }
        set
        {
            Elements[index] = value;
        }
    }

    public MyCollection(int count)
    {
        Elements = new T[count];
    }
}

Składa się ona z trzech elementów: tablicy, indeksatora oraz konstruktora przyjmującego parametr określający rozmiar tablicy. Indeksator poprzez akcesory get i set zwraca lub ustawia określony element tablicy. Oto przykład zastosowania powyższej klasy:

MyCollection<Rect> rectangles = new MyCollection<Rect>(2);
rectangles.Elements[0] = new Rect { Height = 2, Width = 3 };
rectangles[1] = new Rect { Height = 5, Width = 6 };

rectangles.Elements[0].Height = 3;
rectangles[1].Height = 3;

Utworzony został obiekt klasy MyCollection operujący na typie Rect. Do naszego obiektu dodaliśmy dwa prostokąty, jak widać można to zrobić zarówno poprzez bezpośrednie odwołanie do tablicy (rectangles.Elements[0]) jak i poprzez indeksator (rectangles[1]). W kolejnych dwóch liniach następuje próba modyfikacji właściwości Height obu elementów, pierwszego przy użyciu tablicy, drugiego przy użyciu indeksatora. I teraz najważniejszy test, jeżeli typ Rect będzie klasą, cały kod skompiluje się i będzie działał poprawnie. Jeżeli natomiast typ Rect będzie strukturą, przy próbie kompilacji zobaczymy znajomy błąd (linia rectangles[1].Height = 3):

Cannot modify the return value of ‚MyCollection<Rect>.this[int]’ because it is not a variable

Jak widać w przypadku typów wartościowych (struktura), problemem jest użycie indeksatora do pobrania elementu, który chcemy zmodyfikować. Dotyczy to zarówno naszej klasy, jak i klasy List<T> gdzie do odczytu elementów także wykorzystywane są indeksatory. Dlaczego tak się dzieje? Jeżeli pobieramy element poprzez indeksator (akcesor get), zgodnie z zachowaniem typów wartościowych dostajemy tak naprawdę jego kopię, a więc modyfikacja oryginalnego elementu w ten sposób nie jest możliwa. Inaczej zachowują się tablice, w ich przypadku poprzez indeks jesteśmy w stanie operować bezpośrednio na danym obiekcie. Jeżeli stosujemy obiekty typu referencyjnego (klasa), nie ma znaczenia czy odwołujemy się do nich poprzez indeks tablicy czy indeksator listy. W obu przypadkach otrzymamy po prostu referencję do danego obiektu, poprzez którą nastąpi jego modyfikacja.
Jak więc zmodyfikować wybrany element listy zawierającej obiekty typu wartościowego? Musimy pobrać dany element do zmiennej (wykonywana jest jego kopia), następnie dokonać modyfikacji, po czym przypisać zmodyfikowany obiekt do listy (wykonywana jest kolejna kopia):

List<Rect> rectangles = new List<Rect>
{
    new Rect {Height = 2, Width = 3},
    new Rect {Height = 5, Width = 6}
};

Rect tmp = rectangles[0];
tmp.Height = 3;
rectangles[0] = tmp;
Reklamy

Posted on 2012-08-10, in .NET/C# and tagged , , . Bookmark the permalink. Dodaj komentarz.

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Log Out / Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Log Out / Zmień )

Facebook photo

Komentujesz korzystając z konta Facebook. Log Out / Zmień )

Google+ photo

Komentujesz korzystając z konta Google+. Log Out / Zmień )

Connecting to %s

%d blogerów lubi to: