LINK LOOP И ЯЗЫК ПРОГРАММИРОВАНИЯ RUST

LINK LOOP И ЯЗЫК ПРОГРАММИРОВАНИЯ RUST

В большинстве случаев циклические ссылки воспринимаются пользователями как ошибочные выражения. Это обуславливается тем, что сама программа перегружается от их наличия, предупреждая об этом специальным оповещением. Чтобы снять ненужную нагрузку с программных процессов и устранить конфликтные ситуации между ячейками, необходимо найти проблемные места и удалить их.

Начинаем

Загрузите исходные материалы.
Откройте проект в папке Cycles/Starter
. В первой части нашего руководства, разбираясь в ключевых понятиях, мы будем заниматься исключительно файлом MainViewController.swif
t.

Добавьте этот класс внизу MainViewController.swift:

   class User {
  let name: String
  
  init(name: String) {
    self.name = name
    print("User \(name) was initialized")
  }

  deinit {
    print("Deallocating user named: \(name)")
  }
}  
  

Разместите этот код перед методом viewDidLoad()
:

   let user = User(name: "John")  
  

Запустите приложение. Сделайте консоль Xcode видимой при помощи Command-Shift-Y
, чтобы видеть вывод операторов print.

Другими словами, пока view controller, содержащий этот объект, не выйдет из области видимости, объект никогда не будет освобождён.

Время на прочтение

Будучи современным языком высокого уровня, Swift
в основном берёт на себя управление памятью в ваших приложениях, занимаясь выделением и освобождением памяти. Это происходит благодаря механизму, который называется Automatic Reference Counting
, или сокращенно ARC
. В этом руководстве вы разберётесь, как работает ARC и как правильно управлять памятью в Swift. Понимая этот механизм, вы сможете влиять на время жизни объектов, размещенных в куче ( heap
).

В этом руководстве вы прокачаете свои знания Swift и ARC, изучив следующее:

  • как работает ARC
  • что такое циклы ссылок ( reference cycles
    ) и как их правильно устранять
  • как создать пример цикла ссылок
  • как находить циклы ссылок при помощи визуальных средств, предлагаемых Xcode
  • как обращаться с ссылочными типами и типами-значениями

Что такое циклическая ссылка

Циклическая ссылка представляет собой выражение, которое через формулы, расположенные в других ячейках, ссылается на самое начало выражения.
При этом в данной цепочке может быть огромное количество ссылок, из которых образуется замкнутый круг. Чаще всего это ошибочное выражение, которое перегружает систему, не дает программе работать корректно. Однако в некоторых ситуациях циклические ссылки пользователи добавляют осознанно, чтобы выполнить определенные расчетные операции.

Если циклическая ссылка – ошибка, которую пользователь допустил случайно при заполнении таблицы, введении определенных функций, формул, необходимо найти ее и удалить. На этот случай существует несколько эффективных способов. Стоит подробно рассмотреть 2 наиболее простых и проверенных на практике.

Важно!
Раздумывать о том есть ли в таблице циклические ссылки или нет не нужно. Если подобные конфликтные ситуации присутствуют, современные версии Excel сразу уведомляют пользователя об этом предупреждающим окном с соответствующей информацией.

ciklicheskaya-ssylka-v-excel-kak-najti-i-udalit-2-sposoba


Окно оповещения о наличии циклических ссылок в таблице

Удерживаем мобилу

   private(set) var phones: [Phone] = []

func add(phone: Phone) {
  phones.append(phone)
  phone.owner = self
}
  
  

Теперь добавим это в конце runScenario():

   user.add(phone: iPhone)  
  

LINK LOOP И ЯЗЫК ПРОГРАММИРОВАНИЯ RUST

Циклы ссылок в замыканиях

Циклы ссылок применительно к объектам возникают, когда у объектов есть свойства, ссылающиеся друг на друга. Как и объекты, замыкания — это ссылочный тип, и могут приводить к циклам ссылок. Замыкания «захватывают» (capture) объекты, которые используют.

Например, если вы присвоите замыкание свойству класса, и это замыкание использует свойства того же класса, то у нас появляется цикл ссылок. Другими словами, объект держит ссылку на замыкание через свойство. Замыкание содержит ссылку на объект через захваченное значение self.

LINK LOOP И ЯЗЫК ПРОГРАММИРОВАНИЯ RUST

   lazy var completePhoneNumber: () -> String = {
  self.countryCode + " " + self.number
}
  
  

Это замыкание вычисляет и возвращает полный телефонный номер. Свойство объявлено как lazy
, оно будет присвоено при первом использовании.

Это необходимо так как оно использует self.countryCode и self.number, которые будут недоступны до выполнения кода инициалайзера.

Добавьте в конец runScenario():

   print(subscription.completePhoneNumber())  
  

Вызов completePhoneNumber() приведет к исполнению замыкания.

LINK LOOP И ЯЗЫК ПРОГРАММИРОВАНИЯ RUST

Захватите себя

Замените определение completePhoneNumber в CarrierSubscription::

   lazy var completePhoneNumber: () -> String = { [unowned self] in
  return self.countryCode + " " + self.number
}  
  

Запустите приложение и вы увидите, что теперь CarrierSubscription высвобождается.

На самом деле приведённый выше синтаксис — это короткая форма более длинного и полного, в котором появляется новая переменная:

   var closure = { [unowned newID = self] in
  // Use unowned newID here...
}  
  

Тут newID — это unowned копия self. Вне замыкания self остается самим собой. В короткой форме, приведенной ранее, мы создаём новую переменную self
, которая затеняет существующий self внутри замыкания.

Ссылки Unowned

Существует также другой модификатор ссылки, который не приводит к увеличению счётчика ссылок: unowned
.

В чём же отличие unowned
от weak
? Ссылка weak всегда optional и автоматически становится nil, когда ссылаемый объект высвобождается.

Вот почему мы должны объявлять weak свойства как переменную optional типа: это свойство должно измениться.

Ссылки Unowned, напротив, никогда не optional. Если вы попробуете получить доступ к unowned свойству, которое ссылается на освобождённый объект, вы получите ошибку, похожую на принудительное разворачивание содержащую nil переменной (force unwrapping).

LINK LOOP И ЯЗЫК ПРОГРАММИРОВАНИЯ RUST

Давайте попробуем применить unowned
.

Добавим новый класс CarrierSubscription
в конце MainViewController.swift:

   class CarrierSubscription {
  let name: String
  let countryCode: String
  let number: String
  let user: User
              
  init(name: String, countryCode: String, number: String, user: User) {
    self.name = name
    self.countryCode = countryCode
    self.number = number
    self.user = user
    
    print("CarrierSubscription \(name) is initialized")
  }

  deinit {
    print("Deallocating CarrierSubscription named: \(name)")
  }
}          
  

У CarrierSubscription четыре свойства:

Циклы с ссылочными типами и типами-значениями.

В Swift есть ссылочные типы (классы и замыкания) и типы-значения (структуры, перечисления). Тип-значение копируется при его передаче, а ссылочные типы делят одно значение при помощи ссылки.

Это значит, что в случае типов-значений циклов не может быть. Для возникновения цикла нам нужно как минимум 2 ссылочных типа.

Вернёмся к проекту Cycles project и добавим этот код в конце MainViewController.swift:

   struct Node { // Error
  var payload = 0
  var next: Node?
}
  
  

Не выйдет! Структура — тип значение и не может иметь рекурсию на экземпляр самой себя. В противном случае, у такой структуры был бы бесконечный размер.

Изменим структуру на класс.

   class Node {
  var payload = 0
  var next: Node?
}  
  

Ссылка на себя вполне допустима для классов (ссылочный тип), так что проблем у компилятора не возникает.

Теперь добавим это в конце MainViewController.swift:

   class Person {
  var name: String
  var friends: [Person] = []
  init(name: String) {
    self.name = name
    print("New person instance: \(name)")
  }

  deinit {
    print("Person instance \(name) is being deallocated")
  }
}  
  

А это — в конце runScenario():

   do {
  let ernie = Person(name: "Ernie")
  let bert = Person(name: "Bert")
  
  ernie.friends.append(bert) // Not deallocated
  bert.friends.append(ernie) // Not deallocated
}
  
  

Запустите приложение. Обратите внимание: ни ernie, ни bert не высвобождены.

Где же течёт?

При запущенном приложении кликните на кнопке Debug Memory Graph:

LINK LOOP И ЯЗЫК ПРОГРАММИРОВАНИЯ RUST

Понаблюдайте за Runtime Issues в Debug navigator. Они отмечены пурпурными квадратами с белым восклицательным знаком внутри:

LINK LOOP И ЯЗЫК ПРОГРАММИРОВАНИЯ RUST

Выберите в навигаторе один из проблемных Contact объектов. Цикл чётко виден: объекты Contact и Number ссылаясь друг на друга, удерживают.

LINK LOOP И ЯЗЫК ПРОГРАММИРОВАНИЯ RUST

Похоже, вам пора заглянуть в код. Учитывайте, что контакт может существовать без номера, но не наоборот.

Как бы вы разрешили этот цикл? Ссылка из Contact на Number или из Number на Contact? weak или unowned? Попробуйте сначала сами!

Есть 2 возможных решения: или сделать ссылку из Contact на Number weak, или из Number на Contact unowned.

Документация Apple рекомендует, чтобы родительский объект владел сильной ссылкой на «детский» — не наоборот. Это означает, что мы даем Contact сильную ссылку на Number, а Number — unowned ссылку на Contact:

   class Number {
  unowned var contact: Contact
  // Other code...
}

class Contact {
  var number: Number?
  // Other code...
}  
  

Ссылка и значение

Это пример сочетания ссылочного типа и типа-значения, которое привело к циклу ссылок.

ernie и bert остаются невысвобожденными, держа друг друга в своих массивах друзей, хотя массивы сами по себе — типы-значения.

Попробуйте сделать архив friends как unowned, и Xcode покажет ошибку: unowned применим только к классам.

Чтобы пофиксить этот цикл, нам придётся создать объект-обёртку и использовать его для добавления экземпляров в массив.

Добавьте следующее определение перед классом Person:

   class Unowned<T: AnyObject> {
  unowned var value: T
  init (_ value: T) {
    self.value = value
  }
}
  
  

Затем измените определение friends в классе Person:

   var friends: [Unowned<Person>] = []  
  

Наконец, замените содержимое блока do в runScenario():

   do {
  let ernie = Person(name: "Ernie")
  let bert = Person(name: "Bert")
  
  ernie.friends.append(Unowned(bert))
  bert.friends.append(Unowned(ernie))
}  
  

Запустите приложение, теперь ernie и bert корректно высвобождаются!

Массив friends больше не является коллекцией объектов Person. Теперь это коллекция объектов класса Unowned
, которые служат обёрткой для экземпляров Person.

Чтобы получить объекты Person из Unowned, используйте свойство value:

   let firstFriend = bert.friends.first?.value // get ernie   
  

Разрываем цепь

Пользователь владеет подпиской на провайдера, но наоборот — нет, подписка на провайдера не владеет пользователем.

Более того, нет никакого смысла в существовании CarrierSubscription без привязки к владеющему ей пользователю.

Таким образом, ссылка на пользователя должна быть unowned.

   unowned let user: User
  
  

LINK LOOP И ЯЗЫК ПРОГРАММИРОВАНИЯ RUST

Использование инструментов программы

В тех случаях, когда стрелки трассировки не указывают на проблемные места в таблице, необходимо воспользоваться встроенными инструментами Excel для поиска и удаления циклических ссылок. Порядок действий:

  1. В первую очередь нужно закрыть окно с предупреждением.
  2. Перейти на вкладку «Формулы» на основной панели инструментов.
  3. Зайти в раздел «Зависимости формул».
  4. Найти кнопку «Проверка ошибок». Если окно программы находится в сжатом формате, данная кнопка будет обозначена восклицательным знаком. Рядом с ней должен находиться маленький треугольник, который направлен вниз. Нужно нажать на него, чтобы появился список команд.
ciklicheskaya-ssylka-v-excel-kak-najti-i-udalit-2-sposoba


Меню для отображения всех циклических ссылок с их координатами по ячейкам
  1. Из списка выбрать «Циклические ссылки».
  2. Выполнив все описанные выше действия, перед пользователем появится полный список с ячейками, которые содержат циклические ссылки. Для того чтобы понять, где точно находится данная клетка, нужно найти ее в списке, кликнуть по ней левой кнопкой мыши. Программа автоматически перенаправит пользователя в то место, где возник конфликт.
  3. Далее необходимо исправить ошибку для каждой проблемной ячейки, как описывалось в первом способе. Когда конфликтные координаты будут удалены из всех формул, которые есть в списке ошибок, необходимо выполнить заключительную проверку. Для этого возле кнопки «Проверка ошибок» нужно открыть список команд. Если пункт «Циклические ссылки» не будет показан как активный – ошибок нет.
ciklicheskaya-ssylka-v-excel-kak-najti-i-udalit-2-sposoba


Если ошибок нет, пункт поиска циклических ссылок выбрать нельзя

Он в области видимости?

   func runScenario() {
  let user = User(name: "John")
}    
  
  

Теперь вызовем runScenario() добавив это в конце viewDidLoad():

   runScenario()  
  

Запустите приложение. Вывод в консоли теперь выглядит так:

Это означает, что вы высвободили объект, покинувший области видимости.

Язык программирования Rust

Ссылочные зацикливания могут приводить к утечке памяти

Гарантии безопасности памяти в Rust затрудняют, но не делают невозможным случайное выделение памяти, которое никогда не очищается (известное как утечка памяти
). Полное предотвращение утечек памяти не является одной из гарантий Rust, а это означает, что утечки памяти безопасны в Rust. Мы видим, что Rust допускает утечку памяти с помощью Rc<T>
и RefCell<T>
: можно создавать ссылки, в которых элементы ссылаются друг на друга в цикле. Это создаёт утечки памяти, потому что счётчик ссылок каждого элемента в цикле никогда не достигнет 0, а значения никогда не будут удалены.

Создание ссылочного зацикливания

Давайте посмотрим, как может произойти ситуация ссылочного зацикливания и как её предотвратить, начиная с определения перечисления List
и метода tail
в листинге 15-25:

   
   use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
enum List {
    Cons(i32, RefCell<Rc<List>>),
    Nil,
}

impl List {
    fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_, item) => Some(item),
            Nil => None,
        }
    }
}

fn main() {}
  
  

Листинг 15-25: Объявление cons list, который содержит RefCell<T>
, чтобы мы могли изменять то, на что ссылается экземпляр Cons

Мы используем другую вариацию определения List
из листинга 15-5. Второй элемент в варианте Cons
теперь RefCell<Rc<List>>
, что означает, что вместо возможности менять значение i32
, как мы делали в листинге 15-24, мы хотим менять значение List
, на которое указывает вариант Cons
. Мы также добавляем метод tail
, чтобы нам было удобно обращаться ко второму элементу, если у нас есть вариант Cons
.

В листинге 15-26 мы добавляем main
функцию, которая использует определения листинга 15-25. Этот код создаёт список в переменной a
и список b
, который указывает на список a
. Затем он изменяет список внутри a
так, чтобы он указывал на b
, создавая ссылочное зацикливание. В коде есть инструкции println!
, чтобы показать значения счётчиков ссылок в различных точках этого процесса.

   
   

            Cons(_, item) => Some(item),
            Nil => None,

fn main() {
    let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));

    println!("a initial rc count = {}", Rc::strong_count(&a));
    println!("a next item = {:?}", a.tail());

    let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));

    println!("a rc count after b creation = {}", Rc::strong_count(&a));
    println!("b initial rc count = {}", Rc::strong_count(&b));
    println!("b next item = {:?}", b.tail());

    if let Some(link) = a.tail() {
        *link.borrow_mut() = Rc::clone(&b);
    }

    println!("b rc count after changing a = {}", Rc::strong_count(&b));
    println!("a rc count after changing a = {}", Rc::strong_count(&a));

    // Uncomment the next line to see that we have a cycle;
    // it will overflow the stack
    // println!("a next item = {:?}", a.tail());
}
  
  

Листинг 15-26: Создание ссылочного цикла из двух значений List
, указывающих друг на друга

Мы создаём экземпляр Rc<List>
содержащий значение List
в переменной a
с начальным списком 5, Nil
. Затем мы создаём экземпляр Rc<List>
содержащий другое значение List
в переменной b
, которое содержит значение 10 и указывает на список в a
.

Мы меняем a
так, чтобы он указывал на b
вместо Nil
, создавая зацикленность. Мы делаем это с помощью метода tail
, чтобы получить ссылку на RefCell<Rc<List>>
из переменной a
, которую мы помещаем в переменную link
. Затем мы используем метод borrow_mut
из типа RefCell<Rc<List>>
, чтобы изменить внутреннее значение типа Rc<List>
, содержащего начальное значение Nil
на значение типа Rc<List>
взятое из переменной b
.

Когда мы запускаем этот код, оставив последний println!
закомментированным в данный момент, мы получим вывод:

   $ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished dev [unoptimized + debuginfo] target(s) in 0.53s
     Running `target/debug/cons-list`
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2
  
  

Количество ссылок на экземпляры Rc<List>
как в a
, так и в b
равно 2 после того, как мы заменили список в a
на ссылку на b
. В конце main
Rust уничтожает переменную b
, что уменьшает количество ссылок на Rc<List>
из b
с 2 до 1. Память, которую Rc<List>
занимает в куче, не будет освобождена в этот момент, потому что количество ссылок на неё равно 1, а не 0. Затем Rust удаляет a
, что уменьшает количество ссылок экземпляра Rc<List>
в a
с 2 до 1. Память этого экземпляра также не может быть освобождена, поскольку другой экземпляр Rc<List>
по-прежнему ссылается на него. Таким образом, память, выделенная для списка не будет освобождена никогда. Чтобы наглядно представить этот цикл ссылок, мы создали диаграмму на рисунке 15-4.

Reference cycle of lists

Рисунок 15-4: Ссылочный цикл списков a
и b
, указывающих друг на друга

По сравнению с реальной программой, последствия создания цикла ссылок в этом примере не так страшны: сразу после создания цикла ссылок программа завершается. Однако если более сложная программа выделит много памяти в цикле и будет удерживать её в течение длительного времени, программа будет потреблять больше памяти, чем ей нужно, и может перенапрячь систему, что приведёт к исчерпанию доступной памяти.

Вызвать образование ссылочной зацикленности не просто, но и не невозможно. Если у вас есть значения RefCell<T>
которые содержат значения Rc<T>
или аналогичные вложенные комбинации типов с внутренней изменчивостью и подсчётом ссылок, вы должны убедиться, что вы не создаёте зацикленность; Вы не можете полагаться на то, что Rust их обнаружит. Создание ссылочной зацикленности являлось бы логической ошибкой в программе, для которой вы должны использовать автоматические тесты, проверку кода и другие практики разработки программного обеспечения для её минимизации.

Другое решение для избежания ссылочной зацикленности — это реорганизация ваших структур данных, чтобы некоторые ссылки выражали владение, а другие — отсутствие владения. В результате можно иметь циклы, построенные на некоторых отношениях владения и некоторые не основанные на отношениях владения, тогда только отношения владения влияют на то, можно ли удалить значение. В листинге 15-25 мы всегда хотим, чтобы варианты Cons
владели своим списком, поэтому реорганизация структуры данных невозможна. Давайте рассмотрим пример с использованием графов, состоящих из родительских и дочерних узлов, чтобы увидеть, когда отношения владения не являются подходящим способом предотвращения ссылочной зацикленности.

Предотвращение ссылочной зацикленности: замена умного указателя Rc<T>
на Weak<T>

До сих пор мы демонстрировали, что вызов Rc::clone
увеличивает strong_count
экземпляра Rc<T>
, а экземпляр Rc<T>
удаляется, только если его strong_count
равен 0. Вы также можете создать слабую ссылку
на значение внутри экземпляра Rc<T>
, вызвав Rc::downgrade
и передав ссылку на Rc<T>
. Сильные ссылки — это то с помощью чего вы можете поделиться владением экземпляра Rc<T>
. Слабые ссылки не отражают связи владения, и их подсчёт не влияет на то, когда экземпляр Rc<T>
будет очищен. Они не приведут к ссылочному циклу, потому что любой цикл, включающий несколько слабых ссылок, будет разорван, как только количество сильных ссылок для задействованных значений станет равным 0.

Когда вы вызываете Rc::downgrade
, вы получаете умный указатель типа Weak<T>
. Вместо того чтобы увеличить strong_count
в экземпляре Rc<T>
на 1, вызов Rc::downgrade
увеличивает weak_count
на 1. Тип Rc<T>
использует weak_count
для отслеживания количества существующих ссылок Weak<T>
, аналогично strong_count
. Разница в том, что weak_count
не должен быть равен 0, чтобы экземпляр Rc<T>
мог быть удалён.

Поскольку значение, на которое ссылается Weak<T>
могло быть удалено, то необходимо убедиться, что это значение все ещё существует, чтобы сделать что-либо со значением на которое указывает Weak<T>
. Делайте это вызывая метод upgrade
у экземпляра типа Weak<T>
, который вернёт Option<Rc<T>>
. Вы получите результат Some
, если значение Rc<T>
ещё не было удалено и результат None
, если значение Rc<T>
было удалено. Поскольку upgrade
возвращает тип Option<T>
, Rust обеспечит обработку обоих случаев Some
и None
и не будет некорректного указателя.

В качестве примера, вместо того чтобы использовать список чей элемент знает только о следующем элементе, мы создадим дерево, чьи элементы знают о своих дочерних элементах и
о своих родительских элементах.

Создание древовидной структуры данных: Node
с дочерними узлами

Для начала мы построим дерево с узлами, которые знают о своих дочерних узлах. Мы создадим структуру с именем Node
, которая будет содержать собственное значение i32
, а также ссылки на его дочерние значения Node
:

use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
 value: i32,
 children: RefCell<Vec<Rc<Node>>>,
}
    
    
    
    

Мы хотим, чтобы Node
владел своими дочерними узлами и мы хотим поделиться этим владением с переменными так, чтобы мы могли напрямую обращаться к каждому Node
в дереве. Для этого мы определяем внутренние элементы типа Vec<T>
как значения типа Rc<Node>
. Мы также хотим изменять те узлы, которые являются дочерними по отношению к другому узлу, поэтому у нас есть тип RefCell<T>
в поле children
оборачивающий тип Vec<Rc<Node>>
.

Далее мы будем использовать наше определение структуры и создадим один экземпляр Node
с именем leaf
со значением 3 и без дочерних элементов, а другой экземпляр с именем branch
со значением 5 и leaf
в качестве одного из его дочерних элементов, как показано в листинге 15-27:

    
    
    
    
fn main() {
 let leaf = Rc::new(Node {
 value: 3,
 children: RefCell::new(vec![]),
 });

 let branch = Rc::new(Node {
 value: 5,
 children: RefCell::new(vec![Rc::clone(&leaf)]),
 });
}

Листинг 15-27: Создание узла leaf
без дочерних элементов и узла branch
с leaf
в качестве одного из дочерних элементов

Мы клонируем содержимое Rc<Node>
из переменной leaf
и сохраняем его в переменной branch
, что означает, что Node
в leaf
теперь имеет двух владельцев: leaf
и branch
. Мы можем получить доступ из branch
к leaf
через обращение branch.children
, но нет способа добраться из leaf
к branch
. Причина в том, что leaf
не имеет ссылки на branch
и не знает, что они связаны. Мы хотим, чтобы leaf
знал, что branch
является его родителем. Мы сделаем это далее.

Добавление ссылки от ребёнка к его родителю

Для того, чтобы дочерний узел знал о своём родительском узле нужно добавить поле parent
в наше определение структуры Node
. Проблема в том, чтобы решить, каким должен быть тип parent
. Мы знаем, что он не может содержать Rc<T>
, потому что это создаст ссылочную зацикленность с leaf.parent
указывающей на branch
и branch.children
, указывающей на leaf
, что приведёт к тому, что их значения strong_count
никогда не будут равны 0.

Подумаем об этих отношениях по-другому, родительский узел должен владеть своими потомками: если родительский узел удаляется, его дочерние узлы также должны быть удалены. Однако дочерний элемент не должен владеть своим родителем: если мы удаляем дочерний узел то родительский элемент все равно должен существовать. Это случай для использования слабых ссылок!

Поэтому вместо Rc<T>
мы сделаем так, чтобы поле parent
использовало тип Weak<T>
, а именно RefCell<Weak<Node>>
. Теперь наше определение структуры Node
выглядит так:

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
 value: i32,
 parent: RefCell<Weak>,
 children: RefCell<Vec<Rc>>,
}
    
    
    
    
    
    
    
    
    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);
    
    
    

Узел сможет ссылаться на свой родительский узел, но не владеет своим родителем. В листинге 15-28 мы обновляем main
на использование нового определения так, чтобы у узла leaf
был бы способ ссылаться на его родительский узел branch
:

    
    
    
    
fn main() {
 let leaf = Rc::new(Node {
 value: 3,
 parent: RefCell::new(Weak::new()),
 children: RefCell::new(vec![]),
 });

 println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

 let branch = Rc::new(Node {
 value: 5,
 parent: RefCell::new(Weak::new()),
 children: RefCell::new(vec![Rc::clone(&leaf)]),
 });

 *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

 println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}

Листинг 15-28: Узел leaf
со слабой ссылкой на его родительский узел branch

Создание узла leaf
выглядит аналогично примеру из Листинга 15-27, за исключением поля parent
: leaf
изначально не имеет родителя, поэтому мы создаём новый, пустой экземпляр ссылки Weak<Node>
.

На этом этапе, когда мы пытаемся получить ссылку на родительский узел у узла leaf
с помощью метода upgrade
, мы получаем значение None
. Мы видим это в выводе первого println!
выражения:

   leaf parent = None
  
  

Когда мы создаём узел branch
у него также будет новая ссылка типа Weak<Node>
в поле parent
, потому что узел branch
не имеет своего родительского узла. У нас все ещё есть leaf
как один из потомков узла branch
. Когда мы получили экземпляр Node
в переменной branch
, мы можем изменить переменную leaf
чтобы дать ей Weak<Node>
ссылку на её родителя. Мы используем метод borrow_mut
у типа RefCell<Weak<Node>>
поля parent
у leaf
, а затем используем функцию Rc::downgrade
для создания Weak<Node>
ссылки на branch
из Rc<Node>
в branch
.

Когда мы снова напечатаем родителя leaf
то в этот раз мы получим вариант Some
содержащий branch
, теперь leaf
может получить доступ к своему родителю! Когда мы печатаем leaf
, мы также избегаем цикла, который в конечном итоге заканчивался переполнением стека, как в листинге 15-26; ссылки типа Weak<Node>
печатаются как (Weak)
:

   leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
children: RefCell { value: [] } }] } })
  
  

Отсутствие бесконечного вывода означает, что этот код не создал ссылочной зацикленности. Мы также можем сказать это, посмотрев на значения, которые мы получаем при вызове Rc::strong_count
и Rc::weak_count
.

Визуализация изменений в strong_count
и weak_count

Давайте посмотрим, как изменяются значения strong_count
и weak_count
экземпляров типа Rc<Node>
с помощью создания новой внутренней области видимости и перемещая создания экземпляра branch
в эту область. Таким образом можно увидеть, что происходит, когда branch
создаётся и затем удаляется при выходе из области видимости. Изменения показаны в листинге 15-29:

    
    
    
    
fn main() {
 let leaf = Rc::new(Node {
 value: 3,
 parent: RefCell::new(Weak::new()),
 children: RefCell::new(vec![]),
 });

 println!(
 "leaf strong = {}, weak = {}",
 Rc::strong_count(&leaf),
 Rc::weak_count(&leaf),
 );

 {
 let branch = Rc::new(Node {
 value: 5,
 parent: RefCell::new(Weak::new()),
 children: RefCell::new(vec![Rc::clone(&leaf)]),
 });

 *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

 println!(
 "branch strong = {}, weak = {}",
 Rc::strong_count(&branch),
 Rc::weak_count(&branch),
 );

 println!(
 "leaf strong = {}, weak = {}",
 Rc::strong_count(&leaf),
 Rc::weak_count(&leaf),
 );
 }

 println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
 println!(
 "leaf strong = {}, weak = {}",
 Rc::strong_count(&leaf),
 Rc::weak_count(&leaf),
 );
}

Листинг 15-29: Создание branch
во внутренней области видимости и подсчёт сильных и слабых ссылок

После того, как leaf
создан его Rc<Node>
имеет значения strong count равное 1 и weak count равное 0. Во внутренней области мы создаём branch
и связываем её с leaf
, после чего при печати значений счётчиков Rc<Node>
в branch
они будет иметь strong count 1 и weak count 1 (для leaf.parent
указывающего на branch
с Weak<Node>
). Когда мы распечатаем счётчики из leaf
, мы увидим, что они будут иметь strong count 2, потому что branch
теперь имеет клон Rc<Node>
переменной leaf
хранящийся в branch.children
, но все равно будет иметь weak count 0.

Когда заканчивается внутренняя область видимости, branch
выходит из области видимости и strong count Rc<Node>
уменьшается до 0, поэтому его Node
удаляется. Weak count 1 из leaf.parent
не имеет никакого отношения к тому, был ли Node
удалён, поэтому не будет никаких утечек памяти!

Если мы попытаемся получить доступ к родителю переменной leaf
после окончания области видимости, мы снова получим значение None
. В конце программы Rc<Node>
внутри leaf
имеет strong count 1 и weak count 0 потому что переменная leaf
снова является единственной ссылкой на Rc<Node>
.

Вся логика, которая управляет счётчиками и сбросом их значений, встроена внутри Rc<T>
и Weak<T>
и их реализаций типажа Drop
. Указав, что отношение из дочернего к родительскому элементу должно быть ссылкой типа Weak<T>
в определении Node
, делает возможным иметь родительские узлы, указывающие на дочерние узлы и наоборот, не создавая ссылочной зацикленности и утечек памяти.

Итоги

В этой главе рассказано как использовать умные указатели для обеспечения различных гарантий и компромиссов по сравнению с обычными ссылками, которые Rust использует по умолчанию. Тип Box<T>
имеет известный размер и указывает на данные размещённые в куче. Тип Rc<T>
отслеживает количество ссылок на данные в куче, поэтому данные могут иметь несколько владельцев. Тип RefCell<T>
с его внутренней изменяемостью предоставляет тип, который можно использовать при необходимости неизменного типа, но необходимости изменить внутреннее значение этого типа; он также обеспечивает соблюдение правил заимствования во время выполнения, а не во время компиляции.

Мы обсудили также типажи Deref
и Drop
, которые обеспечивают большую функциональность умных указателей. Мы исследовали ссылочную зацикленность, которая может вызывать утечки памяти и как это предотвратить с помощью типа Weak<T>
.

Если эта глава вызвала у вас интерес и вы хотите реализовать свои собственные умные указатели, обратитесь к «The Rustonomicon»
за более полезной информацией.

Далее мы поговорим о параллелизме в Rust. Вы даже узнаете о нескольких новых умных указателях.

Проверим наши ссылки

   class Phone {
  let model: String
  var owner: User?
  
  init(model: String) {
    self.model = model
    print("Phone \(model) was initialized")
  }

  deinit {
    print("Deallocating phone named: \(model)")
  }
}      
  

Этот код добавляет новый класс Phone
с двумя свойствами, одно для модели и другое для владельца, а также методы init и deinit. Свойство владельца опциональное, так как у телефона может и не быть владельца.

Теперь добавьте эту строчку в runScenario():

   let iPhone = Phone(model: "iPhone Xs")
  
  

Это создаст экземпляр класса Phone.

Ссылки Weak

Чтобы разорвать цикл сильных ссылок, вы можете обозначить отношение между объектами как слабое ( weak
).

По умолчанию все ссылки являются сильными и присваивание приводит к увеличению счётчика ссылок. При использовании слабых ссылок (weak references) счётчик ссылок не увеличивается.

Другими словами, слабые ссылки не влияют на управление жизнью объекта
. Слабые ссылки всегда объявлены как optional
. Таким образом, когда счётчик ссылок станет равным 0, ссылка может быть установлена в nil.

LINK LOOP И ЯЗЫК ПРОГРАММИРОВАНИЯ RUST

На этой иллюстрации штриховые линии обозначают слабые ссылки. Обратите внимание, что счётчик ссылок object1 равен 1, так как на него ссылается variable1. Счётчик ссылок object2 равен 2, так как на него ссылается variable2 и object1.

object2 также ссылается на object1, но СЛАБО
, что означает, что это не влияет на счетчик ссылок на object1.

Когда variable1 и variable2 освобождаются, у object1 счётчик ссылок становится равным 0, что высвобождает его. Это, в свою очередь, освобождает сильную ссылку на object2, что приводит уже к его высвобождению.

В классе Phone измените объявление свойства owner следующим образом:

   weak var owner: User?  
  

LINK LOOP И ЯЗЫК ПРОГРАММИРОВАНИЯ RUST

Кто ваш провайдер?

   var subscriptions: [CarrierSubscription] = []  
  

Здесь мы держим массив провайдеров пользователя.

Теперь добавьте это в класс Phone class, после свойства owner:

   var carrierSubscription: CarrierSubscription?

func provision(carrierSubscription: CarrierSubscription) {
  self.carrierSubscription = carrierSubscription
}

func decommission() {
  carrierSubscription = nil
}  
  

Это добавляет опциональное свойство CarrierSubscription и два метода для регистрации и разрегистрации телефона у провайдера.

Теперь добвьте внутри метода init у класса CarrierSubscription, прямо перед оператором print:

   user.subscriptions.append(self)  
  

Мы добавляем CarrierSubscription в массив провайдеров пользователя.

И, наконец, добавьте это в конце метода runScenario():

   let subscription = CarrierSubscription(
  name: "TelBel", 
  countryCode: "0032",
  number: "31415926", 
  user: user)
iPhone.provision(carrierSubscription: subscription)  
  

Мы создаем подписку на провайдера для пользователя и подключаем к ней телефон.

Запустите приложение. В консоли вы увидите:

Сможете найти проблему?

LINK LOOP И ЯЗЫК ПРОГРАММИРОВАНИЯ RUST

Отключение блокировки и создание циклических ссылок

Разобравшись с тем, как находить и исправлять циклические ссылки в таблицах Excel, необходимо рассмотреть ситуации, когда данные выражения можно использовать с пользой. Однако перед этим нужно узнать, как отключить автоматическую блокировку подобных ссылок.

Чаще всего циклические ссылки осознанно применяются во время построения экономических моделей, для выполнения итеративных вычислений. Однако, даже если подобное выражение используется осознанно, программа все равно будет блокировать его в автоматическом режиме. Для запуска выражения в работу, необходимо отключить блокировку.
Для этого нужно выполнить несколько действий:

  1. Зайти во вкладку «Файл» на главной панели.
  2. Выбрать пункт «Параметры».
  3. Перед пользователем должно появиться окно настройки Excel. Из меню в левой части выбрать вкладку «Формулы».
  4. Перейти к разделу «Параметры вычислений». Установить галочку напротив функции «Включить итеративные вычисления». Дополнительно к этому в свободных полях чуть ниже можно установить максимальное количество подобных вычислений, допустимую погрешность.

Важно!
Без особой необходимости лучше не изменять максимальное количество итеративных вычислений. Если их будет слишком много, программа будет перегружена, могут появиться сбои с ее работой.

ciklicheskaya-ssylka-v-excel-kak-najti-i-udalit-2-sposoba


Окно настроек блокиратора циклических ссылок, их допустимого количества в документе
  1. Чтобы изменения вступили в силу, необходимо нажат на кнопку «ОК». После этого программа перестанет автоматически блокировать вычисления в ячейках, которые связаны циклическими ссылками.

Самый простой вариант создания циклической ссылки – выделить любую клетку таблицы, в нее вписать знак «=», сразу после которого добавить координаты этой же ячейки. Чтобы усложнить задачу, расширить циклическую ссылку на несколько ячеек, нужно выполнить следующий порядок действий:

  1. В клетку А1 добавить цифру «2».
  2. В ячейку В1 вписать значение «=С1».
  3. В клетку С1 добавить формулу «=А1».
  4. Останется вернуться в самую первую ячейку, через нее сослаться на клетку В1. После этого цепь из 3 ячеек замкнется.

Визуальный поиск

Самый простой метод поиска, который подойдет при проверке небольших таблиц. Порядок действий:

  1. Когда появится предупреждающее окно, необходимо закрыть его нажатием кнопки «ОК».
  2. Программа автоматически обозначит те ячейки, между которыми возникла конфликтная ситуация. Они будет подсвечены специальной стрелкой трассировки.
ciklicheskaya-ssylka-v-excel-kak-najti-i-udalit-2-sposoba


Обозначение проблемных ячеек стрелкой трассировки
  1. Чтобы убрать цикличность, необходимо зайти в обозначенную ячейку и исправить формулу. Для этого необходимо убрать координаты конфликтной клетки из общей формулы.
  2. Останется перевести курсор мыши на любую свободную ячейку таблицы, нажать ЛКМ. Циклическая ссылка будет удалена.
ciklicheskaya-ssylka-v-excel-kak-najti-i-udalit-2-sposoba


Исправленный вариант после удаления циклической ссылки

Ищем циклы ссылок в Xcode 10

Теперь, когда вы понимаете принципы работы ARC, что такое циклы ссылок и как их разрывать, пришло время посмотреть пример реального приложения.

Откройте проект Starter, находящийся в папке Contacts.

                                                                                                                                                     
                                                                                                                                                     

Это простейший менеджер контактов. Попробуйте кликнуть на контакте, добавьте пару новых. var subscriptions: [CarrierSubscription] = []

ContactsTableViewController: показывает все контакты.

DetailViewController: показывает подробную информацию выбранного контакта.

NewContactViewController: позволяет добавить новый контакт.

ContactTableViewCell: ячейка таблицы, показывающая детали контакта.

Contact: модель контакта.

Number: модель номера телефона.

Однако, с этим проектом всё плохо: тут затаился цикл ссылок. Пользователи сначала не заметят проблем по причине небольшого размера утекающей памяти, по этой же причине сложно найти место утечки.

К счастью, в Xcode 10 есть встроенные средства, чтобы найти мельчайшую утечку памяти.

Запустите снова приложение. Удалите 3-4 контакта при помощи свайпа влево и кнопки delete. Похоже, что они исчезают совсем, да?

LINK LOOP И ЯЗЫК ПРОГРАММИРОВАНИЯ RUST

Разминируем ловушку

Замените greetingMaker в классе WWDCGreeting таким образом:

   lazy var greetingMaker: () -> String = { [weak self] in
  return "Hello \(self?.who)."
}  
  

Мы сделали две вещи: во-первых, мы заменили unowned на weak. Во-вторых, так как self стал weak, мы получаем доступ к свойству who через self?.who. Игнорируйте предупреждение Xcode, мы его скоро исправим.

Приложение больше не крашится, но, если его запустить, мы получим забавный результат: “Hello nil.”

Возможно, полученный результат вполне приемлем, но часто нам нужно сделать что-то, если объект был освобождён. Это можно сделать при помощи оператора guard.

Замените текст замыкания этим:

   lazy var greetingMaker: () -> String = { [weak self] in
  guard let self = self else {
    return "No greeting available."
  }
  return "Hello \(self.who)."
}
  
  

Оператор guard присваивает self, взятое из weak self. Если self — nil, замыкание возвращает “No greeting available.” В противном случае, self становится сильной ссылкой, так что объект гарантированно доживёт до конца выполнения замыкания.

Время жизни объекта

Существование объекта делится на пять этапов:

  • выделение памяти: из стека или из кучи
  • инициализация: выполняется код внутри init
  • использование
  • деинициализация: выполняется код внутри deinit
  • высвобождение памяти: выделенная память возвращается в стек или кучу

Не существует прямого способа чтобы отследить этапы выделения и освобождения памяти, но можно использовать код внутри init и deinit.

Счётчик ссылок ( reference counts
), также известный как ‘количество использований’ ( usage counts
), определяет, когда объект больше не нужен. Этот счётчик показывает число тех, кто «пользуется» этим объектом. Объект становится ненужным, когда счётчик использований равен нулю. Затем объект деинициализируется и высвобождается.

LINK LOOP И ЯЗЫК ПРОГРАММИРОВАНИЯ RUST

Используйте Unowned осторожно

В вашем коде отношения между self и completePhoneNumber обозначены как unowned.

Если вы уверены, что объект, используемый в замыкании, не высвободится, можете использовать unowned. Если он всё-таки высвободится, вы в беде!

Добавьте этот код в конце MainViewController.swift:

   class WWDCGreeting {
  let who: String
  
  init(who: String) {
    self.who = who
  }

  lazy var greetingMaker: () -> String = { [unowned self] in
    return "Hello \(self.who)."
  }
}
  
  

Теперь вот это в конце runScenario():

   let greetingMaker: () -> String

do {
  let mermaid = WWDCGreeting(who: "caffeinated mermaid")
  greetingMaker = mermaid.greetingMaker
}

print(greetingMaker()) // ЛОВУШКА!          
  

Запустите приложение и вы увидите аварийное завершение и что-такое в консоли:

Исключение возникло по причине того, что замыкание ждёт, что self.who существует, но он был высвобожден, как только mermaid вышла из области действия в конце блока do.

Это пример может выглядеть высосанным из пальца, но такие вещи случаются. Например, когда мы используем замыкания, чтобы запустить что-то значительно позже, скажем, после того, как закончился асинхронный вызов в сети.

Списки захвата (Capture Lists)

В Swift предусмотрен простой и элегантный способ разорвать цикл сильных ссылок в замыканиях. Вы объявляете список захвата, в котором определяете отношения между замыканием и объектами, которое оно захватывает.

Для демонстрации списка захвата рассмотрим следующий код:

   var x = 5
var y = 5

let someClosure = { [x] in
  print("\(x), \(y)")
}
x = 6
y = 6

someClosure()        // Prints 5, 6
print("\(x), \(y)")  // Prints 6, 6  
  

x есть в списке захвата замыкания, таким образом значение x копируется в месте определения замыкания. Оно захвачено по значению.

y нет в списке захвата, оно захвачено по ссылке. Это означает, что значение y будет таким, какое оно в момент вызова замыкания.

Списки замыкания помогают определить отношения weak или unowned взаимодействие по отношению к захвачиваемым внутри замыкания объектам. В нашем случае подходящий выбор — unowned, так как замыкание не может существовать, если экземпляр CarrierSubscription высвободится.

Циклы ссылок (Reference Cycles)

В большинстве случаев ARC работает так, как надо. Разработчику обычно не нужно беспокоиться об утечках памяти, когда неиспользуемые объекты остаются неосвобожденными на неопределённое время.

Но не всегда! Возможны утечки памяти.

Как это может произойти? Представим ситуацию, когда два объекта больше не используются, но каждый из них ссылается на другой. Так как у каждого счетчик ссылок не равен 0, ни один из них не будет освобождён.

LINK LOOP И ЯЗЫК ПРОГРАММИРОВАНИЯ RUST

Это цикл сильных ссылок ( strong reference cycle
). Такая ситуация сбивает с толку ARC и не позволяет ему очистить память.

Как видите, счетчик ссылок в конце не равен 0 и, хотя никакие объекты уже не нужны, object1 и object2 не будут освобождены.

Заключение

Найти циклические ссылки в таблице Excel достаточно просто. Значительно упрощает данную задачу автоматическое оповещение самой программы о наличии конфликтующих выражений. После этого останется всего лишь воспользоваться одним из двух описанных выше способов, чтобы избавиться от ошибок.

Оцените качество статьи. Нам важно ваше мнение:

Заключение

Теперь вы хорошо понимаете управление памятью в Swift и знаете, как работает ARC. Надеюсь, публикация была для вас полезной.

Apple: Automatic Reference Counting

Понравилась статья? Поделиться с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: