Фон

Некоторое время назад, копаясь в исходном коде RxSwift, я впервые наткнулся на Never. В то время я хранил это в своем мозгу в разделе странно - загляните в это. Только недавно у меня была возможность вернуться к нему. Это небольшая изящная концепция, но, что еще более важно, полезная! Сейчас мы используем его в производственном коде. Эта статья поможет вам понять Never тип Swift и его полезность на двух реальных примерах.

Что никогда?

Быстрое и грязное: Never выражает невозможность через систему типов. Есть два основных применения:

  1. Экспресс, когда код не может быть запущен или вызван
  2. Выражение, когда функция не может вернуться

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

Невозможный код

Never интересен тем, что это необитаемый тип - его нельзя создать. Взгляните на его реализацию:

enum Never {}

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

Пустота и NSNull

Один из моих первоначальных вопросов был: «Чем это отличается от Void и NSNull?» Это восходит к способности создавать экземпляры вещей. Void и NSNull могут быть созданы как:

let void = ()
let null = NSNull()

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

Never, с другой стороны, не может этого сделать, потому что его нельзя создать. Когда он используется для определения функции или переменной или, если говорить точнее, для наименования универсального типа, это может сделать код невозможным для выполнения. Но зачем кому-то писать код, который никогда не запускается? Сначала это может показаться странным, но он возникает при работе с универсальными шаблонами и может помочь нам избежать fatalError или комментариев разработчиков, таких как // No need to do this.

Давайте посмотрим на несколько примеров невозможного кода.

Нетворкинг с Never

В этом примере будет использоваться Never в сетевой реализации, использующей универсальные шаблоны. Мы начнем с Result Swift и будем использовать его в следующем сетевом коде.

Невозможные результаты

Swift Result (теперь встроенный в Swift 5) - это общее перечисление, которое может помочь продемонстрировать невозможный код. Это определяется как:

enum Result<Success, Failure> where Failure: Error {
    case success(Success)
    case failure(Failure)
}

Здесь он используется для процедуры получения имени:

func fetchName(for number: Int) -> Result<String, Error> {
    guard number >= 0 else { return .failure(ExampleError.error) }
    // (Fetch the name...)
    return .success("Maya")
}
let nameResult = fetchName(for: 33)
switch nameResult {
case .success(let name): print(name)
case .failure(let error): print(error)
}

Что будет означать экземпляр Result<Never, Error>? Это означало бы, что он потерпел неудачу. Поскольку невозможно создать экземпляр Never, было бы невозможно создать здесь случай .success. Вот немного другой пример:

let mrPerfect = Result<String, Never>.success("I never fail")
switch mrPerfect {
case .success(let text): print(text)
}

* Если вам интересно, в Swift 5 Never уже соответствует Error.

Регистр .failure в переключателе не требуется, поскольку компилятор знает, что его невозможно создать. Таким образом, нет необходимости использовать fatalError или поучительный // This should never happen комментарий. Фактически, компилятору было бы полезно выдать предупреждение «Невозможно выполнить», если когда-либо был записан регистр .failure.

Обработка невозможных ответов

Используя предыдущий пример в качестве строительного блока, давайте опишем сетевые запросы, используя абстрактный и общий тип Request:

protocol Request {
    associatedtype Response
    func send(completion: (Result<Response, Error>) -> ())
}

Request имеет общий associatedtype, Response, который используется для описания типа объекта, который в конечном итоге будет возвращен вызывающей стороне. Когда сетевой вызов завершается, вызывается completion с Result<Response, Error>.

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

TrackScreen1 реализует это без Never:

struct TrackScreen1: Request {
    let name: String

    func send(completion: (Result<Void, Error>) -> ()) {
        // (Send the request...)
        let networkError: Error? = nil
        if let error = networkError {
            completion(.failure(error))
        }
        // No need to call `completion` for success.
    }
}

TrackScreen1(name: "Home").send() { result in
    switch result {
    case .success: break // This should never happen.
    case .failure(let error): print(error)
    }
}

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

С Never мы можем напрямую закодировать требование, как показано здесь в улучшенном TrackScreen2:

struct TrackScreen2: Request {
    let name: String

    func send(completion: (Result<Never, Error>) -> ()) {
        // (Send the request...)
        let networkError: Error? = nil
        if let error = networkError {
            completion(.failure(error))
        }
    }
}
TrackScreen2(name: "Home").send() { result in
    switch result {
    case .failure(let error): print(error)
    }
}

Здесь, в send, не нужно беспокоиться о том, что разработчики вызовут completion, потому что невозможно создать .success обращение. Кроме того, когда вызывающий абонент обрабатывает result, регистр .success может быть исключен из переключателя.

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

Координаторы с Never

Если вы когда-либо использовали шаблон «координатор» для описания потоков навигации в приложении для iOS, этот пример может вас заинтересовать. Вот краткий обзор того, как мы будем интерпретировать закономерность.

Координатора можно рассматривать как ответственного за конкретный пользовательский поток. Например, там может быть SettingsCoordinator, который показывает настройки пользователя. В этом конкретном потоке нажатие кнопки «Добавить платеж» может запустить дочерний поток AddPaymentCoordinator для добавления метода оплаты.

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

protocol Coordinator {
    associatedtype Completion
    func start(completion: (Completion) -> ())
}

Описанные выше настройки и пример оплаты могут выглядеть так:

class SettingsCoordinator: Coordinator {
    func start(completion: (()) -> ()) {
        // (Show the flow...)
        // (When user taps "Add Payment".)
        AddPaymentCoordinator().start() { paymentID in
            // (Show button to "Pay now" with new payment...)
        }
    }
}

class AddPaymentCoordinator: Coordinator {
    func start(completion: (String) -> ()) {
        // (Show the flow...)
        let paymentID = "CreditCard123"
        completion(paymentID)
    }
}

Навсегда координаторы

Получается, что некоторые координаторы никогда не заканчивают. Лучшим примером этого является самый первый или корневой координатор. Он представляет приложение и порождает дочерних координаторов, но пользователь не может «закончить» его.

Давайте посмотрим, как это AppCoordinator1 выглядит без использования Never:

class AppCoordinator1: Coordinator {
    func start(completion: (()) -> ()) {
        // (Start the app's UI...)
        // No need to call completion.
    }
}
class AppDelegate {
    let appCoordinator1 = AppCoordinator1()
    
    func applicationDidFinishLaunching() {
        appCoordinator1.start { _ in
            // This should never happen.
        }        
    }
}

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

Вот улучшенный AppCoordinator2 с использованием Never:

class AppCoordinator2: Coordinator {
    func start(completion: (Never) -> ()) {
        // (Start the app's UI...)
    }
}
class AppDelegate {
    let appCoordinator2 = AppCoordinator2()
    
    func applicationDidFinishLaunching() {
        appCoordinator2.start { _ in }
    }
}

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

Резюме

Этот пост исследует Never Swift на двух реальных примерах. Он иллюстрирует, как мы можем написать невозможный для исполнения код - выраженный через типы. Это снижает потребность в использовании комментариев fatalError и // This should never happen. Написание выразительного, чистого кода важно для создания качественных продуктов, которые можно сопровождать. Более того - приятно!

Благодарности

Я хотел бы поблагодарить Марка Коллетта за помощь в создании примеров, Лизу Тран за редактирование и Кэти Люк за иллюстрацию.