Strona główna > Przykłady > [C#] Płytkie i głębokie kopie obiektów – wprowadzenie

[C#] Płytkie i głębokie kopie obiektów – wprowadzenie

Dzisiaj omówię mechanizm tworzenia płytkich i głębokich kopii obiektów w języku C#. Zrozumienie tego zagadnienia jest ważne jeżeli chcemy tworzyć rozbudowane programy orientowane obiektowo.

Wstęp

W języku C# wyróżniamy typy wartościowe i referencyjne.  Typy wartościowe to typy proste (np. char, double, int, bool) oraz typy wyliczeniowe i struktury. Typy referencyjne to klasy, interfejsy, delegacjie i tablice. Zmienne typów wartościowych przechowywane są na stosie (ang. stack), natomiast typów referencyjnych na stercie (ang. heap). W przypadku, gdy chcemy skopiować zawartość jednej zmiennej do drugiej zmiennej, gdy obie zmienne są typu wartościowego, intuicyjnie wykorzystujemy operator przypisania „=”.

int a=5;
int b=a;
Console.WriteLine("b="+b); // b=5

Gdy mamy do czynienia z typem referencyjnym, użycie operatora przypisania spowoduje skopiowanie referencji. Przykład:

class A //bardzo prosta klasa
{
      public int liczba;
}

static void Main(string[] args)
{
    A obiekt1 = new A();
    obiekt1.liczba = 5;
    A obiekt2 = new A();
    obiekt2 = obiekt1; // od tej linijki obiekt1 i obiekt2 są referencjami do tej samej instancji obiektu typu A
    Console.WriteLine("obiekt2.liczba=" + obiekt2.liczba); // 5
    obiekt1.liczba = 7;
    Console.WriteLine("obiekt2.liczba=" + obiekt2.liczba); // 7
}

Wyjście:

obiekt2.liczba=5
obiekt2.liczba=7

W powyższym przykładzie definiujemy bardzo prostą klasę A, która zawiera pole liczba. W metodzie Main tworzymy obiekt typu A o nazwie obiekt1 i przypisujemy 5 do jego pola liczba. Następnie tworzymy obiekt2 i używamy „=” z zamiarem skopiowania do niego zawartości obiekt1. Wypisujemy obiekt2.liczba i po uruchomieniu programu widzimy, że jest równa 5. Ten krok może utwierdzić począkującego programistę w błędnym przekonaniu, że skopiował zawartość obiekt1 do obiekt2. Jednak dwie kolejne linijki programu (13 i 14) udowadniają, że tak nie jest. Do obiekt1.liczba przypisujemy 7 i wypisujemy obiekt2.liczba. Teraz widzimy, że obiekt2.liczba jest równe 7, pomimo tego, że przypisaliśmy 7 tylko do obiekt1.liczba. Tak więc obie zmienne (obiekt1 i obiekt2) wskazują na tą samą instancję obiektu typu A w pamięci.

O tym, czy dwie referencje odwołują się do tego samego obiektu, możemy się dowiedzieć stosując statyczną metodę Object.ReferenceEquals().

if (Object.ReferenceEquals(obiekt1, obiekt2))
       Console.WriteLine("O w mordę jeża!");
else
       Console.WriteLine("Referencje nie odwołują się do tego samego obiektu");

W naszym przykładzie Object.ReferenceEquals(obiekt1, obiekt2) zwróci wartość true.

Jak zatem poradzić sobie z problemem kopiowania obiektów typu referencyjnego? Tego dowiecie się zaraz po reklamie (nie odchodźcie od komputera).

Płytka kopia

Płytkiej kopii używamy kiedy klasa obiektu, który chcemy skopiować, nie zawiera pól referencyjnych. Nasza klasa dziedziczy po interfejsie ICloneable i implementuje metodę Clone(), w której wykorzystamy metodę System.Object.MemberwiseClone().

class A : ICloneable
{
       public int liczba;
       public Object Clone()
       {
             return MemberwiseClone();
       }
}

static void Main(string[] args)
{
      A obiekt1 = new A();
      A obiekt2 = new A();
      obiekt1.liczba = 5;
      obiekt2 = (A)obiekt1.Clone();
      Console.WriteLine("obiekt2.liczba=" + obiekt2.liczba); //Wyświetli 5

      if (Object.ReferenceEquals(obiekt1, obiekt2))
          Console.WriteLine("Referencje odwołuja się do tego samego obiektu");
      else
          Console.WriteLine("Referencje nie odwołuja się do tego samego obiektu");
}

Tym razem użyliśmy metody Colne() i udało nam się skopiować obiekt1. Nasze referencje nie odwołują się do tego samego obiektu w pamięci. Pamiętajmy o tym, że metoda Clone() zwraca typ Object, który musimy rzutować na nasz typ A.

Głęboka kopia

Głębokiej kopii używamy, gdy klasa obiektu, który chcemy skopiować ma pola referencyjne. Wtedy metoda MemberwiseClone() nie wystarcza, czego dowodem jest poniższy kod.

class B
{
      public string slowo;
}

class A : ICloneable
{
      public int liczba;
      public B objB;
      public A()
      {
            objB = new B();
      }
      public Object Clone()
      {
            return MemberwiseClone();
      }
}

static void Main(string[] args)
{
     A obiekt1 = new A();
     A obiekt2 = new A();

     obiekt1.liczba = 5;
     obiekt1.objB.slowo = "kurczak";

     obiekt2 = (A)obiekt1.Clone();

     Console.WriteLine("obiekt2.liczba="+obiekt2.liczba);
     Console.WriteLine("obiekt2.objB.slowo="+obiekt2.objB.slowo);

     if (Object.ReferenceEquals(obiekt1.objB, obiekt2.objB))
         Console.WriteLine("Referencje odwołuja się do tego samego obiektu"); //niestety zostanie wykonana instrukcja w tej linijce
     else
         Console.WriteLine("Referencje nie odwołuja się do tego samego obiektu");
}

Do klasy A dodaliśmy pole objB typu B. Próbujemy skopiować zawartość obiekt1 do obiekt2 i wszystko byłoby w porządku, gdyby nie to, że objekt1.objB i obiekt2.objB wskazują na ten sam obiekt w pamięci, więc zmiany w objekt1.objB będą także widoczne przez objekt2.objB. Nie chcieliśmy uzyskać takiego efektu. Jak zatem rozwiązać ten problem? W klasie A tworzymy własną metodę kopiującą, która zwróci głęboką kopię obiektu.

class B
{
      public string slowo;
}

class A
{
       public int liczba;
       public B objB;
       public A()
       {
             objB = new B();
       }
       public A GlebokaKopia()
       {
             A tempA = new A();
             tempA.liczba=this.liczba;
             tempA.objB.slowo=this.objB.slowo;
             return tempA;
        }
}

static void Main(string[] args)
{
     A obiekt1 = new A();
     A obiekt2 = new A();

     obiekt1.liczba = 5;
     obiekt1.objB.slowo = "kurczak";

     obiekt2 = obiekt1.GlebokaKopia(); // nie używamy już Clone()

     Console.WriteLine("obiekt2.liczba="+obiekt2.liczba);
     Console.WriteLine("obiekt2.objB.slowo="+obiekt2.objB.slowo);

     if (Object.ReferenceEquals(obiekt1.objB, obiekt2.objB))
         Console.WriteLine("Referencje odwołuja się do tego samego obiektu");
     else
         Console.WriteLine("Referencje nie odwołuja się do tego samego obiektu"); //zastanie wykonana instrukcja w tej linii
}

Rezygnujemy z interfejsu ICloneable oraz metody MemberwiseClone(). Zamiast tego tworzymy metodę GlebokaKopia(), która zwraca obiekt tempA. Do tego obiektu zostają skopiowane wszystkie potrzebne nam wartości. W metodzie Main została dokonana jedna zmiana (w linijce nr. 31 ). Teraz obiekt1.objB i obiekt2.objB są referencjami do zupełnie innych obiektów w pamięci. Osiągneliśmy swój cel – stworzyliśmy głęboką kopię obiektu.

W całej galaktyce zapanował pokój, ład i porządek. 🙂

EDIT:
Poniżej zamieszczam link, który świetnie uzupełnia temat przedstawiając całą gamę sposobów klonowania:
http://www.csharp411.com/c-object-clone-wars/

Advertisements
Kategorie:Przykłady Tagi: ,
  1. 18 lutego 2011 o 6:53 am

    Często wystarczające może okazać się podejście:

    var deepCopy = src.Serialize().Deserialize()

    , gdzie te metody używają jakiejś formy serializacji – czy to binarnej, czy do JSON. Trzeba potestować czy wszystkie wartości są kopiowane, a nie np tylko publiczne (tak by było w przypadku JSON). Unikamy wówczas ręcznego klepania, które nigdy nie jest przyjemne.
    Dodatkowo można pobawić się Automapperem i mapować jedną instancję na drugą (pustą) instancję, wtedy mamy do dyspozycji cały power automappera ze wszystkimi jego konfiguracjami. Ale też zadziała tylko dla publicznych wartości (co jednak często może okazać się wystarczające).

    • Grzessiekk
      19 lutego 2011 o 10:02 am

      Procent proszę podaj jakiś link do dobrego przykładu z Automapperem.

      • 21 lutego 2011 o 6:09 am

        Automapper sprowadza się właściwie do:

        SomeClass target = new SomeClass();
        Mapper.Map(source, target);

        czy coś w ten deseń. Ważne jest aby przekazać nową instancję do „wypełnienia”, bo w przeciwnym wypadku automapper zwraca chyba po prostu obiekt źródłowy.

  2. Michal
    22 lutego 2011 o 1:10 pm

    Uwaga na słowo: wykożystamy 🙂

    • 23 lutego 2011 o 10:38 am

      Zawsze warto się zastanowić przed wykorzystaniem 😀 Dzięki! Poprawione.

  3. 23 lutego 2011 o 6:19 pm

    Jeśli już chcesz robić kopię poprzez kopiowanie pól, to przecież szybciej i o niebo lepiej wykorzystać do tego refleksję.

    • 24 lutego 2011 o 9:44 am

      Zgodzę się, a powody nieużycia przeze mnie refleksji są dwa. Pierwszy: kopiowanie po kolei wartości pól jest według mnie najprostszym (jeżeli chodzi o poziom skomplikowania, prostotę zrozumienia) sposobem utworzenia głębokiej kopii. Drugi: jeszcze nie wiedziałem nic o refleksjach 🙂 Możliwe, że nie przygotowałem się wystarczająco przed napisaniem tego artykułu, jednak z drugiej strony miał on na celu jedynie zarysowanie problemu płytkich i głębokich kopii (o czym świadczy tytuł). Bardzo się cieszę, że mogłem się czegoś nauczyć z powyższych komentarzy, oraz z tego, że zmotywowały mnie one do głębszego poznania tematu, ponieważ na tym mi najbardziej zależy 🙂

      Poniżej link, który świetnie uzupełnia temat:
      http://www.csharp411.com/c-object-clone-wars/
      Oprócz wspomnianych już sposobów autor pisze tam o „Clone with IL” oraz „Clone with Extension Methods”.

      I jeszcze HCloner autorstwa Deti z 4programmers.net
      http://pastebin.com/f713584a

      • 24 lutego 2011 o 7:05 pm

        Programista uczy się przez całe życie;)

  4. lestat426
    18 marca 2011 o 11:44 am

    Dla mnie cała ta zabawa z kopiowaniem samych adresów obiektów klas za pomocą operatora ‚=’ to jakaś pomyłka w języku C# i jest to bardzo nie przemyślany pomysł. Microsoft mając w zamiarze ułatwienie pisania kodu poprzez ograniczenie lub wręcz pozbycie się wskaźników na rzecz mniej zawiłych referencji, w niektórych przypadkach z drugiej strony bardziej skomplikował i zaciemnił całą sprawę np. poprzez niejasne, nieintuicyjne posługiwanie się operatorem ‚=’ w przypadku typów referencyjnych. W C++ kopiowanie obiektów jakichś klas jest bardziej naturalne – oczywiście wiem, że są one kopiowane przez wartość, ale weźmy na moment poniższy przykład w C++:
    ———————–
    Klasa Obiekt1; /* Przypuśćmy, że konstruktor zainicjował odpowiednio wszelkie wymagane pola */

    Klasa Obiekt2;

    /*… tu znajduje się jakiś kod operujący na polach obiektu ‚Obiekt1’ i zmieniający ich wartości. */

    Obiekt2 = Obiekt1; // kopiowanie przez wartość.
    —————————
    W ostatniej instrukcji, jeśli klasa zawiera pola, które są np. wskaźnikami na inne obiekty to potencjalne przeciążenie przez nas operatora ‚=’ w definicji klasy ‚Klasa’ pozwala na indywidualne ustalenie sposobu, co zrobić z takimi polami w momencie ich kopiowania (skopiować tylko adresy, które się za nimi kryją, czy zaalokować pamięć dla nowych zasobów i dopiero ich adres podstawić pod problematyczne pola do obiektu docelowego). Najważniejsze jednak, że w C++ mamy nad tym kontrolę poprzez przeciążenie operatora ‚=’. W C# natomiast nie można przeciążyć operatora ‚=’, tak więc jesteśmy skazani na kolejny bałagan tym razem związany z referencjami a nie wskaźnikami. Ech! Czy te mądre głowy w Microsofcie nie mogłyby projektując języka C# choćby np. wprowadzić dwóch różnych operatorów przypisania jeden dla referencji (do obiektów alokowanych na stercie (nie wiem czy dobrze przetłumaczyłem słowo ‚heap’), dla typów typów alokowanych na stosie (stack), przy czym ten drugi powinien mieć możliwość bycia przeciążanym. Dla przykładu operator kopiowania przez wartość byłby zwykłym znakiem równości: ‚=’, a operator kopiowanie referencji mógłby wyglądać np. jak: „:=” (zapożyczony z Pascala :). Byłoby to proste i do tego bardzo obrazowe, bo patrząc na kod i widząc jeden z tych dwóch operatorów od razu wiadomo by było z jakim rodzajem kopiowania mamy do czynienia. Poza tym można by spokojnie pozwolić programiście na przeciążanie operatora ‚=’. Ech! Jak dla mnie Microsoft często najpierw robi, potem myśli. Jeśli moja mała analiza i propozycje zwierają jakieś błędy logiczne lub inne lub nie uwzględniają pewnych ograniczeń, których obecnie nie znam, proszę o poprawienie mnie.

    Lestat426

  5. lestat426
    18 marca 2011 o 5:30 pm

    Heh… chyba znalazłem pewnego rodzaju półśrodek zaradczy, choć może nie jest to idealne rozwiązanie, ale działa trochę bardziej intuicyjne niż wywoływanie funkcji typu:
    Obiekt1 = Obiekt2.Kopia(); // tudzież wywołanie statycznej metody:
    Obiekt1 = Klasa.Kopia(Obiekt2) // zakładając, że obiekty ‚Obiekt1’ i ‚Obiekt2’ są typu ‚Klasa’

    Otóż możemy przynajmniej w każdej przez nas stworzonej klasie i w klasach przez nas stworzonych dziedziczonych od klas z bibliotek .NET (no bo wiadomo, że nic nie zdziałamy np. z klasami które są „zaplombowane” (sealed) itp.).. możemy sobie przeciążyć jakiś operator binarny lub arytmetyczny typu: +, -, *, /, %, &, |, ^ (nic nie zdziałamy z operatorami ‚<>’ bo w tym przypadku uparte C# zmusza nas aby jednym z argumentów była wartość typu int (ech!). No ale weźmy np. operator ‚|’, który po przy przeciążeniu tworzy nowy obiekt naszej klasy i przypisuje go drugiemu argumentowi. np.:

    class Klasa
    { private int x;
    private int y;
    public int X {get; set;}; // property X
    public int Y {get; set;}; // property Y
    public static operator | (Klasa A, Klasa B)
    { // Zakładamy, że kopiujemy B do A (drugi argument do pierwszego)
    if (B == null) return null // To taka mini ochrona przed wystąpieniem błędu
    A = new Klasa(); // tworzymy nową „treść” dla A
    A.X = B.X; // kopiowanie pól…
    A.Y = B.Y; // …według naszego uznania 🙂
    return A; // zwracamy referencje do A
    }
    }

    Oczywiście jeśli gdzieś potem będziemy chcieli skopiować jeden obiekt klasy ‚Klasa” do jakiegoś innego obiektu tego samego typu, to instrukcja w stylu:

    Obiekt1 | Obiekt2; // błąd!

    spowoduje oczywiście błąd kompilacji, ale jeśli użyjemy niejawnie automatycznie utworzonego przez klasę przeciążonego operatora przypisania: |= , to otrzymamy to co chcemy:

    Obiekt1 |= Obiekt2; // Ha! Tu kompilator nie będzie się „złościł”.

    Za kulisami zostanie utworzona kopia obiektu ‚Obiekt2’ a jego referencja zostanie przypisana do zmiennej ‚Obiekt1’. Na zewnątrz będzie to wyglądać jak dla mnie choć trochę bardziej naturalnie i intuicyjnie niż: Obiekt1 = Obiekt2.Kopia(). Oczywiście jeśli ‚Obiekt1’ odnosił się wcześniej do jakiejś treści, to treść ta zostanie osierocona w czeluściach sterty :), skąd potem zostanie usunięta przez zbieracza śmieci (garbage collector). Jeśli natomiast ‚Obiekt2’ miał wartość ‚null’, to i ‚Obiekt1’ otrzyma taką samą wartość. Jednak obsługę takich sytuacji niech sobie może każdy obmyśli we własnym zakresie (np. obsługa jakichś wyjątków już w definicji przeciążonego operatora lub coś w tym stylu).
    Możemy oczywiście przeciążyć inne operatory np. &, ^, %, co po połączeniu z ‚=’ da nam: &=, ^=, %=, ale to już rzecz gustu. Mnie najbardziej pasuje postać |= (choć może trochę mylić się z != ).

    Życzę przyjemnych zmagań z niedopracowanymi stronami języka C# 🙂

    Lestat426

  6. lestat426
    18 marca 2011 o 5:37 pm

    ERRATA

    W poprzednim moim mailu jest błąd, ale nie z mojej winy. Tam gdzie jest wymieniony operator ”, którego nie ma oczywiście w C# miałem na myśli dwa operatory typu: (muszę to napisać słownie) dwa nawiasy trójkątne zwrócone w lewo i dwa nawiasy trójkątne zwrócone w prawo. Po napisaniu tekstu został on automatycznie przeformatowany, stąd ów błąd.

    Lestat426

    • 19 marca 2011 o 12:44 pm

      lestat426!
      Na początku muszę wyrazić swój podziw – trochę się „naprodukowałeś” przy tych komentarzach, dzięki! 🙂
      Przeciążanie operatorów zostało wymyślone by poprawić czytelność kodu, jednak czy naprawdę to robi, to już kwestia gustu (zresztą sam o tym wspominałeś). Jeżeli chodzi tyko o taką prostą rzecz jak zastąpienie zapisu Obiekt1 = Obiekt2.Kopia() zapisem Obiekt1 „jakiś operator” Obiekt2, to niektórzy wolą mieć dobrze nazwaną funkcję.
      A pomysł dwóch różnych operatorów dla kopiowania wartości i referencji wydaje mi się ciekawy.
      Skoro jesteśmy przy c++ to warto wspomnieć, że można utworzyć własny konstruktor kopiujący, który jest wywoływany automatycznie m.in. w sytuacji obiekt1=obiekt2 (jeśli jeszcze dobrze pamiętam „Symfonie” Grębosza 😉 )

  7. lestat426
    20 marca 2011 o 5:21 am

    Ot, gaduła ze mnie 🙂
    Tak, racja, konstruktor kopiujący też się przyda. Ogólnie to kilka tygodni temu po długim zastoju (jako programista) byłem zmuszony przypomnieć sobie C++ i od razu wejść w C#, jak dla mnie obecne Microsoftowskie C++ w połączeniu z .NET to mi wygląda po prostu na jakiś potworny śmietnik słów kluczowych, kwalifikatorów, identyfikatorów a nawet operatorów, itp, tak, że kod staje się na prawdę ciężki w pisaniu, czytaniu a szczególnie rozumieniu. Wybrałem wyjście pośrednie: jeśli chodzi o projektowanie interfejsu, lepszą czytelność i łatwość pisania kodu – wykorzystać C#, natomiast korzystanie z gotowych (czasem już starych ale często nieocenionych, lub jakichś uniwersalnych) kodów źródłowych C++ przeznaczonych do siermiężnej pracy nie związanej z interfejsem – obudować takiego typu kody klasą typu „managed” w C++ (taki jakby interfejs pomiędzy managed i unmanaged) i potem tę „obudowę” wykorzystać bezpośrednio w C# dzięki CLS i CTS (jak to się wykładało? Common Language Specification i Common Type System, czy jakoś podobnie), no bo skoro sam język nie gra już takiej ważnej roli (jak to ciągle z dumą podkreśla MS), jeśli tylko dostosujemy się w naszym między-językowym interfejsie do zasad jakie panują w .NET, to kłopoty przy korzystaniu z kodów źródłowych innych języków mamy z głowy (tak niby mówi MS, ale zapewne do końca tak nie jest :), choć jeszcze się w to nie zagłębiałem).
    Wracając jeszcze na moment do różnych typów operatorów kopiowania (np. jak na zaproponowanym przeze mnie przykładzie: ‚=’ i ‚:=’ ) to myślę, że już po szeregu latach używania przez ludzi C#, które zdążyło umocnić się wśród innych języków teraz jeśli Microsoft jeszcze łaskawie zwróciłby uwagę na tę sprawę (operatorów) (pewnie trzeba byłoby wysyłać niezliczone petycje :)) to i tak ze względu na wsteczną kompatybilność kodu pewnie pozostałaby tylko możliwość dodania do już istniejącej składni jakiegoś operatora do kopiowania przez wartość typów referencyjnych i możliwość przeciążania tegoż operatora + ewentualny konstruktor kopiujący (no bo definiowanie własnego takiego konstruktora w C# jest wiadomo – zabronione).
    Apropos książek…swego czasu ja łamałem sobie głowę na „Praktycznym wprowadzeniu do programowania obiektowego w języku C++” (Barteczko). To było dawno, ale w każdym razie miał facet zacięcie, sporo można było się nauczyć szczególnie od strony ideowej.

    Pozdrawiam
    Lestat426

  1. 18 lutego 2011 o 12:18 am
  2. 18 lutego 2011 o 12:30 am
  3. 25 listopada 2011 o 12:33 pm

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: