Przypisanie istniejącego obiektu do innej zmiennej niesie ze sobą pewne ryzyko. Należy być świadomym, czym różni się i co zawiera w sobie typ referencyjny, a co typ wartościowy. O typach wartościowych pisałem już nieco podczas omawiania struktur, w tym materiale. Tam też przedstawiłem problem przypisywania typów referencyjnych do innej zmiennej, który zaraz przypomnę.
Przypisywanie typów wartościowych
Jak sama nazwa wskazuje typy te zawierają w sobie wartość, która zostaje im przypisana. Stwórzmy w naszym testowym przykładzie zwykłą strukturę reprezentującą punkt na płaszczyźnie 2D:
public struct { public int X { get; set; } public int Y { get; set; } }
Punkt taki składa się z dwóch właściwości, jedna wskazuje położenie w poziomie, a druga w pionie (takie małe przypomnienie ze szkoły). Teraz utwórzmy obiekt naszej struktury, czyli punkt, którym chcemy manipulować, a następnie przypiszmy go do kolejnej zmiennej i zedytujmy wartości. Przedstawia to kod:
Point p1 = new Point { X = 1, Y = 2 }; Point p2 = p1; p1.X = 15; p2.Y = 3; Console.WriteLine($"Punkt 1: ({p1.X},{p1.Y})"); Console.WriteLine($"Punkt 2: ({p2.X},{p2.Y})");
Nie ma tutaj nic do tłumaczenia, operacja to podstawy programowania. Tworzymy zmienne, zmieniamy wartości, a potem wyświetlamy wyniki. Rezultat na konsoli to:

Oba obiekty zmieniają swoją wartość niezależnie od siebie i program działa tak, jakby można było się spodziewać, ale…
Przypisywanie typów referencyjnych
Zróbmy tylko jedną, malusieńką zmianę w tym przykładzie. Zamieńmy naszą strukturę punktu na klasę. Czy rezultat będzie taki sam?

Jak widzimy nie jest. A to dlatego, że typy referencyjne nie przechowują wartości (czyli dwóch intów) w sobie, a referencje. Referencja wskazuje miejsce do innego obszaru pamięci nazwanego stertą, w której będą przechowywane wartości. Czyli my podstawiając p1 do p2 nie kopiujemy wartości, tylko adres. Od tej pory te dwa obiekty przechowują adres do tego samego elementu, więc zmiana wartości na jednej zmiennej spowoduje również zmianę wartości na drugiej, bo to są te same byty. A co jeżeli chcielibyśmy utworzyć kopię elementu, w taki sam sposób w jaki działa to na typach wartościowych, czyli gdy chcemy otrzymać niezależne byty z takimi samymi wartościami?
Metoda MemberwiseClone
Metoda ta należy do klasy Object, więc dziedziczą ją wszystkie klasy, które utworzymy. MemberwiseClone spowoduje utworzenie kopii obiektów, dzięki czemu otrzymamy dwa różne byty z tymi samymi wartościami. Wróćmy do przykładu powyżej i ją zastosujmy.
static void Main(string[] args) { Point p1 = new Point { X = 1, Y = 2 }; Point p2 = p1.CreateCopy(); p1.X = 15; p2.Y = 3; Console.WriteLine($"Punkt 1: ({p1.X},{p1.Y})"); Console.WriteLine($"Punkt 2: ({p2.X},{p2.Y})"); Console.Read(); } public class Point { public int X { get; set; } public int Y { get; set; } public Point CreateCopy() { return (Point)this.MemberwiseClone(); } }
Otrzymujemy rezultat, taki jaki chcieliśmy:

Metoda MemberwiseClone zwraca obiect, wiec musimy rzutować ją na odpowiedni typ, którą chcemy uzyskać. Utworzyliśmy tutaj dodatkową metodę, która zwraca pożądaną kopię. Ale dlaczego nie wywołaliśmy MemberwiseClone bezpośrednio w Maini-e? Można by tu powiedzieć, że chcieliśmy, aby odpowiedzialność za tworzenie kopii punktu znajdowała się tam gdzie powinna, czyli w klasie Point. Dbamy wtedy o zasadę pojedynczej odpowiedzialności (S w SOLID), ale nie byłaby to prawda. Bo pomimo, że każda klasa posiada metodę MemberwiseClone, to jest to metoda chroniona (protected). Więc możemy odwołać się do niej jedynie z wnętrza naszej klasy (lub innych dziedziczących po niej). Ale z pomocą przychodzi nam …
Interfejs ICloneable
Składa się on tylko z jednej metody Clone, której zadaniem jest zwrócenie kopii obiektu. Robi właściwie to samo co metoda pośrednia CreateCopy stworzona przykład temu:
class Point : ICloneable { public int X { get; set; } public int Y { get; set; } public object Clone() { return this.MemberwiseClone(); } }
Implementacja tej metody jest identyczna, z wyjątkiem zwracanego typu, bo tutaj jest to object. Zatem rzutowanie przenosimy do metody Main.
Podsumowanie
Nie są to mechanizmy, które są używane często, a zazwyczaj nawet nie są wykorzystywane do funkcji, do których zostały przeznaczone. Skopiowanie typu wartościowego do innego może odbyć się również przez serializacje i deserializacje obiektu. Jednak metoda MemberwiseCopy jest elementem najważniejszej klasy języka C#, czyli klasy Object. Z tego powodu warto o niej przeczytać, a wykorzystywać ją w bardzo specyficznych momentach, kiedy na prawdę jest to konieczne. Ten artykuł nie kończy rozważań o MemberwiseClone, temat pociągnę jeszcze w kolejnym wpisie i wyjaśnię czym jest shallow oraz deep copy, bo te metody bez tej wiedzy mogą więcej napsuć, niż naprawić.