Rozróżnia się dwie możliwości klonowania obiektów:
- shallow copy – tzw. płytka kopia. Najczęściej wykonywana przy użyciu metody MemberwiseClone z klasy Object.
- deep copy – tzw. głęboka, pełna kopia. Przyjrzymy się jednej z możliwości jej realizacji – wykorzystania serializacji do strumienia w pamięci.
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 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:
{
return this.MemberwiseClone();
}
Co się jednak stanie, gdy wykonamy kopię zmiennej kowalski z poniższego przykładu?
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ć:
- musi być oznaczony atrybutem Serializable
- wszystkie jego pola muszą być serializowalne
Jeśli klasa nie będzie spełniać powyższych wymagań, w runtime otrzymamy SerializationException.
Do naszej klasy wprowadzamy więc małą modyfikację:
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:
{
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:
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
Dodaj komentarz



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