Використання незакритого параметра може закрити його


139

У мене є протокол:

enum DataFetchResult {
    case success(data: Data)
    case failure
}

protocol DataServiceType {
    func fetchData(location: String, completion: (DataFetchResult) -> (Void))
    func cachedData(location: String) -> Data?
}

З прикладом реалізації:

    /// An implementation of DataServiceType protocol returning predefined results using arbitrary queue for asynchronyous mechanisms.
    /// Dedicated to be used in various tests (Unit Tests).
    class DataMockService: DataServiceType {

        var result      : DataFetchResult
        var async       : Bool = true
        var queue       : DispatchQueue = DispatchQueue.global(qos: .background)
        var cachedData  : Data? = nil

        init(result : DataFetchResult) {
            self.result = result
        }

        func cachedData(location: String) -> Data? {
            switch self.result {
            case .success(let data):
                return data
            default:
                return nil
            }
        }

        func fetchData(location: String, completion: (DataFetchResult) -> (Void)) {

            // Returning result on arbitrary queue should be tested,
            // so we can check if client can work with any (even worse) implementation:

            if async == true {
                queue.async { [weak self ] in
                    guard let weakSelf = self else { return }

                    // This line produces compiler error: 
                    // "Closure use of non-escaping parameter 'completion' may allow it to escape"
                    completion(weakSelf.result)
                }
            } else {
               completion(self.result)
            }
        }
    }

Код, описаний вище, компілюється та працює у Swift3 (Xcode8-beta5), але вже не працює з бета-версією 6. Чи можете ви вказати на основну причину?


5
Це дуже чудова стаття про те, чому це робиться саме так у Swift 3
Honey

1
Немає сенсу, що ми повинні це робити. Жодна інша мова цього не вимагає.
Андрій Костер

Відповіді:


243

Це пов’язано зі зміною поведінки за замовчуванням для параметрів типу функції. Перед Swift 3 (зокрема, збіркою, що постачається з Xcode 8 beta 6), вони за замовчуванням будуть втечею - вам потрібно буде позначити їх @noescape, щоб запобігти їх збереженню або захопленню, що гарантує, що вони не переживуть тривалість виклику функції.

Однак зараз @noescapeпараметри типових параметрів за замовчуванням. Якщо ви хочете зберігати або захоплювати такі функції, тепер вам потрібно позначити їх @escaping:

protocol DataServiceType {
  func fetchData(location: String, completion: @escaping (DataFetchResult) -> Void)
  func cachedData(location: String) -> Data?
}

func fetchData(location: String, completion: @escaping (DataFetchResult) -> Void) {
  // ...
}

Дивіться пропозицію Swift Evolution для отримання додаткової інформації про цю зміну.


2
Але, як ви використовуєте замикання, щоб воно не дозволило втекти?
Енеко Алонсо

6
@EnekoAlonso Не зовсім впевнений у тому, що ви запитуєте - ви можете викликати параметр функції, що не виходить, безпосередньо в самій функції, або ви можете зателефонувати, коли буде зроблено в режимі, що не закривається. У цьому випадку, оскільки ми маємо справу з асинхронним кодом, немає гарантії, що asyncпараметр функції (і, отже, completionфункція) буде викликаний перед fetchDataвиходом - і тому повинен бути @escaping.
Хаміш

Відчуває себе некрасивим, що нам потрібно вказати @escaping як підпис методу для протоколів ... це те, що нам робити? Пропозиція не говорить! : S
Саджон

1
@Sajjon В даний час вам потрібно зіставити @escapingпараметр у вимозі протоколу з @escapingпараметром при реалізації цієї вимоги (і навпаки для параметрів, що не виходять). Те саме було в Swift 2 для @noescape.
Гаміш


30

Оскільки @noescape за замовчуванням, є 2 варіанти виправити помилку:

1) як вказував @Hamish у своїй відповіді, просто позначте завершення як @escaping, якщо ви піклуєтесь про результат і дійсно хочете, щоб він уникнув (це, мабуть, так у запитанні @ Лукаша з Unit Tests як приклад та можливість асинхронізації завершення)

func fetchData(location: String, completion: @escaping (DataFetchResult) -> Void)

АБО

2) зберігайте поведінку @noescape за замовчуванням, роблячи завершення необов'язковим, відкидаючи результати взагалі у випадках, коли ви не дбаєте про результат. Наприклад, коли користувач вже "пішов" і контролеру подання викликів не потрібно зависати в пам'яті лише тому, що відбувся недбалий мережевий дзвінок. Так само, як це було в моєму випадку, коли я прийшов сюди шукати відповіді, і зразок коду був для мене не дуже актуальним, тому маркування @noescape було не найкращим варіантом, подія, хоча воно звучало як єдине з першого погляду.

func fetchData(location: String, completion: ((DataFetchResult) -> Void)?) {
   ...
   completion?(self.result)
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.