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

Дженерики дают вызывающему абоненту

право выбора типа. Мы говорим, что дженерики создают свободный контракт с внешней областью видимости. Автор универсальной функции должен создать границы того, как будут использоваться универсальные типы. Давайте посмотрим на пример из языка Swift…

func type<T, Metatype>(of value: T) -> Metatype

//Declare a generic inside of <> brackets
// Here we have 2 types T and Metatype

Функция type‹of: T, Metatype› имеет 2 универсальных типа; T и Метатип. Эта функция определяет свободный контракт, поскольку у нас нет ограничений на тип ввода для типа T .

Теперь давайте посмотрим на пользовательскую универсальную функцию для добавления значений…

func add<T: Numeric>(lhs: T, rhs: T) -> T {

    return lhs + rhs
}

let result = add(lhs: 2, rhs: 3)

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

Непрозрачные типы переворачивают концепцию дженериков

определяя и ограничивая возвращаемый тип. Руководство по языку программирования Swift говорит

«Непрозрачный тип позволяет реализации функции выбирать тип для возвращаемого значения способом, который абстрагируется от кода, вызывающего функцию».

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

func add(lhs: Int, rhs: Int) -> some Numeric {

    return lhs + rhs
}

Теперь функция определяет, какой тип параметра вы можете назначить, требуя два целых числа, и разрешает отображать только числовой интерфейс на выходе.

В Руководстве по языку программирования Swift говорится, что…

«Вы можете думать о непрозрачном типе как об обратном универсальному типу…»
Язык программирования Swift (Swift 5.7)
Apple Inc.

Возможно, нам следует сосредоточиться на последней части определения непрозрачного типа: «…возврат осуществляется абстрагировано от кода, вызывающего функцию».

Возвращая некоторое числовое значение, мы можем стереть Int. В Руководстве по языку программирования Swift говорится, что…

«Скрытие информации о типе полезно на границах между модулем и кодом, который вызывает модуль, потому что базовый тип возвращаемого значения может оставаться закрытым».

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

Например, посмотрите на разницу между целочисленным интерфейсом из нашего первого примера и числовым интерфейсом, возвращенным из нашего второго примера.

Вы можете смешивать непрозрачные и общие

для большей гибкости. Давайте проверим этот пример, где мы решили, что протокол Numeric предоставляет слишком много информации. Numeric наследуется от AdditiveArithmetic и ExpressibleByIntegerLiteral. Мы можем ввести стирание ExpressibleByIntegerLiteral, используя дженерики и непрозрачность.

func add<T: Numeric>(lhs: T, rhs: T) -> some AdditiveArithmetic {

    return lhs + rhs
}

Этот метод эффективно удаляет доступ к ExpressibleByIntegerLiteral у вызывающего объекта. Обратите внимание, что хотя доступ к вызывающему объекту удален, компилятор по-прежнему поддерживает запись, что Numerics были задействованы. Другими словами, для вызывающего объекта произошло стирание типа, но для компилятора сохраняется исходная информация о типе. Мы можем доказать это так…

//A Generic function with an opaque return type 
func add<T: Numeric>(lhs: T, rhs: T) -> some AdditiveArithmetic {

    return lhs + rhs
}

//Add two Int Types
let num1 = add(lhs: 4, rhs: 2)
//Add two Int Types
let num2 = add(lhs: 4, rhs: 2)

let newNum = num1 + num2
print(newNum)
//num returns a value of 12 to the caller

print(type(of: num1))
// Int

//Recover the concrete Int
print(newNum as? Int)

В приведенном выше случае компилятор позволяет нам добавить num1 и num2. Мы также показали, что конкретный тип может быть восстановлен. Он сохранил доступ к Self для типа Int вместе с дополнительными метаданными из самого вызова функции.

На самом деле, если бы вы написали одну и ту же функцию дважды, вы бы увидели, что типы, которые вы ожидаете найти, не совпадают...

func adder<T: Numeric>(lhs: T, rhs: T) -> some AdditiveArithmetic {

    return lhs + rhs
}

func add<T: Numeric>(lhs: T, rhs: T) -> some AdditiveArithmetic {

    return lhs + rhs
}

let num1 = adder(lhs: 3, rhs: 3)
let num2 = add(lhs: 1, rhs: 1)

let newNum = num1 + num2
//Cannot convert value of type 'some AdditiveArithmetic' 
//(result of 'adder(valueOne:valueTwo:)') to expected argument 
//type 'some AdditiveArithmetic' (result of 'add(valueOne:valueTwo:)')

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

Экзистенциальный тип

упоминается только один раз в Руководстве по программированию на Swift…

Использование протокола в качестве типа иногда называют экзистенциальным типом, что происходит от фразы «существует такой тип T, что T соответствует протоколу.
https://itunes.apple.com /WebObjects/MZStore.woa/wa/viewBook?id=0

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

//A Generic function with an existential return type 
func add<T: Numeric>(lhs: T, rhs: T) -> any AdditiveArithmetic {

    return lhs + rhs
}

//Add two Int Types
let num1 = add(lhs: 4, rhs: 2)
//Add two Int Types
let num2 = add(lhs: 4, rhs: 2)

print(num + num2)
//Error: Cannot convert value of type 'any AdditiveArithmetic' 
//to expected argument type 'AttributedString'

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

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

Продвинутый Swift
Крис Эйдхоф

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

Экзистенциальный контейнер также несет некоторые дополнительные накладные расходы, поэтому, если вы можете, рекомендуется сначала использовать дженерики и непрозрачные объекты.

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

Продвинутый Swift
Крис Эйдхоф

Последний пример, который подчеркивает различия

между непрозрачным и экзистенциальным типами.

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

//Opaque
let nums: [some AdditiveArithmetic] = [Int(2), Double(3.9), CGFloat(1.2)]
//Error: Cannot convert value of type 'Int' to expected element type 'Double'

//Existential
let nums: [any AdditiveArithmetic] = [Int(2), Double(3.9), CGFloat(1.2)]
//Compiles

Так что же происходит? Непрозрачные результаты могут возвращать только один тип. Компилятор сохраняет информацию о Integer с индексом 0, а затем сразу же сталкивается с Double с индексом 1. Это несоответствие информации прерывает компиляцию.

В экзистенциальной версии компилятор заботится только о том, чтобы каждый из индексов соответствовал AdditiveArithmetic. Компилятор не сохранил достаточно информации для формирования отношений между каждым индексом. Экзистенциальные типы касаются только достаточных требований протокола.

Ну вот и все на сегодня! На эту тему есть еще много чего. Другие люди понимают это глубже и объясняют яснее. Моя цель состояла в том, чтобы просто добавить его на ваш радар. Удачного кодирования!

👨‍💻 Читайте мой самый популярный блог о стиле Swift Code



Ссылки:
https://www.objc.io/books/advanced-swift/

https://docs.swift.org/swift-book/LanguageGuide/TheBasics.html