O Mnie

Aktualnie czytam

Komentarze

Polecam

DajSiePoznac
.NET Blogs PL
CodeGuru

Struktury jako własności klas

March 4, 2010 in categories: pro by Gutek

7

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).


Podziel się:

 

 

 

6 comments for "Struktury jako własności klas"

  1. Szymon Pobiega
    Szymon Pobiega Says:

    Może się czepiamSmile, 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 (blogs.msdn.com/.../...implementation-detail.aspx), to i tak nie ważne, bo liczy się tylko to, że value type jest kopiowany jako wartość, a nie referencja.

    Reply

    • Gutek
      Gutek Says:

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

      Gutek

      Reply

  2. apl
    apl Says:

    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.

    Reply

  3. Jacek
    Jacek Says:

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

    Reply

  4. apl
    apl Says:

    @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ą.

    Reply

  5. Gutek
    Gutek Says:

    @Jacek

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

    @apl

    true jak to mawiaja Smile

    Reply

Leave a Comment


(Will show your Gravatar icon)


biuquote




© 2008-2010 Jakub Gutkowski. Powered by BlogEngine.NET 1.5.1.14. Hosted on OrcsWeb.

Design