Непрозрачные типы
Функция или метод с непрозрачным типом возвращаемого значения скрывает информацию о типе своего возвращаемого значения. Вместо того, чтобы указывать конкретный тип в качестве типа возвращаемого значения функции, возвращаемое значение описывается в терминах поддерживаемых им протоколов. Сокрытие информации о типе полезно на границах между модулем и кодом, который вызывает модуль, поскольку базовый тип возвращаемого значения может оставаться закрытым. В отличие от возврата значения, тип которого является типом протокола, непрозрачные типы сохраняют идентичность типа - компилятор имеет доступ к информации о типе, но клиенты модуля - нет.
Проблема, которую решают непрозрачные типы
Предположим, что вы пишете модуль, который рисует фигуры в формате ASCII. Основной характеристикой фигуры ASCII art является функция draw(), которая возвращает строковое представление этой фигуры, которое вы можете использовать в качестве требования для протокола Shape:
protocol Shape {
func draw() -> String
}
struct Triangle: Shape {
var size: Int
func draw() -> String {
var result: [String] = []
for length in 1...size {
result.append(String(repeating: "*", count: length))
}
return result.joined(separator: "\n")
}
}
let smallTriangle = Triangle(size: 3)
print(smallTriangle.draw())
// *
// **
// ***
Вы можете использовать универсальные шаблоны для реализации таких операций, как вертикальное переворачивание фигуры, как показано в приведенном ниже коде. Однако у этого подхода есть важное ограничение: перевернутый результат показывает точные универсальные типы, которые использовались для его создания.
struct FlippedShape: Shape {
var shape: T
func draw() -> String {
let lines = shape.draw().split(separator: "\n")
return lines.reversed().joined(separator: "\n")
}
}
let flippedTriangle = FlippedShape(shape: smallTriangle)
print(flippedTriangle.draw())
// ***
// **
// *
Этот подход к определению структуры JoinedShape<T: Shape, U: Shape>, которая объединяет две фигуры вместе по вертикали, как показано в приведенном ниже коде, приводит к таким типам, как JoinedShape<FlippedShape <Triangle>, Triangle>, от соединения перевернутого треугольника с другим треугольником.
struct JoinedShape: Shape {
var top: T
var bottom: U
func draw() -> String {
return top.draw() + "\n" + bottom.draw()
}
}
let joinedTriangles = JoinedShape(top: smallTriangle, bottom: flippedTriangle)
print(joinedTriangles.draw())
// *
// **
// ***
// ***
// **
// *
Предоставление подробной информации о создании формы позволяет типам, которые не предназначены для использования в общедоступном интерфейсе художественного модуля ASCII, просачиваться наружу из-за необходимости указывать полный тип возвращаемого значения. Код внутри модуля может создавать одну и ту же форму различными способами, и другой код вне модуля, который использует эту форму, не должен учитывать детали реализации списка преобразований. Оберточные типы, такие как JoinedShape и FlippedShape, не имеют значения для пользователей модуля и не должны быть видны. Открытый интерфейс модуля состоит из таких операций, как соединение и отражение фигуры, и эти операции возвращают другое значение Shape.
Возвращение непрозрачного типа
Вы можете думать о непрозрачном типе как о противоположности универсального типа. Универсальные типы позволяют коду, вызывающему функцию, выбирать тип для параметров этой функции и возвращать значение таким образом, чтобы абстрагироваться от реализации функции. Например, функция в следующем коде возвращает тип, зависящий от вызывающего ее кода:
func max(_ x: T, _ y: T) -> T where T: Comparable { ... }
Код, вызывающий max(_: _ :), выбирает значения для x и y, и тип этих значений определяет конкретный тип T. Вызывающий код может использовать любой тип, соответствующий протоколу Comparable. Код внутри функции написан в общем виде, поэтому он может обрабатывать любой тип, который предоставляет вызывающий. Реализация max(_: _ :) использует только функциональные возможности, общие для всех типов Comparable.
Эти роли меняются местами для функции с непрозрачным возвращаемым типом. Непрозрачный тип позволяет реализации функции выбирать тип для возвращаемого значения таким образом, чтобы абстрагироваться от кода, вызывающего функцию. Например, функция в следующем примере возвращает трапецию, не раскрывая базовый тип этой формы.
struct Square: Shape {
var size: Int
func draw() -> String {
let line = String(repeating: "*", count: size)
let result = Array(repeating: line, count: size)
return result.joined(separator: "\n")
}
}
func makeTrapezoid() -> some Shape {
let top = Triangle(size: 2)
let middle = Square(size: 2)
let bottom = FlippedShape(shape: top)
let trapezoid = JoinedShape(
top: top,
bottom: JoinedShape(top: middle, bottom: bottom)
)
return trapezoid
}
let trapezoid = makeTrapezoid()
print(trapezoid.draw())
// *
// **
// **
// **
// **
// *
Функция makeTrapezoid() в этом примере объявляет свой возвращаемый тип как некоторую форму; в результате функция возвращает значение некоторого заданного типа, которое соответствует протоколу Shape, без указания какого-либо конкретного типа. Такой способ написания makeTrapezoid() позволяет выразить фундаментальный аспект своего публичного интерфейса - значение, которое оно возвращает, является формой - без создания конкретных типов, которые создаются на основе части публичного интерфейса. В этой реализации используются два треугольника и квадрат, но функцию можно переписать для рисования трапеции множеством других способов без изменения ее возвращаемого типа.
В этом примере показано, как непрозрачный возвращаемый тип похож на обратный тип универсального типа. Код внутри makeTrapezoid() может возвращать любой тип, который ему нужен, если этот тип соответствует протоколу Shape, как вызывающий код для универсальной функции. Код, вызывающий функцию, должен быть написан в общем виде, как реализация универсальной функции, чтобы он мог работать с любым значением Shape, возвращаемым makeTrapezoid().
Вы также можете комбинировать непрозрачные возвращаемые типы с универсальными. Обе функции в следующем коде возвращают значение некоторого типа, соответствующего протоколу Shape.
func flip(_ shape: T) -> some Shape {
return FlippedShape(shape: shape)
}
func join(_ top: T, _ bottom: U) -> some Shape {
JoinedShape(top: top, bottom: bottom)
}
let opaqueJoinedTriangles = join(smallTriangle, flip(smallTriangle))
print(opaqueJoinedTriangles.draw())
// *
// **
// ***
// ***
// **
// *
Значение opaqueJoinedTriangles в этом примере такое же, как и connectedTriangles в примере универсальных шаблонов в разделе «Проблема, которую решают непрозрачные типы» ранее в этой главе. Однако, в отличие от значения в этом примере, функции flip(_ :) и join(_: _ :) заключают в оболочку базовые типы, возвращаемые операциями универсальной формы, в непрозрачный тип возвращаемого значения, что предотвращает отображение этих типов. Обе функции являются универсальными, поскольку типы, на которые они опираются, являются универсальными, а параметры типа функции передают информацию о типе, необходимую для FlippedShape и JoinedShape.
Если функция с возвращаемым непрозрачным типом возвращается из нескольких мест, все возможные возвращаемые значения должны иметь один и тот же тип. Для универсальной функции этот возвращаемый тип может использовать параметры универсального типа функции, но он все равно должен быть одного типа. Например, вот недопустимая версия функции переворота формы, которая включает специальный случай для квадратов:
func invalidFlip(_ shape: T) -> some Shape {
if shape is Square {
return shape // Ошибка: несоответствующий возвращаемый тип
}
return FlippedShape(shape: shape) // Ошибка: несоответствующий возвращаемый тип
}
Если вы вызываете эту функцию с помощью Square, она возвращает Square; в противном случае она возвращает FlippedShape. Это нарушает требование возвращать значения только одного типа и делает код invalidFlip(_ :) недопустимым. Один из способов исправить invalidFlip(_ :) - переместить специальный случай для квадратов в реализацию FlippedShape, которая позволяет этой функции всегда возвращать значение FlippedShape:
struct FlippedShape: Shape
var shape: T
func draw() -> String {
if shape is Square {
return shape.draw()
}
let lines = shape.draw().split(separator: "\n")
return lines.reversed().joined(separator: "\n")
}
}
Требование всегда возвращать один тип не мешает вам использовать универсальные шаблоны в непрозрачном возвращаемом типе. Вот пример функции, которая включает параметр типа в базовый тип возвращаемого значения:
func `repeat`(shape: T, count: Int) -> some Collection {
return Array(repeating: shape, count: count)
}
В этом случае базовый тип возвращаемого значения зависит от T: какая бы фигура ни была передана, repeat(shape: count :) создает и возвращает массив этой формы. Тем не менее, возвращаемое значение всегда имеет один и тот же базовый тип [T], поэтому следует требование, чтобы функции с непрозрачными типами возврата должны возвращать значения только одного типа.
Различия между типом протокола и непрозрачным типом
Возврат непрозрачного типа очень похож на использование типа протокола в качестве типа возвращаемого значения функции, но эти два вида возвращаемого типа различаются тем, что по-разному работают с идентичностью типа. Непрозрачный тип относится к одному конкретному типу, хотя вызывающая функция не может видеть конкретно что это за тип. Тип протокола может относиться к любому типу, который соответствует протоколу. Вообще говоря, типы протоколов дают вам больше гибкости в отношении базовых типов значений, которые они хранят, а непрозрачные типы позволяют вам делать более строгие гарантии в отношении этих базовых типов.
Например, вот версия protoFlip(_ :), которая использует тип протокола в качестве возвращаемого типа вместо непрозрачного типа возврата:
func protoFlip(_ shape: T) -> Shape {
return FlippedShape(shape: shape)
}
Эта версия protoFlip(_ :) имеет то же тело, что и flip(_ :), и всегда возвращает значение того же типа. В отличие от flip(_ :), значение, которое возвращает protoFlip(_ :), не обязательно должно всегда иметь один и тот же тип - оно просто должно соответствовать протоколу Shape. Другими словами, protoFlip(_ :) делает гораздо более свободный контракт API со своим вызывающим, чем flip(_ :). Эта функция оставляет за собой возможность возвращать значения нескольких типов:
func protoFlip(_ shape: T) -> Shape {
if shape is Square {
return shape
}
return FlippedShape(shape: shape)
}
Обновленная версия кода возвращает экземпляр Square или экземпляр FlippedShape, в зависимости от того, какая фигура передана. Две перевернутые фигуры, возвращаемые этой функцией, могут иметь совершенно разные типы. Другие допустимые версии этой функции могут возвращать значения разных типов при отражении нескольких экземпляров одной и той же формы. Менее конкретная информация о типе возвращаемого значения из protoFlip(_ :) означает, что многие операции, зависящие от информации о типе, недоступны для возвращаемого значения. Например, невозможно написать оператор == для сравнения результатов, возвращаемых этой функцией.
let protoFlippedTriangle = protoFlip(smallTriangle)
let sameThing = protoFlip(smallTriangle)
protoFlippedTriangle == sameThing // Ошибка
Ошибка в последней строке примера возникает по нескольким причинам. Непосредственная проблема заключается в том, что Shape не включает оператор == как часть требований протокола. Если вы попытаетесь добавить его, то следующая проблема, с которой вы столкнетесь, заключается в том, что оператору == необходимо знать типы своих левых и правых аргументов. Операторы такого типа обычно принимают аргументы типа Self, соответствующие любому конкретному типу, принимающему протокол, но добавление к протоколу требования Self не допускает "стирание" типа, которое происходит, когда вы используете протокол как тип.
Использование типа протокола в качестве типа возвращаемого значения для функции дает вам возможность возвращать любой тип, соответствующий протоколу. Однако цена такой гибкости заключается в том, что некоторые операции с возвращаемыми значениями невозможны. В примере показано, что оператор == недоступен - это зависит от конкретной информации о типе, которая не сохраняется при использовании типа протокола.
Еще одна проблема с этим подходом заключается в том, что преобразования формы не вкладываются. Результатом переворота треугольника является значение типа Shape, а функция protoFlip(_ :) принимает аргумент некоторого типа, который соответствует протоколу Shape. Однако значение типа протокола не соответствует этому протоколу; значение, возвращаемое protoFlip(_ :), не соответствует Shape. Это означает, что такой код, как protoFlip(protoFlip (smallTriange)), который применяет несколько преобразований, недействителен, поскольку перевернутая форма не является допустимым аргументом для protoFlip(_ :).
Напротив, непрозрачные типы сохраняют идентичность базового типа. Swift может определять связанные типы, что позволяет использовать непрозрачное возвращаемое значение в тех местах, где тип протокола не может использоваться в качестве возвращаемого значения. Например, вот версия протокола контейнера из раздела "Универсальные шаблоны":
protocol Container {
associatedtype Item
var count: Int { get }
subscript(i: Int) -> Item { get }
}
extension Array: Container { }
Вы не можете использовать Container в качестве возвращаемого типа функции, потому что у этого протокола есть связанный тип. Вы также не можете использовать его в качестве ограничения в универсальном возвращаемом типе, потому что за пределами тела функции недостаточно информации, чтобы сделать вывод, каким должен быть универсальный тип.
Ошибка: Протоколы со связанными типами не могут быть использованы в качестве возвращаемого типа.
func makeProtocolContainer(item: T) -> Container {
return [item]
}
// Ошибка: Недостаточно информации для определения типа C.
func makeProtocolContainer(item: T) -> C {
return [item]
}
Использование непрозрачного типа some Container в качестве возвращаемого типа выражает желаемый контракт API - функцию возвращающую контейнер, но не указывающую его тип:
func makeOpaqueContainer(item: T) -> some Container {
return [item]
}
let opaqueContainer = makeOpaqueContainer(item: 12)
let twelve = opaqueContainer[0]
print(type(of: twelve))
// Выведет "Int"
Тип значения twelve считается Int, что иллюстрирует тот факт, что вывод типа работает с непрозрачными типами. В реализации makeOpaqueContainer(item :) базовый тип непрозрачного контейнера - [T]. В этом случае T - это Int, поэтому возвращаемое значение представляет собой массив целых чисел, а связанный с Item тип выводится как Int. Нижний индекс в Container возвращает Item, что означает, что тип twelve также определяется как Int.