Привет, Хабр. В рамках стартующего в феврале курса «iOS Developer. Professional» подготовили для вас перевод полезного материала.

Также предлагаем принять участие в
открытом вебинаре на тему «Пишем приложение на SwiftUI и Combine». Участники вместе с экспертом разберут, что такое SwiftUI и фреймворк Combine, а также как с их помощью создать небольшое приложение.

Вы также можете прочитать эту статью в моем блоге Xcoding With Alfian, перейдя по ссылке.

Opaque return types (непрозрачные типы) — это новая языковая конструкция, представленная Apple в Swift 5.1. Их можно использовать для возврата некоторого (some) значения функции (function)/метода (method) и свойства (property), не раскрывая конкретный тип значения клиенту, который вызывает API. Тип возврата будет некоторым типом, реализующим протокол (protocol). С помощью этого решения API-интерфейс модуля больше не должен публично раскрывать базовый внутренний возвращаемый тип метода, ему просто нужно вернуть opaque type протокола с помощью ключевого слова some. Компилятор Swift также сможет сохранить базовую идентичность (identity) возвращаемого типа, в отличии от варианта с использованием протокола в качестве возвращаемого типа. SwiftUI использует opaque return typesвнутри своего протокола View, который возвращает some View в свойстве body.

Вот некоторые из важных возможностей, предоставляемых opaque return types, которые закрепят их в нашем наборе инструментов и будут использоваться всякий раз, когда мы захотим создать API с использованием Swift:

  1. Возможность предоставить определенный тип протокола, не раскрывая конкретный (concrete) тип вызывающему API для лучшей инкапсуляции (encapsulation).

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

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

  4. Из-за строгой гарантии возврата определенного типа протокола. Функция может возвращать opaque protocol type, который имеет в требовании Self или associated type.

  5. Протокол оставляет решение возвращать тип вызывающему объекту функции. В обратном случае для opaque return types сама функция принимает решение для конкретного типа возвращаемого значения, если она реализует протокол.

Пример использования Opaque return types

Чтобы понять больше о opaque return type и почему он отличается от простого использования протокола в качестве возвращаемого типа, давайте погрузимся в код примеров того, как мы можем его использовать.

Объявление протокола с associatedtype

Допустим, у нас есть протокол под названием MobileOS. Этот протокол имеет associatedtype (связанный тип) называемый Version и свойство, позволяющее получить Version, для конкретного типа, который необходимо реализовать.

protocol MobileOS {
    associatedtype Version
    var version: Version { get }
    init(version: Version)
}

Реализация конкретных типов протокола

Определим два конкретных типа для этого протокола: iOS и Android. Оба они имеют разную семантику версий. IOS использует тип float, а Android использует String (хотя недавно они изменили его в Android 10).

struct iOS: MobileOS {
    var version: Float
}
struct Android: MobileOS {
    var version: String
}

Создание функции для возврата типа протокола

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

Решение 1 (возвращает тип протокола):

func buildPreferredOS() -> MobileOS {
    return iOS(version: 13.1)
}
// ОШИБКА компилятора
Протокол 'MobileOS' может использоваться только как общее ограничение, 
потому что у него есть требования к типу Self или associated type.

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

Решение 2 (возвращает конкретный тип):

func buildPreferredOS() -> iOS {
    return iOS(version: 13.1)
}
// Сборка прошла успешно

Это решение работает, но, как вы можете видеть, API теперь передает вызывающей стороне конкретный тип. Этот код потребует серьезного рефакторинга, если в будущем мы передумаем и вернем Android в качестве возвращаемого типа функции.

Решение 3 (Generic Function Return)

func buildPreferredOS<T: MobileOS>(version: T.Version) -> T {
    return T(version: version)
}
let android: Android =  buildPreferredOS(version: "Jelly Bean")
let ios: iOS = buildPreferredOS(version: 5.0)

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

Окончательное решение (на помощь приходит Opaque Return Type)

func buildPreferredOS() -> some MobileOS {
    return iOS(version: 13.1)
}

Используя opaque return type, мы, наконец, можем вернуть MobileOS в качестве типа возврата функции. Здесь компилятор поддерживает идентичность базового конкретного возвращаемого типа, и вызывающей стороне не обязательно знать внутренний тип возвращаемого типа, если он реализует протокол MobileOS.

Opaque returns types могут возвращать только один конкретный тип

Возможно, вы думаете, что как и тип возвращаемого значения протокола, мы также можем возвращать конкретное значение другого типа внутри opaque return type, например.

func buildPreferredOS() -> some MobileOS {
   let isEven = Int.random(in: 0...100) % 2 == 0
   return isEven ? iOS(version: 13.1) : Android(version: "Pie")
}
// ОШИБКА компилятора
Невозможно преобразовать возвращаемое выражение типа 'iOS' 
в возвращаемое выражение типа 'some MobileOS'

func buildPreferredOS() -> some MobileOS {
   let isEven = Int.random(in: 0...100) % 2 == 0
   return isEven ? iOS(version: 13.1) : iOS(version: "13.0")
}
// Сборка прошла успешно 

Компилятор вернет ошибку сборки, если вы пытаетесь вернуть другой конкретный тип для opaque return value. Однако вы все равно можете вернуть другое значение того же конкретного типа.

Упрощение сложных и вложенных типов в opaque return type для вызывающего API

Последний пример opaque return value — действительно хороший пример того, как мы можем использовать Opaque return type, чтобы скрыть сложный и вложенный типы в простой opaque protocol type, который может быть представлен клиенту.

Рассмотрим функцию, которая принимает массив, который использует generic constraint для своего элемента, чтобы соответствовать протоколу numeric. Этот массив выполняет несколько функций:

  1. Отбрасывает элементы головы и хвоста из массива.

  2. Лениво сопоставьте функцию, выполняя операцию умножения самого себя для каждого элемента.

Вызывающей стороне этого API не нужно знать тип возвращаемого значения функции, вызывающая сторона просто хочет выполнить цикл for и вывести (print) значение последовательности на консоль.

Давайте реализуем это с помощью простой функции.

Решение 1. Использование Generic Return Function

func sliceFirstAndEndSquareProtocol<T: Numeric>(array: Array<T>) -> LazyMapSequence<ArraySlice<T>, T> {
   return array.dropFirst().dropLast().lazy.map { $0 * $0 }
}
sliceFirstAndEndSquareProtocol(array: [2,3,4,5]).forEach { print($0) }
// 9
// 16

Как вы можно увидеть, тип возвращаемого значения этой функции очень сложен и вложен LazyMapSequence, T>, в то время как клиент использует его только для печати каждого элемента в цикле.

Решение 2. Простые Opaque Return Types

func sliceHeadTailSquareOpaque<T: Numeric>(array: Array<T>) -> some Sequence {
    return array.dropFirst().dropLast().lazy.map { $0 * $0 }
}
sliceHeadTailSquareOpaque(array: [3,6,9]).forEach { print($0) }
// 36

Используя это решение, клиент не нужно знать о базовом возвращаемом типе функции, если он соответствует протоколу sequence, который может использоваться клиентом.

Opaque Return Types в SwiftUI

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

struct Row: View {
    var body: some View {
        HStack {
           Text("Hello SwiftUI")
           Image(systemName: "star.fill")
        }
    }
}

Предполагаемый тип возврата body:

HStack<TupleView<(Text, Image)>>

Он достаточно сложен и вложен, помните, что он также будет меняться всякий раз, когда мы добавляем новое вложенное view внутри HStack. Opaque return type предстает во всей красе в реализации SwiftUI. Пользователя API не сильно заботит базовый конкретный типа в View, покуда возвращаемый тип соответствует протоколу View.

Дополнительные сведения о Opaque return type

Чтобы узнать больше о opaque return type, вы можете проследовать по ссылкам, которые я привел ниже. Это официальное предложение, документация и видео от Apple.

apple/swift-evolution

Это предложение является первой частью группы изменений, которые мы рассматриваем в проектном документе для улучшения пользовательского интерфейса…

github.com

Opaque Types — язык программирования Swift (Swift 5.1)

Функция или метод с opaque return type скрывает информацию о типе возвращаемого значения. Вместо предоставления…

docs.swift.org

Что нового в Swift - WWDC 2019 - Видео - Apple Developer

Swift теперь является главным языком для ряда основных фреймворков на всех платформах Apple, включая…

developer.apple.com

Заключение

Это было очень удивительное и невероятное путешествием для нас, разработчиков Swift, которые изучали и применяли Swift с момента его изначального релиза на WWDC 2014. Если вспомнить его тогда, в этом языке не было ряда удивительных фич, которые открывают новую парадигму построения API, например расширение протокола, генерик в протоколе с associatedtype и даже такие интересные фичи, как протокол Codable для упрощения декодирования и кодирования модели определенного типа данных.

Хотя бы только потому, что Apple выпускает Swift в качестве языка программирования с открытым исходным кодом, мы можем использовать силу коллективности разработчиков со всего мира, которые хотят улучшить язык с помощью предложений Swift Evolution. Существует даже реализация предложения, которая была принята в Swift Language от старшеклассника (синтезированный инициализатор по умолчанию для struct в Swift 5.1 от Алехандро Алонсо).

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


Узнать подробнее о курсе «iOS Developer. Professional».

Смотреть открытый вебинар «Пишем приложение на SwiftUI и Combine».