Załóżmy dla przykładu taki o to kod:

public class Point
{
    public int X;
    public int Y;
}

class Program
{
    public Point Position { get; set; }

    public void SetPosition(int x, int y)
    {
        Position.X = x;
        Position.Y = y;
    }

    static void Main(string[] args)
    {
        var p = new Program();

        p.Position = new Point();

        p.SetPosition(10, 10);

        Console.ReadLine();
    }
}

AKTUALIZACJA kodu zgodnie z komentarzem Jacka

Przy kompilacji nie zostanie zwrócony żaden błąd, a program wykona się poprawnie.

Teraz, zamiast klasy Point wykorzystajmy strukturę (podmieniając jedno słowo kluczowe):

public struct Point
{
    public int X;
    public int Y;
}

Kompilacja zwróci nam błąd:

Cannot modify the return value of ‘Program.Position’ because it is not a variable

Dla przykładu, ten kod zadziała:

class Program
{
    public int PositionX { get; set; }
    public int PositionY { get; set; }

    public void SetPosition(int x, int y)
    {
        PositionX = x;
        PositionY = y;
    }

    static void Main(string[] args)
    {
        var p = new Program();
        
        p.SetPosition(10, 10);

        Console.ReadLine();
    }
}

Na pierwszy rzut oka zachowanie z błędem może wydawać się dziwnym i niepoprawnym zachowaniem. Jednakże, jeżeli przyjrzymy się temu bliżej zobaczymy, że ma to ręce i nogi.

Jest kilka rzeczy, które trzeba zrozumieć. Pierwszą i najważniejszą jest różnica pomiędzy typami referencyjnymi a value type. Typ referencyjny przechowuje referencje do obiektu i jego danych na kopcu (ang. heap). Typ value type zaś przechowuje dane na stosie. Nie chcę tutaj wchodzić w szczegóły jak to dokładnie jest zrobione i na jakiej zasadzie to wszystko działa – temat był już nieraz wałkowany, google prawdę wam powie – ważne by zrozumieć tą podstawową rożnicę: value types trafiają na stos zaś reference types na kopiec.

Wraz z nią idzie pewna zasada, przypisanie wartości value type tworzy jej kopie, zaś przypisanie reference type przypisuje referencje do danego obiektu. Czyli mając taki kod:

public class PointX
{
    public int X;
}

class Program
{
    static void Main(string[] args)
    {
        int x = 5;
        PointX obj = new PointX() {X = x};

        AddFive(x);
        AddFive(obj);

        Console.WriteLine("x: {0}", x);
        Console.WriteLine("obj.X: {0}", obj.X);

        Console.ReadLine();
    }

    static void AddFive(int val)
    {
        val += 5;
    }

    static void AddFive(PointX val)
    {
        val.X += 5;
    }
}

Wartość x będzie 5 zaś wartość obj.X 10 – mając R#, będziemy mieli podpowiedź, iż przy metodzie AddFive(int val):

Value assigned is not used in any execution path

Oczywiście od tego są wyjątki – chociażby słowo kluczowe ref.

No dobrze, wiedząc na czym polega różnica i na jakiej zasadzie to działa, pora zrozumieć jak działają własności.

Każda własność, którą utworzymy tworzy zdefiniowane accessors – get i/lub set. Accessors to nic innego jak metoda PropertyType get_PropertyName i set_PropertyName(PropertyType value), specyfikacja tak to określa:

A get accessor corresponds to a parameterless method with a return value of the property type. Except as the target of an assignment, when a property is referenced in an expression, the get accessor of the property is invoked to compute the value of the property.

A set accessor corresponds to a method with a single parameter named value and no return type. When a property is referenced as the target of an assignment or as the operand of ++ or –, the set accessor is invoked with an argument that provides the new value.

Jeżeli nie wierzycie, że tak jest to w Reflector rozwińcie plusik przy property (zmieniłem nazwy by móc to łatwo rozróżnić, Class to public class Point, zaś Struct to public struct Point):

clip_image002

Lub otwórzcie assembly w ILDASM:

clip_image004

Teraz mamy już podstawy by wyjaśnić, na czym polega problem. Jeżeli korzystamy z klasy, metoda get_Class zwróci nam referencje do obiektu, przez co cokolwiek zrobimy z własnościami tej klasy zmodyfikuje ją na stałe.

Rzecz ma się inaczej przy value type, mianowicie get_Struct zwraca nam niepowiązaną kopię naszej struktury. Kopia ta zawiera skopiowane wartości własności struktury, przez co Struct zwraca nam lokalnie kopię, która trafia na stos – wyjątkiem są oczywiście własności struktury, które są typem referencyjnym:

public struct Point
{
    public int X;
    public int Y;
    public Size GridSize;
}

public class Size
{
    public int Width;
    public int Height;
}

class Program
{
    public Point Position { get; set; }

    public void SetPosition(int x, int y)
    {
        Position = new Point(){ GridSize = new Size(){Width = 10, Height = 10}, X = x, Y = y};
        
        Console.WriteLine("Before update, grid width was: " + Position.GridSize.Width);
        
        Point tmpPoint = Position;
        tmpPoint.GridSize.Width = 20;
        
        Console.WriteLine("After update, grid width is: " + Position.GridSize.Width);
    }

    static void Main(string[] args)
    {
        var p = new Program();
        
        p.SetPosition(10, 10);

        Console.ReadLine();
    }
}

Powyższy kod wyświetli:

Before update, grid width was: 10

After update, grid width is: 20

Oczywiście w tym wypadku kod:

Position.GridSize.Width = 10;

Zadziała i kompilator nie zwróci nam błędu.

Ale wracając do tematu, wykonanie kodu Struct.X = 10; nie ma większego sensu.

  1. Najpierw tworzymy kopię Struct;
  2. Następnie dla własności X tej kopi przypisujemy wartość 10;
  3. Po tej akcji zapominamy o niej.

Wynik daje się łatwo przewidzieć – wartość skopiowana na stos zostanie usunięta zaraz jak tylko przestanie nam być potrzebna, czyli w następnej linijce kodu, przy czym kod nie wykona żadnej widocznej modyfikacji.

Dlatego też kompilator MS wyłapuje takie rzeczy i zabrania nam wykonania kompletnie bezmyślnej rzeczy.

Krótkie info na koniec. Nie zagłębiłem się tutaj dokładnie, co się dzieje, jak to jest wykonywane itp., zrobiłem kilka uogólnień itp. jednakże moim celem nie było przedstawienie krok po kroku, co i jak jest wykonywane zarówno na stosie, kopcu jak i w IL. Jeżeli chcecie poznać „graficznie” jak wyglądają operacje na stosie i kopcu to sądzę, że ta seria artykułów pomoże wam to zrozumieć (przynajmniej graficznie jest to fajnie pokazane, osobiście nie czytałem ich).

6 KOMENTARZE

  1. Może się czepiam:-), ale nie zgadzam się ze stwierdzeniem

    "Typ referencyjny przechowuje referencje do obiektu i jego danych na kopcu (ang. heap). Typ value type zaś przechowuje dane na stosie."

    Instancja view type przechowywana jest tam, gdzie obiekt ją zawierający, lub na stosie, jeśli jest to instancja referowana przez zmienną lokalną. Ale, jak słusznie napisał Eric Lippert (http://blogs.msdn.com/ericlippert/archive/2009/04/27/the-stack-is-an-implementation-detail.aspx), to i tak nie ważne, bo liczy się tylko to, że value type jest kopiowany jako wartość, a nie referencja.

  2. zgadza sie, zastosowalem ogolno przyjety "skrot myslowy", ktory Erica doprowadza do szalu :) przepraszam za to, jednakze dalem na koncu linki ktore prezentuja to co Eric i Ty piszecie :)

    Gutek

  3. Są to powody, dla których użycie struktur zamiast klas należy rozważać tylko w przypadku, gdy spełnione są wszystkie poniższe warunki:

    1) **Instancja typu zajmuje niewiele miejsca w pamięci.**

    2) **Instancja typu reprezentuje _wartość_.** Za wartość można uznać byt nie posiadający tożsamości. Przykładowo _Kwota_ jest dobrym kandydatem na strukturę, gdyż jeśli zastąpimy jedną kwotę inną kwotą o tej samej wysokości i w tej samej walucie, nie zrobi nam to żadnej różnicy. Z drugiej strony _Banknot_ nie jest dobrym kandydatem na strukturę, gdyż jeśli zastąpimy jeden banknot innym banknotem o tym samym nominale, nie będziemy mogli uznać, że dysponujemy tym samym banknotem, co wcześniej.

    3) **Instancja typu jest _niemutowalna_.** Po utworzeniu nie można zmienić jej stanu, jeśli jest nam potrzebna instancja reprezentująca inną wartość, po prostu tworzymy nową.

    Oczywiście czasem istnieją dobre powody, by użyć struktur w innych okolicznościach. Rzadko, ale mimo wszystko, można pokusić się o użycie struktur ze względu na wydajność. Dobrym przykładem jest tutaj typ _System.Windows.Forms.Message_, który reprezentuje komunikat Windows przekazywany do procedury obsługi komunikatów – czasami zachodzi potrzeba utworzenia setek instancji tego typu w ułamku sekundy. Innym dobrym powodem może być potrzeba interoperacyjności z kodem niezarządzanym, i tu kłaniają się przykładowo wszystkie typy z przestrzeni nazw _System.Drawing_.

  4. Niestety pierwszy przykład nie skompiluje się bo Position.X jest nullem. Nie został zainicjalizowany dlatego nie wiem co dokładnie chciałeś tym przykładem pokazać.

  5. @Jacek
    Według mnie kod się skompiluje, ale zaraz po uruchomieniu wyrzuci _NullReferenceException_. Zwykłe przeoczenie, jak sądzę; nie ma sensu popadać w histerię.

    Może jeszcze jako ciekawostkę dodam, że metodę _SetPosition_ można zaimplementować w ten sposób:

    _public void SetPosition(int x, int y)
    {
    Point p = this.Position;
    p.X = x;
    p.Y = y;
    this.Position = p;
    }_

    i to zadziała niezależnie od tego, czy _Point_ jest klasą, czy strukturą.

  6. @Jacek

    Tak, przepraszam, Apl ma rację, zwykłe przeoczenie inicjalizacji. Zaraz to poprawie.

    @apl

    **true** jak to mawiaja :)

Comments are closed.