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

Если у меня есть замыкание передано в функцию, как это:

 someFunctionWithTrailingClosure { [weak self] in

     anotherFunctionWithTrailingClosure { [weak self] in 
                    self?.doSomething()
     }

 }

Если я объявлю себя как [weak self] в someFunctionWithTrailingClosureсписок перехвата без повторного выделения его как weak снова в списке захвата anotherFunctionWithTrailingClosureself уже становится Optional типа, но это также становится weak Ссылка, а?

Спасибо!

2 ответа

Решение

[weak self] в anotherFunctionWithTrailingClosure не нужен

Вы можете эмпирически проверить это:

class Experiment {

    func someFunctionWithTrailingClosure(closure: () -> ()) {
        print("starting someFunctionWithTrailingClosure")
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(1 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) {
            closure()
            print("finishing someFunctionWithTrailingClosure")
        }
    }

    func anotherFunctionWithTrailingClosure(closure: () -> ()) {
        print("starting anotherFunctionWithTrailingClosure")
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(1 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) {
            closure()
            print("finishing anotherFunctionWithTrailingClosure")
        }
    }

    func doSomething() {
        print("doSomething")
    }

    func testCompletionHandlers() {
        someFunctionWithTrailingClosure { [weak self] in
            self?.anotherFunctionWithTrailingClosure { // [weak self] in
                self?.doSomething()
            }
        }
    }

    // go ahead and add `deinit`, so I can see when this is deallocated

    deinit {
        print("deinit")
    }
}

А потом:

func performExperiment() {
    dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0)) { 
        let obj = Experiment()

        obj.testCompletionHandlers()

        // sleep long enough for `anotherFunctionWithTrailingClosure` to start, but not yet call its completion handler

        NSThread.sleepForTimeInterval(1.5) 
    }
}

Если вы сделаете это, вы увидите, что doSomething никогда не называется и что deinit называется раньше anotherFunctionWithTrailingClosure называет его закрытием.

Проверяя это поведение, я лично все еще был бы склонен использовать [weak self] синтаксис на anotherFunctionWithTrailingClosure сделать мое намерение явным, поскольку все это далеко не очевидно.

TL;DR

Хотя использование [weak self]один раз во внешнем блоке - нормально (EX1), если вы измените эту ссылку на сильную (например,guard let self = self) вам понадобится [weak self] также во внутреннем блоке (EX3).

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


Хорошее практическое правило - всегда использовать weak если объект сильный сразу за укупоркой.

Примеры, которые не сохраняются self (т.е. обычно это "хорошие" сценарии):

// EX1
fn { [weak self] in
  self?.foo() 
}
// EX2
fn { [weak self] in 
  fn2 {
    self?.foo()
  }
}
// self is weak inside fn, thus adding an extra `[weak self]` inside fn2 is unnecessary
// EX3
fn { [weak self] in 
  guard let self = self else { return }
  fn2 { [weak self] in
    self?.foo()
  }
}

Примеры, которые ДОЛЖНЫ сохраняться self (т.е. обычно "плохие" сценарии):

// EX1_B
fn {
  self.foo()
}
// fn retains self
// EX2_B
fn {
  fn2 { [weak self] in
    self.foo()
  }
}
// fn retains self (this is a common, hard-to-spot mistake)
// EX3_B
fn { [weak self] in 
  guard let self = self else { return }
  fn2 {
    self.foo()
  }
}
// fn2 retains self

Как намекает Хэмиш, есть две основные причиныweak Полезно:

  1. Чтобы предотвратить циклы сохранения.
  2. Чтобы предметы не жили дольше, чем они должны быть.

Подробнее о #2 (предотвращение долгоживущих объектов)

В примере Роба функция не сохраняет закрытие (помимо dispatch_async, который почти гарантированно запустит закрытие в какой-то момент в будущем), поэтому у вас никогда не будет цикла сохранения. Итак, используяweak в этом случае, значит, предотвратить # 2.

Как упоминает Хэмиш, в этом примере weak на самом деле не требуется для предотвращения циклов сохранения, поскольку циклов сохранения нет. weakв данном случае используется для предотвращения жизни объекта дольше, чем необходимо. Это полностью зависит от вашего варианта использования, когда вы считаете, что объект живет дольше, чем необходимо. Таким образом, бывают случаи, когда вы захотите использоватьweak только снаружи (EX2), а в других случаях, когда вы хотите использовать weak внешний strong внутренний weak внутренний танец (EX3), например.

Подробнее о #1 (предотвращение циклов сохранения)

Чтобы изучить проблему цикла сохранения, предположим, что функция хранит ссылку на блок (например, кучу) вместо прямой ссылки на функцию (например, стек). Много раз мы не знаем внутренности класса / функции, так что безопаснее предположить, что функция является сохранение блока.

Теперь вы можете легко создать цикл сохранения, используя weak внешний, и только используя strong внутренний (EX3_B):

public class CaptureListExperiment {

    public init() {

    }

    var _someFunctionWithTrailingClosure: (() -> ())?
    var _anotherFunctionWithTrailingClosure: (() -> ())?

    func someFunctionWithTrailingClosure(closure: @escaping () -> ()) {
        print("starting someFunctionWithTrailingClosure")
        _someFunctionWithTrailingClosure = closure

        DispatchQueue.global().asyncAfter(deadline: .now() + 1) { [weak self] in
            self?._someFunctionWithTrailingClosure!()
            print("finishing someFunctionWithTrailingClosure")
        }
    }

    func anotherFunctionWithTrailingClosure(closure: @escaping () -> ()) {
        print("starting anotherFunctionWithTrailingClosure")
        _anotherFunctionWithTrailingClosure = closure

        DispatchQueue.global().asyncAfter(deadline: .now() + 1) { [weak self] in
            self?._anotherFunctionWithTrailingClosure!()
            print("finishing anotherFunctionWithTrailingClosure")
        }
    }

    func doSomething() {
        print("doSomething")
    }

    public func testCompletionHandlers() {
        someFunctionWithTrailingClosure { [weak self] in
            guard let self = self else { return }
            self.anotherFunctionWithTrailingClosure { // [weak self] in
                self.doSomething()
            }
        }
    }

    // go ahead and add `deinit`, so I can see when this is deallocated

    deinit {
        print("deinit")
    }
}

func performExperiment() {

    let obj = CaptureListExperiment()

    obj.testCompletionHandlers()
    Thread.sleep(forTimeInterval: 1.3)
}

performExperiment()

/* Output:

starting someFunctionWithTrailingClosure
starting anotherFunctionWithTrailingClosure
finishing someFunctionWithTrailingClosure
doSomething
finishing anotherFunctionWithTrailingClosure
*/

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

Это можно исправить, удалив strong ссылка (EX2):

public func testCompletionHandlers() {
    someFunctionWithTrailingClosure { [weak self] in
        //guard let self = self else { return }
        self?.anotherFunctionWithTrailingClosure { // [weak self] in
            self?.doSomething()
        }
    }
}

Или используя танец слабый / сильный / слабый (Пример 3):

public func testCompletionHandlers() {
    someFunctionWithTrailingClosure { [weak self] in
        guard let self = self else { return }
        self.anotherFunctionWithTrailingClosure { [weak self] in
            self?.doSomething()
        }
    }
}

Обновлено для Swift 4.2:

public class CaptureListExperiment {

    public init() {

    }

    func someFunctionWithTrailingClosure(closure: @escaping () -> ()) {
        print("starting someFunctionWithTrailingClosure")

        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            closure()
            print("finishing someFunctionWithTrailingClosure")
        }
    }

    func anotherFunctionWithTrailingClosure(closure: @escaping () -> ()) {
        print("starting anotherFunctionWithTrailingClosure")

        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            closure()
            print("finishing anotherFunctionWithTrailingClosure")
        }
    }

    func doSomething() {
        print("doSomething")
    }

    public func testCompletionHandlers() {
        someFunctionWithTrailingClosure { [weak self] in
            guard let self = self else { return }
            self.anotherFunctionWithTrailingClosure { // [weak self] in
                self.doSomething()
            }
        }
    }

    // go ahead and add `deinit`, so I can see when this is deallocated

    deinit {
        print("deinit")
    }
}

попробуйте Playgorund:

func performExperiment() {

    let obj = CaptureListExperiment()

    obj.testCompletionHandlers()
    Thread.sleep(forTimeInterval: 1.3)
}

performExperiment()
Другие вопросы по тегам