Путаный пример сильного эталонного цикла в Swift

Это пример из документа Apple:

class HTMLElement {

    let name: String
    let text: String?

    lazy var asHTML: Void -> String = {
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        print("\(name) is being deinitialized")
    }

}

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

Что меня действительно смущает, так это код:

var heading: HTMLElement? = HTMLElement(name: "h1")
let defaultText = "some default text"
heading!.asHTML = {
    // confusing, this closure are supposed to retain heading here, but it does not
    return "<\(heading!.name)>\(heading!.text ?? defaultText)</\(heading!.name)>"
}
print(heading!.asHTML())
heading = nil
// we can see the deinialization message here, 
// it turns out that there is not any strong reference cycle in this snippet.

Насколько я знаю из Swift документации и моего собственного опыта Objective-c, переменная heading будет зафиксирована закрытием, таким образом, должен быть вызван сильный опорный цикл. Но это не так, это меня действительно смутило.

Я также написал экземпляр Objective-c этого примера, и он вызвал сильный опорный цикл, как я ожидал.

typedef NSString* (^TagMaker)(void);

@interface HTMLElement : NSObject

@property (nonatomic, strong) NSString      *name;
@property (nonatomic, strong) NSString      *text;

@property (nonatomic, strong) TagMaker      asHTML;

@end

@implementation HTMLElement

- (void)dealloc {
    NSLog(@"%@", [NSString stringWithFormat:@"%@ is being deinitialized", self.name]);
}

@end

;

HTMLElement *heading = [[HTMLElement alloc] init];
heading.name = @"h1";
heading.text = @"some default text";

heading.asHTML = ^ {
    return [NSString stringWithFormat:@"<%@>%@</%@>", heading.name, heading.text, heading.name];
};

NSLog(@"%@", heading.asHTML());

heading = nil;
// heading has not been deinitialized here

Любые подсказки или руководства будут очень признательны.

Ответ 1

Потому что в более позднем случае

Swift закрытие содержит сильную ссылку heading, а не экземпляр heading, указывающий на

В изображении это выглядит так:

aN7hq.png

если мы сломаем красную строку с помощью set heading = nil, тогда кружок задания сломан.

Начало обновления:

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

func testCircle(){
    var heading: HTMLElement? = HTMLElement(name: "h1")
    let defaultText = "some default text"
    heading.asHTML = {
        return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
    }
    print(heading.asHTML())
}
testCircle()//No dealloc message is printed

Окончание обновления

Я также пишу приведенный ниже тестовый код, чтобы доказать, что замыкание не содержит сильной ссылки на экземпляр в памяти

var heading: HTMLElement? = HTMLElement(name: "h1")
var heading2 = heading
let defaultText = "some default text"
heading!.asHTML = {
// confusing, this closure are supposed to retain heading here, but it does not
    return "<\(heading!.name)>\(heading!.text ?? defaultText)</\(heading!.name)>"
}
let cloureBackup = heading!.asHTML
print(heading!.asHTML())

heading = HTMLElement(name: "h2")

print(cloureBackup())//<h2>some default text</h2>

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

И вы увидите журнал с игровой площадки

<h1>some default text</h1>
<h2>some default text</h2>

Не нашли документа об этом, просто из моего тестирования и понимания, надеюсь, что это будет полезно

Ответ 2

Я думаю, что деви находится в деталях. В documentation указано:

... может произойти захват, потому что тело закрывает доступ к свойству экземпляра, например self.someProperty, или потому, что замыкание вызывает метод в экземпляре, например self.someMethod().

Примечание self здесь, что, я считаю, является ключом к вопросу.

Другой фрагмент documentation предлагает:

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

Итак, другими словами, это фиксированные константы и переменные, а не объекты как таковые. Это просто self является особым случаем, потому что, когда self используется в замыкании, которое инициализируется внутри объекта, контракт заключен в том, что такой self всегда присутствует, когда замыкание запускается. Другими словами, ни при каких обстоятельствах не может произойти, что замыкание с self в его теле выполняется, но объект, который этот self указывает на, исчез. Рассмотрим это: такое закрытие можно взять в другом месте, например. присваивается другому свойству другого объекта, и поэтому он должен иметь возможность запускать, даже если первоначальный владелец объекта, который был захвачен, "забывает" об этом. Было бы смешно просить разработчика проверить, есть ли self nil, правильно? Следовательно, необходимо сохранить сильную ссылку.

Теперь, если вы перейдете к другому случаю, где нет self, используемого закрытием, но некоторые (явно развернутые) необязательные, то это совершенно другая игра в мяч. Такой необязательный параметр может быть nil, и разработчик должен принять этот факт и позаботиться об этом. Когда такое закрытие выполняется, возможно, это было так, что необязательное свойство, которое он использует, на самом деле никогда не было назначено с конкретным значением! Итак, какой смысл держать ссылку?


Чтобы проиллюстрировать. Вот базовый класс:

class Foo {
    let name: String

    lazy var test: Void -> Void = {
        print("Running closure from \(self.name)")
    }

    init(name: String) {
        self.name = name
    }
}

И это зеркало цикла сильной ссылки:

var closure: Void -> Void

var captureSelf: Foo? = Foo(name: "captureSelf")
closure = captureSelf!.test
closure()                       // Prints "Running closure from captureSelf"
captureSelf = nil
closure()                       // Still prints "Running closure from captureSelf"

Теперь следующий случай с необязательным свойством вне объекта:

var tryToCaptureOptional: Foo? = Foo(name: "captureSomeOptional")
tryToCaptureOptional?.test = {
    print("Running closure from \(tryToCaptureOptional?.name)")
}
closure = tryToCaptureOptional!.test
closure()                       // Prints "Running closure from Optional("captureSomeOptional")"
tryToCaptureOptional = nil
closure()                       // Prints "Running closure from nil"

.. т.е. мы все еще "помним" закрытие, но замыкание должно иметь возможность обрабатывать случай, что используемое им свойство фактически nil.

Но "забава" начинается только сейчас. Например, мы можем сделать:

var tryToCaptureAnotherOptional: Foo? = Foo(name: "tryToCaptureAnotherOptional")
var holdItInNonOptional: Foo = tryToCaptureAnotherOptional!
tryToCaptureAnotherOptional?.test = {
    print("Running closure from \(tryToCaptureAnotherOptional?.name)")
}
closure = tryToCaptureAnotherOptional!.test
closure()                       // Prints "Running closure from Optional("tryToCaptureAnotherOptional")"
tryToCaptureAnotherOptional = nil
closure()                       // Prints "Running closure from nil"
print(holdItInNonOptional.name) // Prints "tryToCaptureAnotherOptional" (!!!)
holdItInNonOptional.test()      // Also prints "Running closure from nil"

.. Иными словами, даже если объект на самом деле не "ушел", но только какое-то конкретное свойство больше не указывает на него, рассматриваемое закрытие все равно будет адаптироваться и действовать в соответствии с тем, что нет объекта (в то время как исходный объект все еще живет, он только что переместился на другой адрес).


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

Ответ 3

Вдохновленный ответами @Leo и @Антон Бронников, а также эта статья от г-на Тертона

Понимание опций в Swift

Я обнаружил, что все мое замешательство исходит из моего периферийного понимания Optional Types в Swift.

Как мы можем видеть описания Optional и ImplicitlyUnwrappedOptional в документации Swift и определении Optional:

public enum Optional<Wrapped> : _Reflectable, NilLiteralConvertible {
    case None
    case Some(Wrapped)
    /// Construct a `nil` instance.
    public init()
    /// Construct a non-`nil` instance that stores `some`.
    public init(_ some: Wrapped)
    /// If `self == nil`, returns `nil`.  Otherwise, returns `f(self!)`.
    @warn_unused_result
    public func map<U>(@noescape f: (Wrapped) throws -> U) rethrows -> U?
    /// Returns `nil` if `self` is nil, `f(self!)` otherwise.
    @warn_unused_result
    public func flatMap<U>(@noescape f: (Wrapped) throws -> U?) rethrows -> U?
    /// Create an instance initialized with `nil`.
    public init(nilLiteral: ())
}

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

Итак, в приведенном выше примере кода:

var heading: HTMLElement? = HTMLElement(name: "h1")

переменная heading, о которой мы говорим, на самом деле представляет собой перечисление, тип значения - не ссылка на экземпляр HTMLElement. Следовательно, это перечисление скорее ссылочного типа, которое было зафиксировано закрытием. И, конечно, количество ссылок экземпляра HTMLElement не было добавлено внутри закрытия.

heading!.asHTML = {
    return "<\(heading!.name)>\(heading!.text ?? defaultText)</\(heading!.name)>"
}
print(heading!.asHTML())

В этот момент счетчик экземпляра HTMLElement равен +1, экземпляр удерживается перечислением heading. И количество удержаний закрытия равно +1, удерживаемое экземпляром HTMLElement. Пока перечисление heading захватывается закрытием. Цикл ссылок в точности соответствует изображению @Leo, проиллюстрированному в его ответе,

Leo's illustration

Когда мы устанавливаем heading = nil, ссылка на экземпляр HTMLElement, содержащаяся в перечислении heading, будет освобождена, счетчик ссылок экземпляра станет равным 0, а затем счетчик ссылок закрытия будет также равен 0, затем, само перечисление будет выпущено путем закрытия. Все будет правильно выпущено в конце.

В заключение: для начинающих Swift, которые раньше были разработчиками Objective-c, таких как я, для нас чрезвычайно важно понять глубокое понимание различий между двумя языками. И большое спасибо всем повторителям, ваши ответы действительно вдохновляют, а также помогают, спасибо.


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

Ответ 4

Ваш asHTML - это переменная, содержащая замыкание. Закрытие содержит ссылку на объект HTMLElement. И это закрытие хранится, снова имея сильную ссылку. Итак, у вас есть свой цикл.

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

В качестве альтернативы вы можете объявить, какие значения блокирует захват, поэтому пусть он фиксирует слабую копию "я".

Ответ 5

Я думаю, что swift ведет себя несколько иначе:

Из this:

Если какая-либо переменная объявлена ​​вне области закрытия, ссылка на эту переменную внутри области закрытия создает еще одну сильную ссылку на этот объект. Единственными исключениями для этого являются переменные, которые используют семантику значения, такие как Ints, Strings, Arrays и Dictionaries в Swift.