Работа с асинхронными задачами является повседневной задачей разработчика, и разработчики Swift в основном используют замыкания для этого типа работы. Их синтаксис ясен и выразителен, но обработка замыкания может быть подвержена ошибкам, когда мы не адекватно представляем результаты асинхронной операции. Ориентированное на результат программирование направлено на уменьшение этой сложности, предоставляя простой способ представления этих результатов. Эта статья расскажет об основах замыканий и покажет, как использовать Result
в вашем коде.
Все примеры можно найти в этом репозитории Github .
Затворы в Свифте
Замыкания — это автономные блоки кода. Они могут быть переданы и использованы в вашем коде как любой другой объект. Фактически, у замыканий есть типы, точно так же, как Int
или String
являются типами. Их отличает то, что они могут иметь входные параметры, и они должны предоставлять тип возвращаемого значения.
По умолчанию у замыканий нет специальной функции, которая делает их асинхронными. Именно способ их использования заставляет их хорошо играть в асинхронных средах.
Синтаксис
Закрытие определяется его параметрами, типом возвращаемого значения и инструкциями внутри него:
{ (parameters) -> (return type) in (statements) }
Типы параметров закрытия могут быть любого типа Swift. Тип возврата также может быть любым типом Swift. В некоторых случаях типом возврата является Void
и нет необходимости в выражении return
.
Давайте посмотрим, как реализовать простое замыкание, которое вычисляет квадрат числа. Как и любая обычная функция, у вас будет два параметра и одно возвращаемое значение.
let sumNumbers: ((Int, Int) -> Int) = { firstNumber, secondNumber -> Int in return firstNumber + secondNumber }
Внутри тела замыкания может быть любое количество операторов, если возвращается значение возвращаемого типа. В этом случае замыкание возвращает сумму firstNumber
и secondNumber
.
Замыкания могут быть выполнены как любой метод:
sumNumbers(10,4) // returns 14
Как замыкания работают в асинхронной среде
Если вы посмотрите на любое замыкание, вы увидите, что оно инкапсулирует блок кода. Экземпляр замыкания может передаваться по вашему коду и выполняться без каких-либо знаний о его внутренностях. Это делает их идеальными для асинхронной разработки. Типичный вариант использования — это когда вы указываете код, который должен выполняться по возвращении асинхронной операции, и передаете его в качестве параметров. Асинхронный метод выполнит свою работу и выполнит код из вашего замыкания, независимо от того, что он содержит.
Предположим, что мы хотим выполнить наше вычисление квадрата в фоновом режиме и выполнить некоторый произвольный код, который может использовать его результат. Мы передаем закрытие как наш параметр completion
:
func square(of number: Int, completion: @escaping (Int) -> Void)
Как видите, метод square(of number: completion:)
имеет два параметра. Первый параметр — это number
типа Int
которое является квадратом. Вторым является закрытие типа (Int) -> Void
. При выполнении этого метода результат числа в квадрате будет предоставлен как параметр Int
при закрытии. Тип возврата замыкания — Void
поскольку возвращать нечего. И так как он будет вызван после возврата функции, он должен иметь префикс @escaping
.
Давайте посмотрим, как эта функция может быть реализована:
func square(of number: Int, completion: @escaping (Int) -> Void) { OperationQueue().addOperation { let squared = number * number OperationQueue.main.addOperation { completion(squared) } } }
В теле метода сначала вычисляется squared
в фоновом режиме. После этого закрытие completion
вызывается в главном потоке. Поскольку завершение имеет в качестве первого параметра Int
, squared
будет передан этому параметру.
square(of: 5, completion: { squaredNumber in print(squaredNumber) // Prints "25" })
Это изображение показывает, как параметры и возвращаемые значения замыкания передаются.
Необязательные параметры и их проблемы
В некоторых случаях успешное выполнение асинхронной работы может зависеть от различных переменных, таких как сетевое подключение. В этих случаях асинхронные вызовы должны предоставлять информацию об ошибках, которые произошли. Закрытие также должно обеспечить результаты, если вызов был успешным. Поскольку существует множество возможных результатов, замыкания определяют свои параметры как дополнительные.
Например, API URLSession
определяет следующий метод для выполнения сетевых запросов.
func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
Вы можете видеть, что completionHandler
— это замыкание с тремя необязательными параметрами. Работая с ними, вы должны проверить, какие из них равны nil
а какие нет. Если Error
не равна нулю, вам нужно обработать ошибку. Если это так, вам нужно URLResponse
объекты Data
и URLResponse
и работать с ними. Но нет очевидной процедуры для охвата всех возможных успешных и неудачных случаев.
Ориентированное на результат программирование
Ориентированное на результат программирование стремится исправить это, по существу заменяя nil-проверки в опциях перечислением Result<T>
.
Каков Result<T>
?
Result<T>
— это то, как мы представляем возвращаемое значение операции, которая имеет два возможных результата: успешный или неудачный.
Это представление очень похоже на способ реализации дополнительных функций в Swift. Помните, что optimonals может находиться в одном из двух состояний
- Содержит упакованное значение, означающее, что в необязательном экземпляре есть какое-то значение.
- Ничего не содержит — это означает, что дополнительный экземпляр не содержит ничего.
Если вы посмотрите, как дополнительные функции реализованы в Swift, вы увидите, что они на самом деле реализованы как enum:
enum Optional<T> { case some(T) case none }
Optional
, это общее перечисление. Это позволяет нам использовать дополнительные функции с любым типом, например, Optional<Int>
или Optional<UIView>
.
Аналогично, Result<T>
предоставляет два основных случая: success(T)
и error(Error)
. T
— это тип, определенный результатом, а Error
— определение собственной ошибки Swift.
Основное использование Result<T>
Базовая реализация перечисления Result<T>
будет выглядеть так:
enum Result<T> { case success(T) case error(Error) }
Допустим, вы создаете программное обеспечение, которое занимается математическими вычислениями. Некоторые из них могут дать сбой из-за неправильного ввода данных пользователем Классическим примером будет попытка разделить число на ноль (что не допускается фундаментальными правилами математики). Таким образом, если бы вы построили метод деления, который берет два числа и делит их, у вас будет два возможных результата. Либо деление в случае успеха и возвращается номер, либо произошло деление на ноль. Используя Result<T>
мы можем выразить это так:
// Define error that can occur in the computation process enum MathError: Error { case divisionWithZero } func divide(_ first: Double, with second: Double) -> Result<Double> { // Check if divisor is zero // If it is, return error result if second == 0 { return .error(MathError.divisionWithZero) } // Return successful result if second number // is not zero return .success(first / second) }
Чтобы затем выполнить наше деление, мы использовали бы оператор switch
:
let divisonResult = divide(5, with: 0) switch divisionResult { case let .success(value): print("Result of division is \(value)") case let .error(error): print("error: \(error)") }
Расширенное использование Result<T>
Result<T>
enum может быть легко встроен в любую среду разработки. Чтобы понять, как это сделать, мы будем использовать пример манипуляции изображениями.
В этом примере изображение, расположенное по URL-адресу, будет отфильтровано с помощью фильтра сепии и возвращено в Result<UIImage>
объект Result<UIImage>
. Подпись нашего метода будет выглядеть так:
func applyFilterToImage(at urlString: String, completion: @escaping ((Result<UIImage>) -> ()))
Чтобы ускорить этот процесс, мы будем использовать фоновые очереди для загрузки и фильтрации изображений. Таким образом, пользовательский интерфейс приложения не блокируется сетевым запросом. Когда загрузка изображения завершится, фильтрация будет отправлена в основную очередь.
Существует несколько этапов фильтрации изображения с помощью этого метода:
- Создайте объект
URL
из параметраurlString
, чтобы можно было получать данные изображения - Создать фоновую очередь для выполнения сетевых запросов
- Получить двоичные данные изображения из объекта
URL
в фоновой очереди - Проверьте, могут ли
UIImage
данные использоваться для создания объектаUIImage
- Создать
UIImage
из полученных данных - Создать и применить фильтр к главной очереди
- Передайте соответствующий результат через закрытие
Хорошая реализация этой операции должна охватывать все эти шаги и обрабатывать все ошибки, которые могут произойти:
- Параметр строки URL может иметь неправильный формат
- Из-за проблем с сетью загрузка изображений может быть неудачной
- Извлеченные данные могут не быть фактическим изображением
Пример реализации
Давайте сначала определим наши ошибки. Этот пример будет использовать простое перечисление PhotoError
для их определения. Конечно, вы всегда можете использовать ошибки, отличные от тех, которые вы определили.
enum PhotoError: Error { // Invalid url string used case invalidURL(String) // Invalid data used case invalidData }
После ошибок определяется тело метода:
func applyFilterToImage(at urlString: String, completion: @escaping ((Result<UIImage>) -> ())) { // Check if `URL` object can be created from the URL string guard let url = URL(string: urlString) else { completion(.error(PhotoError.invalidURL(urlString))) return } // Create background queue let backgroundQueue = DispatchQueue.global(qos: .background) // Dispatch to background queue backgroundQueue.async { do { let data = try Data(contentsOf: url) // Check if `UIImage` object can be constructed with data guard let image = UIImage(data: data) else { completion(.error(PhotoError.invalidData)) return } // Dispatch filtering to main queue DispatchQueue.main.async { // Crate sepia filter let filter = CIFilter(name: "CISepiaTone")! // Setup filter options let inputImage = CIImage(image: image) filter.setDefaults() filter.setValue(inputImage, forKey: kCIInputImageKey) // Set input image // Get filtered image let filteredImage = UIImage(ciImage: filter.outputImage!) // Return successful result completion(.success(filteredImage)) } } catch { // Dispatch error completion to main queue DispatchQueue.main.async { completion(.error(error)) } } } }
Как видите, методы, которые могут генерировать ошибки, инкапсулированы в блоке do { } catch { }
. Сначала создается объект URL
. Если urlString
недействителен, PhotoError.invalidURL
будет возвращено. Этот шаблон проверки ошибок следует остальной части метода. Если каждая операция прошла успешно, будет возвращен успешный результат с отфильтрованным изображением.
Допустим, мы хотим использовать этот метод на локальной фотографии с именем landscape.jpeg
. Мы создаем imageURL
и затем выполняем метод applyFilterToImage
. Чтобы проверить успешность фильтрации изображений, мы можем использовать оператор switch
. Если результат успешен, связанный с UIImage
объект UIImage
можно использовать как любой другой объект.
let imageURL = Bundle.main.path(forResource: "landscape", ofType: "jpeg")! applyFilterToImage(at: imageURL) { result in switch result { case let .success(image): let someImageView = UIImageView() someImageView.image = image case let .error(error): print(error) } }
Вывод
Ориентированное на результат программирование является важным инструментом для любого разработчика Swift. Это может сделать написание и обработку асинхронного кода более простым и интуитивно понятным. Идея, лежащая в основе этого, очень проста и понятна даже для начинающих.