Rozróżnia się dwie możliwości klonowania obiektów:

Zapewne wszyscy wiedzą o istnieniu metody MemberwiseClone w klasie Object.
Pozwala ona na wykonanie tzw. “shallow copy”, czyli “płytkiej kopii” obiektu. Kopia taka nie radzi sobie jednak z typami referencyjnymi zagnieżdżonymi w klonowanym obiekcie. Potrafi poprawnie kopiować tylko Value Types.

Weźmy przykładową klasę Person:

public class Person
{
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public DateTime BirthDate { get; set; }
        public Person Boss { get; set; }

        public Person( string firstName, string lastName, DateTime birthDate )
        {
            this.FirstName = firstName;
            this.LastName = lastName;
            this.BirthDate = birthDate;
        }      
}

Możemy zaimplementować interfejs ICloneable dla “shallow copy” w ten sposób:

public object Clone()
{
      return this.MemberwiseClone();
}

Co się jednak stanie, gdy wykonamy kopię zmiennej kowalski z poniższego przykładu?

Person jankowski = new Person("Stefan", "Jankowski", new DateTime(1977,2,25));
Person kowalski = new Person("Jan", "Kowalski", new DateTime(1980,12,4));
           
kowalski.Boss = jankowski;

Person kowalskiCopy = (Person)kowalski.Clone();

Debug.Write(kowalski.Boss == kowalskiCopy.Boss);

Wynik? Tak jak oczekiwaliśmy, Boss w obiekcie kowalski oraz kowalskiCopy wskazują na ten sam obiekt – jankowski.

Jak widać nie jest to idealne wyjście. Rzadko zdarza się, by obiekt zawierał wyłącznie value types. Ponadto metodę MemberwiseClone możemy wywołać tylko w klasie, do której mamy dostęp. Dlaczego? Ponieważ jest ona oznaczona jako protected w klasie Object. Oznacza to, że nie użyjemy jej, gdy chcemy sklonować typ z innej biblioteki, której źródła nie jesteśmy w stanie sami zmodyfikować implementując ICloneable.

Właśnie tutaj z pomocą przychodzi nam serializacja. Wystarczy zserializować obiekt do strumienia w pamięci i zdeserializować do innej instancji. Gotowe.

Jak z realizacją?
Nic nie stoi na przeszkodze, aby zamknąć kod w statycznej metodzie i wykorzystać typ generyczny. Dzięki temu będziemy mogli przy użyciu jednej metody serializować wszystkie typy! No – prawie wszystkie.

Należy pamiętać, że typ, który chcemy serializować:

Jeśli klasa nie będzie spełniać powyższych wymagań, w runtime otrzymamy SerializationException.

Do naszej klasy wprowadzamy więc małą modyfikację:

[Serializable]
public class Person
{
        ...
}

Metoda, którą będziemy serializować nasze obiekty jest dość prosta.

W dyrektywie using tworzymy nowy MemoryStream. Następnie tworzymy instancję BinaryFormattera, informując go w konstruktorze, że będzie użyty w kontekście serializacji.
Serializujemy przekazany obiekt, przewijamy stream do początku i deserializujemy. Musimy przeprowadzić rzutowanie (Deserialize zwraca Object). Trochę kodu:

public static T CloneObject<T>( T item )
{
   using ( MemoryStream ms = new MemoryStream() )
   {
      //instancja BinaryFormattera - informujemy go,
      //ze bedzie uzyty w celu klonowania
      BinaryFormatter bf = new BinaryFormatter(null,
                new StreamingContext(StreamingContextStates.Clone));
               
      //serializacja
      bf.Serialize(ms, item);

      //przewijamy memoryStream do poczatku
      ms.Seek(0, SeekOrigin.Begin);

      //deserializacja
      return ( T )bf.Deserialize(ms);
   }
}

To wszystko. Przykład użycia:

Person kowalskiCopy = Tools.CloneObject<Person>(kowalski);

Debug.Write(kowalski.Boss == kowalskiCopy.Boss);

Efekt wykonania tej metody? W oknie output pojawi się false. Oznacza to, że obiekt został w pełni sklonowany – łącznie z referencją do szefa.

Jest jeszcze kilka niewielkich niuansów związanych z serializacją – jak np. delegaty czy propertiesy, które nie powinny być serializowalne. To jednak materiał na odrębny, obszerny wpis.


Podobne wpisy
Show-hide JavaScript – proste podłączenie przy użyciu C#

Komentarze

  1. dotnetomaniak.pl on 01.07.2010

    Shallow copy a deep copy – klonowanie obiektu z MemberwiseClone oraz przy użyciu serializacji : andrzej.net.pl…

    Dziękujemy za publikację – Trackback z dotnetomaniak.pl…

  2. Novakov on 01.07.2010

    A jak wygląda wydajność tego rozwiązania? Pozatym i tak musimy zmodyfikować klasę dopisując atrybut [Serializable]. Wydaje mi się, że lepszym rozwiązaniem byłoby jednak zaimplementowanie interfejsu ICloneable i sklonowanie odpowiednich składowych

  3. andrzej

    andrzej on 01.07.2010

    Wiadomo – wszystko ma swoje plusy i minusy. Jak z wydajnością? Szczerze powiem – nie wiem. Wątpie jednak by w aplikacjach, które mam okazję pisać robiło to wielką różnicę.Nie tworzę żadnych stricte “time critical” aplikacji.. Może kiedyś będzie chciało mi się sprawdzić ;) a może ktoś z Was już sprawdzał?
    Co do opcji ICloneable vs serializacja… Ja wychodzę z założenia “po co pisać więcej kodu, skoro można czasem napisać go mniej”. Z [Serializable] sprawa jest o tyle łatwiejsza, że jest duża szansa, że obiekt jest oznaczony takim atrybutem. Jeśli nie jest to – tak jak zwróciłeś uwagę – może to być niewielka różnica, bo i tak trzeba grzebać i tak. Jest jeden sposób na obejście braku atrybutu Serializable – jednak opiera się on na refleksjach.

    Plus klonowania przez serializację jest taki, że jeśli mamy wiele obiektów zależnych w aplikacji którą tworzymy, można po prostu oznaczyć je jako [Serializable] i użyć jednej metody. Skupić można się na logice aplikacji a nie zabawie w implementację ICloneable. Odchodzi nam wtedy dodatkowo problem zależności między obiektami czy “zapętlenia” przy skomplikowanych połączeniach między nimi. BinaryFormatter jest sprytny i dba o to, aby nie wpaść w nieskończoną pętle (nie potrafi tego już np. XMLSerializer).

    Nie wskazuję tutaj tego, co jest lepsze do sklonowania obiektu w C#. Pokazuję dwie możliwości. Której z nich użyjemy – zależy tylko od stopnia skomplikowania naszych aplikacji, preferencji itp.

    Sam BinaryFormatterem ma kilka niuansów, o których wypadałoby wspomnieć. Może jakiś wpis na ten temat wysmaruję w najbliższych tygodniach.

Dodaj komentarz