JavaRush /Курсы /Swift SELF /weak vs unowned: отличия и типичные ошибки

weak vs unowned: отличия и типичные ошибки

Swift SELF
37 уровень , 2 лекция
Открыта

1. Почему вообще есть два слова: weak и unowned

Когда вы впервые видите weak и unowned, хочется спросить: «Зачем два способа не удерживать объект? Разве нельзя было сделать один?» Вопрос честный. Но на практике эти два ключевых слова закрывают два разных жизненных сценария: «ссылка может стать пустой — и это нормально» и «ссылка не должна стать пустой никогда — это часть контракта». Именно из‑за этих разных контрактов язык даёт два разных инструмента.

ARC в Swift вообще устроен так, чтобы управление памятью было предсказуемым и «точным», в отличие от подходов со сборщиком мусора. Это одна из причин, почему Swift ценят за производительность и контроль над ресурсами.

При этом важно помнить: хотя ARC убирает необходимость вручную retain/release, утечки (и просто неожиданные «не умирающие» объекты) всё равно возможны — в основном из-за циклов удержания между объектами или из-за связки «объект ↔ замыкание». Сегодня мы закрываем половину этой истории: как разрывать циклы между объектами правильно, выбирая weak или unowned.

2. weak: ссылка, которая умеет исчезать

Если описывать weak по‑человечески, то это «ссылка‑наблюдатель». Она говорит: «мне интересно знать про этот объект, но я не собираюсь держать его в живых». И важная деталь: если объект умер — weak-ссылка автоматически станет nil. Отсюда вытекает ключевая особенность, которая новичков сначала раздражает, а потом спасает: weak всегда Optional.

Именно поэтому weak — главный кандидат для случаев, когда связь логически может оборваться. Например, «квартира может временно быть без жильца», «у экрана приложения может не быть координатора», «у дочернего объекта может исчезнуть владелец, и это нормальная ситуация». В таких сценариях Swift заставляет вас честно обработать отсутствие значения — потому что оно реально возможно.

Синтаксис weak и его последствия

Здесь всё довольно строго:

  • weak можно применять только к ссылочным типам (class), потому что смысл «не удерживать объект» относится именно к объектам в куче.
  • weak-свойство всегда имеет тип Optional.
  • weak-свойство нельзя объявить как let, потому что оно может стать nil в любой момент, когда объект будет освобождён. Поэтому только var.

С точки зрения «я читаю чужой код и хочу понимать, что произойдёт», weak делает две вещи: разрывает цикл удержания и заставляет вас писать код так, будто объект может исчезнуть. И это правильно: если объект может исчезнуть, то притворяться, что он вечен — плохая стратегия.

Мини‑пример: разрываем цикл между объектами через weak

Ниже пример, который можно запустить как отдельный файл. Он показывает типичный паттерн: один объект владеет другим (strong), а обратная ссылка — weak.

import Foundation

func demoWeakBreaksCycle() {
    final class Person {
        let name: String
        var apartment: Apartment?
        init(name: String) { self.name = name; print("init Person \(name)") }
        deinit { print("deinit Person \(name)") }
    }

    final class Apartment {
        let number: Int
        weak var tenant: Person?           // <-- не удерживаем жильца
        init(number: Int) { self.number = number; print("init Apt \(number)") }
        deinit { print("deinit Apt \(number)") }
    }

    var p: Person? = Person(name: "Ann")
    var a: Apartment? = Apartment(number: 10)

    p?.apartment = a
    a?.tenant = p

    p = nil
    a = nil
}

demoWeakBreaksCycle()
// Возможный вывод (порядок deinit может отличаться):
// init Person Ann
// init Apt 10
// deinit Person Ann
// deinit Apt 10

Почему это работает? Потому что Apartment больше не держит Person сильной ссылкой. Цикла Person -> Apartment -> Person не получается: обратно идёт weak, а значит «круг» не замыкается.

3. unowned: ссылка‑обещание

С unowned логика другая. Он тоже не удерживает объект, то есть тоже помогает разрывать циклы. Но он говорит не «объект может исчезнуть», а «объект не исчезнет, пока я жив».

То есть unowned — это не про «мне не жалко отпустить», а про «я точно знаю порядок жизни объектов». И это знание становится частью контракта вашего кода.

Техническое следствие очень заметное: unowned обычно не Optional. А раз он не Optional, Swift не заставляет вас проверять nil. Это приятно… пока контракт не нарушился. Если вы обращаетесь по unowned-ссылке к уже освобождённому объекту, программа падает (это именно ошибка времени выполнения, а не «мягкое» поведение).

В языке даже есть формулировка на уровне дизайна: для некоторых контекстов существуют варианты unowned(safe) / unowned(unsafe) — то есть сама идея «неудерживающей ссылки без Optional» довольно фундаментальна. В повседневном коде, особенно новичковом, обычно используют просто unowned.

Когда unowned уместен

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

Если ребёнок не может существовать без родителя, weak будет создавать лишнюю «опциональность» и засорять код проверками. В этом случае unowned может быть аккуратнее.

Мини‑пример: родитель–ребёнок через unowned

import Foundation

func demoUnowned() {
    final class Parent {
        let name: String
        var child: Child?
        init(name: String) { self.name = name; print("init Parent \(name)") }
        deinit { print("deinit Parent \(name)") }
    }

    final class Child {
        let name: String
        unowned let parent: Parent         // <-- не удерживаем, но обещаем "жив"
        init(name: String, parent: Parent) {
            self.name = name
            self.parent = parent
            print("init Child \(name)")
        }
        deinit { print("deinit Child \(name)") }
    }

    var p: Parent? = Parent(name: "P")
    if let parent = p {
        parent.child = Child(name: "C", parent: parent)
        print(parent.child?.parent.name ?? "no parent")
        // P
    }
    p = nil
}

demoUnowned()
// Возможный вывод:
// init Parent P
// init Child C
// P
// deinit Parent P
// deinit Child C

Заметьте важную идею: в примере Parent держит child сильно, значит ребёнок живёт «внутри жизни родителя». Поэтому ребёнку безопасно иметь unowned-ссылку на родителя: контракт выполняется.

4. Как выбрать между weak и unowned

Сейчас будет момент, когда мозг хочет простой «если‑то» алгоритм. Это нормально: на старте проще иметь чуть более грубое правило, чем каждый раз философствовать про архитектуру. Со временем вы научитесь чувствовать «владение» по коду, но сначала пусть будет честная шпаргалка.

Таблица отличий: weak и unowned

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

Свойство
weak
unowned
Удерживает объект? Нет Нет
Может стать nil автоматически? Да Нет
Тип ссылки Всегда Optional Обычно не Optional
Можно объявить как let? Нет, только var Да, часто let уместен
Что будет, если объект уже освобождён? Вы увидите nil и должны обработать Падение при обращении (нарушен контракт)
Главная идея «Объект может исчезнуть — это нормально» «Объект не исчезнет — я гарантирую»

Эта таблица напрямую подводит к практическому правилу выбора: weak — для «может не быть», unowned — для «должен быть всегда».

Шпаргалка выбора: маленькая схема

flowchart TD
    A[Нужно разорвать цикл или сделать ссылку не-удерживающей] --> B{Ссылка может легально стать пустой?}
    B -->|Да, это нормальный сценарий| C[weak var x: T?]
    B -->|Нет, объект обязан быть жив пока я жив| D[unowned let x: T]

Если вы сомневаетесь — выбирайте weak. Это безопаснее. unowned экономит проверки, но требует уверенности в жизненном цикле. Если уверенности нет, это не «оптимизация», а лотерея.

5. Пример в стиле учебного приложения: Library и Member

Чтобы это не оставалось абстракцией «человек и квартира», давайте пример ближе к нашему курсу: представим, что в нашем CLI‑приложении управления библиотекой у нас появляются классы для «сессии» или «контекста» приложения. Нам нужно хранить участников (читателей) и при этом у каждого читателя должна быть ссылка на библиотеку — например, чтобы печатать сообщения с названием библиотеки или проверять правила.

Вариант с weak: читатель может существовать отдельно

Этот вариант уместен, если Member может быть создан «временно», например во время парсинга/валидации, ещё до добавления в библиотеку. То есть «читатель без библиотеки» — легальный сценарий.

import Foundation

func demoLibraryWithWeak() {
    final class Library {
        let name: String
        var members: [Member] = []
        init(name: String) { self.name = name; print("init Library \(name)") }
        deinit { print("deinit Library \(name)") }
    }

    final class Member {
        let name: String
        weak var library: Library?         // <-- библиотека может исчезнуть/не быть
        init(name: String) { self.name = name; print("init Member \(name)") }
        deinit { print("deinit Member \(name)") }
    }

    var lib: Library? = Library(name: "City Library")
    var m: Member? = Member(name: "Sam")

    m?.library = lib
    lib?.members.append(m!)

    lib = nil
    m = nil
}

demoLibraryWithWeak()

Почему тут weak выглядит честно? Потому что Member вообще-то можно представить без Library. Да, в «идеальном мире» он всегда в библиотеке, но код допускает переходные состояния. И weak поддерживает такой дизайн.

Вариант с unowned: читатель не может существовать без библиотеки

Если же по вашему доменному правилу Member создаётся только внутри Library и без неё не имеет смысла, можно сделать контракт жёстче: unowned. Код станет чище, но требования к жизненному циклу вырастут.

import Foundation

func demoLibraryWithUnowned() {
    final class Library {
        let name: String
        var members: [Member] = []
        init(name: String) { self.name = name; print("init Library \(name)") }
        func addMember(name: String) {
            members.append(Member(name: name, library: self))
        }
        deinit { print("deinit Library \(name)") }
    }

    final class Member {
        let name: String
        unowned let library: Library       // <-- обязана жить дольше
        init(name: String, library: Library) {
            self.name = name
            self.library = library
            print("init Member \(name)")
        }
        deinit { print("deinit Member \(name)") }
    }

    var lib: Library? = Library(name: "Campus Library")
    lib?.addMember(name: "Alex")
    lib = nil
}

demoLibraryWithUnowned()

Обратите внимание на архитектурный намёк: чтобы unowned был честным, полезно сделать так, чтобы Member нельзя было создать «сам по себе» без библиотеки — например, через метод addMember (или фабрику), а не через публичный init без контекста. Тогда контракт поддерживается самим API, а не только вашей верой в лучшее.

6. weak/unowned и синтаксис Swift

Когда вы начнёте активно использовать weak (особенно в местах, где переменная может стать nil), вы столкнётесь с тем, что Swift любит заставлять вас делать контроль потока явным. Это видно даже на уровне диагностики: когда self захвачен как weak, он становится optional, и компилятор требует явного self? или явной распаковки.

Даже если сегодня мы не углубляемся в тему замыканий, сама логика полезна: weak означает «может быть nil», следовательно, язык будет подталкивать вас к аккуратной обработке nil.

И ещё одна маленькая «памятка из будущего опыта»: когда вам нужно на время «сделать weak сильным» внутри некоторой области видимости, в Swift есть паттерн optional binding, и он настолько распространён, что даже обсуждался и закреплялся на уровне эволюции языка. Это не про магию, а про стиль: сначала проверяем, что объект жив, потом работаем с ним как с нормальным не‑optional. Практически это чаще всего выглядит как if let

7. Типичные ошибки при выборе weak / unowned

Ошибка №1: использовать unowned «чтобы не писать ?», не имея железной гарантии жизненного цикла.
Это самая частая ловушка: вы смотрите на weak var x: T?, видите optional, устаете от if let, и рука тянется к unowned. Но unowned — это не косметика и не «сделать покороче». Это обещание. Если объект может умереть раньше — приложение упадёт. Поэтому, если вы хотя бы на 1% не уверены, что владелец живёт дольше, weak честнее.

Ошибка №2: ставить weak там, где связь обязательна по смыслу, и превращать код в «optional‑болото».
Иногда наоборот: разработчик боится падений и начинает ставить weak везде подряд, даже там, где связь обязательна. Получается странная модель: объект по смыслу «не может жить без владельца», но в коде владелец почему‑то optional, и вам приходится постоянно делать проверки, которые по логике никогда не должны срабатывать. В такой ситуации стоит подумать о unowned и о том, чтобы API не позволял создать объект в «неправильном» состоянии.

Ошибка №3: делать обе стороны связи weak, а потом удивляться, что всё внезапно стало nil.
Если вы поставили weak с обеих сторон, вы действительно разорвали цикл… но вы ещё и убрали владение как факт. Тогда объект может исчезнуть «раньше, чем вы ожидали», потому что его никто не держит. В итоге вы ловите nil не как «редкий сценарий», а как постоянное состояние системы. Обычно в паре всегда должен быть хотя бы один владелец (strong), иначе непонятно, кто отвечает за жизнь объекта.

Ошибка №4: забывать, что weak — только var.
Это простая, но частая ошибка начинающих: хочется написать «ссылка не владеет и не меняется» и поставить let. Но weak меняется автоматически (становится nil при освобождении объекта), поэтому weak let невозможен. Если вам нужна «неизменяемая по вашей воле» ссылка, но при этом не удерживающая — часто это как раз случай для unowned let (но только при честном контракте жизненного цикла).

Ошибка №5: пытаться совместить weak/unowned с property wrappers и удивляться ошибкам компилятора.
Если вы дойдёте до property wrappers, там есть ограничения: свойства с wrapper’ом нельзя объявлять как weak или unowned. Это не «каприз», а следствие того, как разворачиваются обёртки в хранимые свойства. На практике это означает: если вам нужно weak/unowned, скорее всего, это должно быть обычное stored property без wrapper’а.

1
Задача
Swift SELF, 37 уровень, 2 лекция
Недоступна
Хозяин питомца
Хозяин питомца
1
Задача
Swift SELF, 37 уровень, 2 лекция
Недоступна
Экран и модель
Экран и модель
1
Задача
Swift SELF, 37 уровень, 2 лекция
Недоступна
Родитель ребёнка
Родитель ребёнка
1
Задача
Swift SELF, 37 уровень, 2 лекция
Недоступна
Отчёт менеджера
Отчёт менеджера
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ