Backend,  Programowanie

Płytkie (shallow) oraz głębokie (deep) kopiowanie – Język C#

Kontynuuję artykuł na temat tworzenia kopii obiektów referencyjnych. We wcześniejszym wpisie, który możecie znaleźć tutaj, zaprezentowałem jak działa metoda MemberwiseClone oraz interfejs ICloneable, ale czy na pewno wszystko zostało powiedziane?

Płytkie kopiowanie (ang. shallow copy)

W poprzednim materiale nie bez powodu badanym obiektem był punkt, składający się z dwóch typów wartościowych: X oraz Y. Tym razem będzie trochę inaczej, bo utworzymy obiekt zawierający typy referencyjne.

public class Detective : ICloneable
{
   public string Name { get; set; }
   public Task Task { get; set; }
   public object Clone()
   {
      return this.MemberwiseClone();
   }
}
public class Task
{
   public int Id { get; set; }
   public string Title { get; set; }
}

W przykładzie mamy klasę detektywa, który ma do wykonania pewne zadanie. Będzie ono składać się z identyfikatora oraz zmiennej tekstowej określającej co należy zrobić. Sklonujmy teraz detektywa i zmienimy pewne wartości.

Task task1 = new Task { Id = 1, Title = "Śledzenie Alicji" };
Detective detective1 = new Detective { Name = "Patryk", Task = task1 };
Detective detective2 = (Detective)detective1.Clone();

detective1.Name = "Krystian";
detective2.Task.Id = 2;

Console.WriteLine($"Detektyw {detective1.Name} zadanie: {detective1.Task.Id}. {detective1.Task.Title}");
Console.WriteLine($"Detektyw {detective2.Name} zadanie: {detective2.Task.Id}. {detective2.Task.Title}");

Po uruchomieniu przykładu zobaczymy:

Nie wyszło jednak tak, jak chcieliśmy. Imiona detektywów pokazały się prawidłowo, ale w obu przypadkach zmienił się numer zadania detektywów, a edytowaliśmy go tylko u jednej osoby. Wszystko przez to, że podczas klonowania, za pomocą metody MemberwiseClone zachodzi tzw. kopiowanie płytkie. Polega ono na tym, że kopiowane są wartości poszczególnych składowych (np. pól czy właściwości) danego obiektu. Ale no właśnie, wartości! Zobaczcie, że wartością detektyw1.Task jest referencja do zadania. Czyli po skopiowaniu detektyw2.Task dostaje dokładnie ten sam adres (referencję), a nie całkiem nową instancję zadania z identycznymi danymi. Teraz obaj manipulują na tym samym zadaniu. 

Zatem, zapamiętaj, że klonowanie obiektu za pomocą samej metody MemberwiseClone zadziała tak jak się tego spodziewasz, gdy masz typy wartościowe, a z typami referencyjnymi trzeba się będzie trochę pomęczyć.

Ale, ale, ale! String jest typem referencyjnym i u mnie działa!

Jedynym wyjątkiem od powyższej reguły jest string. Jest to typ bardzo specyficzny, ponieważ w wielu sytuacjach zachowuję się on jak typ wartościowy, a w rzeczywistości jest to typ referencyjny. O nim zrobię kiedyś osobny materiał, ale tutaj krótko wyjaśnię, że jest to obiekt niemutowalny (ang. immutable), czyli po przypisaniu nowego stringa tworzy się nowy obiekt. W naszym przypadku po sklonowaniu detektywów mamy dokładnie takie same referencję w obu właściwościach Name, o czym może świadczyć ten kod:

if (ReferenceEquals(detective1.Name, detective2.Name))
   Console.WriteLine("Taka sama referencja");

Wynikiem jego będzie wyświetlenie komunikatu na ekranie. Obiekt ten jednak przestaje być tą samą referencją w momencie edycji wartości, czyli przypisania do niego:

detective1.Name = "Krystian";

W drugim detektywie referencja, a w raz z nią zawartość tekstowa bez zmian, ale pierwszy detektyw dostaje zupełnie nową instancję ciągu znaków, czyli idzie z tym w parze nowa referencja.

Ale wróćmy teraz do problemu zagnieżdżonych typów referencyjnych. Z pomocą przyjdzie nam:

Głębokie kopiowanie (ang. deep copy)

W kopiowaniu głębokim sami musimy zadbać o to, by sklonować poprawnie zagnieżdżone elementy. Prezentuje to przykładowy kod:

public object Clone()
{
   Detective copyDetective = (Detective)this.MemberwiseClone();
   copyDetective.Task = new Task { Id = copyDetective.Task.Id, Title = String.Copy(copyDetective.Task.Title) };
   copyDetective.Name = String.Copy(copyDetective.Name);
   return copyDetective;
}

Po uruchomieniu dostaniemy teraz taki rezultat:

Tutaj ręcznie tworzymy nowy obiekt Task, dzięki czemu wiemy, że operujemy na dwóch różnych obiektach. Nie możemy jednak wywołać metody MemberwiseClone na obiekcie Task z tego miejsca, bo jest to metoda chroniona (protected), a poza tym jeżeli nasz Task będzie miał w sobie typ referencyjny to w głębokim kopiowaniu musimy zadbać, aby wszystko wewnątrz zostało poprawnie skopiowane, a metoda MemberwiseClone kopiuje tylko obiekt do którego się odnosimy. Jednak, żeby uatrakcyjnić nasz kod możemy użyć:

Interfejs ICloneable

Zwiększy to pewną czytelność rozwiązania, bo będziemy mogli bez problemu sklonować obiekt Task wewnątrz detektywa, bez tworzenia obiektu od nowa:

public class Detective : ICloneable
{
   public string Name { get; set; }
   public Task Task { get; set; }

   public object Clone()
   {
      Detective copyDetective = (Detective)this.MemberwiseClone();
      copyDetective.Task = copyDetective.Task.Clone() as Task;
      return copyDetective;
   }
}
public class Task : ICloneable
{
   public int Id { get; set; }
   public string Title { get; set; }

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

Co jednak jest nie tak z tym interfejsem?

  1. Dokumentacja nie precyzuje czy ICloneable odnosi się do kopiowania głębokiego, czy płytkiego. Zastanawiasz się pewnie, co to za problem, przecież będziesz wiedział, którego rozwiązania używasz, ale no właśnie będziesz wiedział to tylko Ty (jeżeli nie zapomnisz) i nikt poza Tobą. Do samodzielnych garażowych projektów okej, ale jeżeli będziesz chciał wystawić coś na zewnątrz, albo pracował z kimś w zespole, skąd będziesz wiedział jakiego sposobu kopiowania użyła inna osoba?
  2. Jeżeli chce się kopiować głęboko przydało by się opatrzeć interfejsem ICloneable wszystkie zagnieżdżone typy, inaczej będzie trzeba w klonowaniu tworzyć ręcznie instancję.
  3. W tym przykładzie pominąłem kopiowanie stringów, żeby nie zaciemniać przykładu, który i tak działa jak chcemy, bo typ ten jest niemutowalny.

Na szczęście potrzeba używania kopiowania nie jest częsta i da się ją wykonać innymi sposobami, jednak warto mieć w świadomości czym różnią się te dwa rodzaje kopiowania, oraz jakie niedoskonałości ma interfejs ICloneable.

Podsumowanie

Kopiowanie płytkie odnosi się jedynie do obiektu, który chcemy skopiować i operacja klonowania nie wychodzi poza jego ramy. Tzn. jeżeli nasz obiekt będzie zawierał w sobie jakieś typy referencyjne, to skopiowany obiekt dostanie dokładnie takie same referencje co oryginał. Wewnętrzny obiekt nie zostanie skopiowany.

Natomiast kopiowanie głębokie oznacza, że zostaną skopiowane wszystkie typy w zakresie całego obiektu, również te zagnieżdżone typy referencyjne.

Więc „We’re far from the shallow now” jakoś samo narzuca się na zakończenie, a piosenka Shallow dostała niedawno Oscara.

Social media & sharing icons powered by UltimatelySocial