Дълбоко копие срещу плитко копие - и как можете да ги използвате в Swift

Копирането на обект винаги е било съществена част в кодиращата парадигма. Независимо дали е в Swift, Objective-C, JAVA или друг език, винаги ще трябва да копираме обект за използване в различни контексти.

В тази статия ще обсъдим подробно как да копирате различни типове данни в Swift и как те се държат при различни обстоятелства.

Тип и стойност

Всички типове данни в Swift попадат в две категории, а именно типове стойности и референтни типове.

  • Тип стойност - всеки екземпляр запазва уникално копие на своите данни. Типовете данни, които попадат в тази категория, включват - всички основни типове данни, структура, enum, масив, кортежи.
  • Референтен тип - инстанциите споделят едно копие на данните и типът обикновено се определя като клас.

Най-отличителната черта и на двата типа се крие в тяхното копиращо поведение.

Какво представлява Deep and Shallow copy?

Екземпляр, независимо дали е тип стойност или референтен тип, може да бъде копиран по един от следните начини:

Дълбоко копие - Дублира всичко

  • С дълбоко копие всеки обект, посочен от източника, се копира и копието е насочено към местоназначението. Така ще бъдат създадени два напълно отделни обекта.
  • Колекции - Дълбоко копие на колекция е две колекции с дублирани всички елементи в оригиналната колекция.
  • По-малко податливи на състезателни условия и се представят добре в многопоточна среда - промените в един обект няма да имат ефект върху друг обект.
  • Типовете стойности се копират дълбоко.

В горния код,

  • Ред 1: arr1 - масив (тип стойност) на Strings
  • Ред 2: arr1 е присвоен на arr2. Това ще създаде дълбоко копие на arr1 и след това ще присвои това копие на arr2
  • Редове 7 до 11: всички промени, направени в arr2, не се отразяват в arr1.

Ето какво е дълбокото копие - напълно отделни случаи. Същата концепция работи с всички типове стойности.

В някои сценарии, когато тип стойност съдържа вложени вложени референтни типове, дълбокото копиране разкрива различен вид поведение. Ще видим това в следващите раздели.

Плитко копие - Дублира се възможно най-малко

  • С плитко копие всеки обект, посочен от източника, се посочва и от местоназначението. Така само един обект ще бъде създаден в паметта.
  • Колекции - плитко копие на колекция е копие на структурата на колекцията, а не на елементите. С плитко копие две колекции сега споделят отделните елементи.
  • По-бързо - копира се само препратката.
  • Копирането на референтни типове създава плитко копие.

В горния код,

  • Редове от 1 до 8: Тип клас на адреса
  • Ред 10: a1 - екземпляр от тип Адрес
  • Ред 11: a1 е присвоен на a2. Това ще създаде плитко копие на a1 и след това ще присвои това копие на a2, тоест само препратката се копира в a2.
  • Редове 16 до 19: всички промени, направени в a2, със сигурност ще се отразят в a1.

В горната илюстрация можем да видим, че и a1 и a2 сочат към един и същ адрес на паметта.

Копиране на референтни типове дълбоко

Към момента знаем, че всеки път, когато се опитваме да копираме референтен тип, се преписва само препратката към обекта. Не е създаден нов обект. Ами ако искаме да създадем напълно отделен обект?

Можем да създадем дълбоко копие на референтния тип, използвайки метода copy (). Според документацията,

copy () - Връща обекта, върнат с копие (с :).

Това е удобен метод за класове, които приемат протокола NSCopying. Изключение се поставя, ако няма изпълнение за копиране (с :).

Нека да преструктурираме адреса клас, който създадохме в Code Snippet 2, за да съответства на протокола NSCopying.

В горния код,

  • Редове от 1 до 14: Типът адресен клас отговаря на NSCopying и реализира копиране (с метод :)
  • Ред 16: a1 - екземпляр от тип Адрес
  • Ред 17: a1 се присвоява на a2 с помощта на метод copy (). Това ще създаде дълбоко копие на a1 и след това ще присвои това копие на a2, че ще бъде създаден напълно нов обект.
  • Редове 22 до 25: всички промени, направени в a2, не се отразяват в a1.

Както е видно от горната илюстрация, a1 и a2 сочат към различни места в паметта.

Нека да разгледаме друг пример. Този път ще видим как работи с вложени референтни типове - референтен тип, съдържащ друг референтен тип.

В горния код,

  • Ред 22: дълбоко копие на p1 е присвоено на p2 чрез метода copy (). Това означава, че всяка промяна в една от тях не трябва да има ефект върху другата.
  • Редове 27 до 28: стойностите на името и града на p2 се променят. Те не трябва да се отразяват в p1.
  • Ред 30: името на p1 е според очакванията, но неговият град? Трябва да е „Мумбай“, нали? Но не можем да видим това да се случва. „Бангалор“ беше само за p2, нали? Мда ... exactly.

Дълбоко копие ...! Това не се очакваше от вас. Казахте, че ще копирате всичко. И сега се държите така. Защо о, защо ..?! Какво да правя сега?

Не изпадайте в паника. Нека да разгледаме какво трябва да кажат адресите на паметта в това.

От горната илюстрация можем да видим това

  • p1 и p2 насочват към различни места в паметта, както се очаква.
  • Но техните променливи за адреси все още сочат към едно и също място. Това означава, че дори след като ги копирате дълбоко, се преписват само препратките - тоест плитко копие разбира се.

Моля, обърнете внимание: всеки път, когато копираме референтен тип, по подразбиране се създава плитко копие, докато изрично не уточним, че то трябва да бъде копирано дълбоко.

func copy (със зона: NSZone? = nil) -> Any
{
    нека човек = Личност (собствено име, собствена адреса)
    лице за връщане
}

В горния метод, който реализирахме по-рано за класа Person, създадохме нов екземпляр, като копирахме адреса с self.address. Това ще копира само препратката към адресния обект. Това е причината адресът на p1 и p2 да насочва към едно и също място.

Така че, копирането на обекта чрез метода copy () няма да създаде истинско дълбоко копие на обекта.

За напълно дублиране на референтен обект: референтният тип заедно с всички вложени типове референции трябва да се копира с метода copy ().

нека човек = Лице (име, собствено име, адреса.copy () като? адрес)

Използвайки горния код във функцията копие (със зона: NSZone? = Nil) -> Всеки метод ще работи всичко. Можете да видите това от илюстрацията по-долу.

True Deep Copy - типове справка и стойност

Вече видяхме как можем да създадем дълбоко копие на референтните типове. Разбира се, че можем да направим това с всички вложени типове референции.

Но какво да кажем за вложен тип референтен тип в стойност, който е масив от обекти или референтен тип променлива в структура или може би кортеж? Можем ли да разрешим това и с copy ()? Не, всъщност не можем. Методът copy () изисква прилагане на протокол NSCopying, който работи само за подкласове NSObject. Типовете стойности не поддържат наследяване, така че не можем да използваме copy () с тях.

В ред 2 само структурата на arr1 се копира дълбоко, но обектите Адрес вътре в нея все още се копират плитко. Можете да видите това от картата с памет по-долу.

Елементите и в arr1 и arr2 сочат към едни и същи места в паметта. Това е поради същата причина - референтните типове се копират плитки по подразбиране.

Сериализирането и след това десериализацията на обект винаги създава съвсем нов обект. Той е валиден както за типови стойности, така и за референтните типове.

Ето някои API-та, които можем да използваме за сериализиране и де-сериализиране на данни:

  1. NSCoding - протокол, който дава възможност да се кодира обект и декодира обект за архивиране и разпространение. Той ще работи само с обекти от клас клас, тъй като изисква наследяване от NSObject.
  2. Codable - Направете вашите типове данни кодируеми и декодируеми за съвместимост с външни представителства като JSON. Той ще работи и за двата типа стойност - структура, масив, кортеж, основни типове данни, като референтни типове - клас.

Нека да преструктурираме класа на адреса малко по-нататък, за да се съобразим с протокола Codable и да премахнем всички кодове за NSCopying, които добавихме по-рано в Code Snippet 3.

В горния код редовете 11–13 ще създадат истинско дълбоко копие на arr1. По-долу е илюстрацията, която дава ясна картина на местата в паметта.

Копиране при запис

Copy on write е техника за оптимизация, която помага за повишаване на производителността при копиране на типове стойности.

Да речем, че копираме един String или Int или може би друг тип стойност - в този случай няма да се сблъскаме с никакви решаващи проблеми с ефективността. Но какво да кажем, когато копираме масив от хиляди елементи? Все още няма ли да създаде проблеми с производителността? Ами ако просто го копираме и не правим никакви промени в това копие? Не е ли тази допълнителна памет, която използвахме само в този случай?

Тук идва концепцията Copy in Write - при копиране всяка отправна точка сочи към един и същ адрес в паметта. Само когато едно от препратките променя основните данни, че Swift всъщност копира оригиналния екземпляр и прави модификацията.

Тоест, независимо дали става въпрос за дълбоко копие или плитко копие, ново копие няма да бъде създадено, докато не направим промяна в някой от обектите.

В горния код,

  • Ред 2: дълбоко копие на arr1 е присвоено на arr2
  • Редове 4 и 5: arr1 и arr2 все още сочат към един и същ адрес на паметта
  • Ред 7: промени, направени в arr2
  • Редове 9 и 10: arr1 и arr2 сега сочат към различни места в паметта

Сега знаете повече за дълбоки и плитки копия и как се държат в различни сценарии с различни типове данни. Можете да ги опитате със собствен набор от примери и да видите какви резултати получавате.

Допълнителна информация

Не забравяйте да прочетете и другите ми статии:

  1. Всичко за Codable в Swift 4
  2. Всичко, което винаги сте искали да знаете за известията в iOS
  3. Оцветете го с ГРАДИЕНТИ - iOS
  4. Кодиране за iOS 11: Как да влачите и пускате в колекции и таблици
  5. Всичко, което трябва да знаете за днешните разширения (джаджа) в iOS 10
  6. Изборът на UICollectionViewCell стана лесен .. !!

Не се колебайте да оставяте коментари, в случай че имате въпроси.