Unetway

Swift - Непрозрачные типы

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

Проблема, которую решают непрозрачные типы

Например, предположим, что вы пишете модуль, который рисует художественные формы ASCII. Основной характеристикой художественной фигуры ASCII является 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<T: Shape>: 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<T: Shape, U: Shape>: 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<T>(_ 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<String>(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()Функция в этом примере объявляет тип возвращаемого в качестве ; в результате функция возвращает значение некоторого данного типа, которое соответствует протоколу, без указания какого-либо конкретного конкретного типа. Написание этого способа позволяет ему выражать фундаментальный аспект своего открытого интерфейса - возвращаемое им значение является формой - без создания определенных типов, которые эта форма создается из части своего открытого интерфейса. Эта реализация использует два треугольника и квадрат, но функцию можно переписать, чтобы нарисовать трапецию множеством других способов без изменения ее типа возврата.some ShapeShapemakeTrapezoid()

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

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

func flip<T: Shape>(_ shape: T) -> some Shape {

return FlippedShape(shape: shape)

}

func join<T: Shape, U: Shape>(_ top: T, _ bottom: U) -> some Shape {

JoinedShape(top: top, bottom: bottom)

}



let opaqueJoinedTriangles = join(smallTriangle, flip(smallTriangle))

print(opaqueJoinedTriangles.draw())

// *

// **

// ***

// ***

// **

// *

Значение opaqueJoinedTrianglesв этом примере такое же, как joinedTriangles. Однако, в отличие от значения в этом примере, flip(_:)и join(_:_:)оберните базовые типы, которые универсальные операции формы возвращают в непрозрачный тип возвращаемого значения, что предотвращает отображение этих типов. Обе функции являются общими, потому что типы, на которые они полагаются, являются общими, а параметры типа функции передают информацию о типе, необходимую для FlippedShapeи JoinedShape.

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

func invalidFlip<T: Shape>(_ shape: T) -> some Shape {

if shape is Square {

return shape // Error: return types don't match

}

return FlippedShape(shape: shape) // Error: return types don't match

}

Если вы вызываете эту функцию с помощью a Square, она возвращает a Square; в противном случае возвращается FlippedShape. Это нарушает требование возвращать значения только одного типа и делает invalidFlip(_:)неверный код. Один из способов исправить invalidFlip(_:)это переместить специальный случай квадратов в реализацию FlippedShape, что позволяет этой функции всегда возвращать FlippedShapeзначение:

struct FlippedShape<T: Shape>: 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`<T: Shape>(shape: T, count: Int) -> some Collection {

return Array<T>(repeating: shape, count: count)

}

В этом случае базовый тип возвращаемого значения варьируется в зависимости от того, Tкакая форма ему передана, repeat(shape:count:)создает и возвращает массив этой формы. Тем не менее, возвращаемое значение всегда имеет один и тот же базовый тип [T], поэтому следует требование, чтобы функции с непрозрачными возвращаемыми типами возвращали значения только одного типа.

Различия между непрозрачными типами и типами протоколов

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

Например, вот версия, flip(_:)которая возвращает значение типа протокола вместо использования непрозрачного типа возврата:

func protoFlip<T: Shape>(_ shape: T) -> Shape {

return FlippedShape(shape: shape)

}

Эта версия protoFlip(_:)имеет то же тело flip(_:), что и всегда возвращает значение того же типа. В отличие flip(_:)от этого, protoFlip(_:)возвращаемое значение не обязательно должно всегда иметь один и тот же тип - оно просто должно соответствовать Shapeпротоколу. Другими словами, protoFlip(_:)контракт с API для вызывающего абонента flip(_:)становится намного более свободным, чем для make. Он оставляет за собой возможность возвращать значения нескольких типов:

func protoFlip<T: Shape>(_ 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 // Error

Ошибка в последней строке примера возникает по нескольким причинам. Непосредственная проблема заключается в том, что оператор Shapeне включает в себя ==часть своих требований к протоколу. Если вы попытаетесь добавить его, следующая проблема, с которой вы столкнетесь, заключается в том, что ==оператору нужно знать типы его левых и правых аргументов. Операторы такого типа обычно принимают аргументы типа Self, соответствующие любому конкретному типу, принимающему протокол, но добавление Selfтребования к протоколу не допускает стирания типа, которое происходит при использовании протокола в качестве типа.

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

Другая проблема с этим подходом состоит в том, что преобразования формы не вкладываются. Результатом переворота треугольника является значение типа Shape, и protoFlip(_:)функция принимает аргумент некоторого типа, который соответствует Shapeпротоколу. Однако значение типа протокола не соответствует этому протоколу; возвращаемое значение protoFlip(_:)не соответствует Shape. Это означает, protoFlip(protoFlip(smallTriange))что подобный код применяет несколько преобразований, недопустим, потому что перевернутая фигура не является допустимым аргументом для protoFlip(_:).

Напротив, непрозрачные типы сохраняют идентичность базового типа. Swift может выводить связанные типы, что позволяет использовать непрозрачное возвращаемое значение в тех местах, где тип протокола не может быть использован в качестве возвращаемого значения. Например, вот версия Containerпротокола от Generics :

protocol Container {

associatedtype Item

var count: Int { get }

subscript(i: Int) -> Item { get }

}

extension Array: Container { }

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

// Error: Protocol with associated types can't be used as a return type.

func makeProtocolContainer<T>(item: T) -> Container {

return [item]

}



// Error: Not enough information to infer C.

func makeProtocolContainer<T, C: Container>(item: T) -> C {

return [item]

}

Использование непрозрачного типа в качестве возвращаемого типа выражает желаемый контракт API - функция возвращает контейнер, но отказывается указывать тип контейнера:some Container

func makeOpaqueContainer<T>(item: T) -> some Container {

return [item]

}

let opaqueContainer = makeOpaqueContainer(item: 12)

let twelve = opaqueContainer[0]

print(type(of: twelve))

// Prints "Int"

Предполагается, что тип twelveis указывает Intна то, что вывод типов работает с непрозрачными типами. В реализации makeOpaqueContainer(item:), базовым типом непрозрачного контейнера является [T]. В этом случае, Tесть Int, поэтому возвращаемое значение является массивом целых чисел, и Itemсвязанный тип выводится как Int. Индекс по Containerвозвращениям Item, который означает, что тип twelveтакже подразумевается как Int.