Spec-Zone.ru › Swift Language

Непрозрачные и упакованные типы протоколов

Скрыть детали реализации типа значения.

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<Triangle, FlippedShape<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 Shape; в результате функция возвращает значение некоторого типа, соответствующего протоколу Shape, не указывая какой-либо конкретный тип. Запись makeTrapezoid() таким образом позволяет выразить основной аспект её публичного интерфейса — возвращаемое значение является фигурой — без включения конкретных типов, из которых состоит фигура, в публичный интерфейс. Эта реализация использует два треугольника и квадрат, но функция может быть переписана для рисования трапеции различными другими способами без изменения возвращаемого типа.

Этот пример иллюстрирует, как непрозрачный возвращаемый тип аналогичен обобщенному типу. Код внутри 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 в примере с обобщениями в разделе <doc:OpaqueTypes#The-Problem-That-Opaque-Types-Solve> ранее в этой главе. Однако в отличие от значения в этом примере, 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
}

Если вы вызовете эту функцию с Square, она возвращает 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], поэтому оно соответствует требованию, что функции с непрозрачными возвращаемыми типами должны возвращать значения только одного типа.

Упакованные типы протоколов

Упакованный тип протокола иногда также называют существующим типом, что происходит от фразы «существует тип T, такой что T соответствует протоколу». Чтобы создать упакованный тип протокола, напишите any перед именем протокола. Вот пример:

struct VerticalShapes: Shape {
    var shapes: [any Shape]
    func draw() -> String {
        return shapes.map { $0.draw() }.joined(separator: "\n\n")
    }
}

let largeTriangle = Triangle(size: 5)
let largeSquare = Square(size: 5)
let vertical = VerticalShapes(shapes: [largeTriangle, largeSquare])
print(vertical.draw())

В примере выше, VerticalShapes объявляет тип shapes как [any Shape] — массив упакованных элементов Shape. Каждый элемент массива может быть разного типа, и каждый из этих типов должен соответствовать протоколу Shape. Чтобы поддерживать эту гибкость во время выполнения, Swift добавляет уровень косвенности при необходимости — эта косвенность называется боксом, и она имеет издержки производительности.

Внутри типа VerticalShapes код может использовать методы, свойства и индексы, которые требуются протоколом Shape. Например, метод draw() типа VerticalShapes вызывает метод draw() для каждого элемента массива. Этот метод доступен, потому что Shape требует метода draw(). В отличие от этого, попытка доступа к свойству size треугольника или любых других свойств или методов, которые не требуются протоколом Shape, приводит к ошибке.

Сравните три типа, которые можно использовать для shapes:

  • Использование обобщений, написав struct VerticalShapes<S: Shape> и var shapes: [S], создает массив, элементы которого являются некоторым конкретным типом фигуры, и где идентичность этого конкретного типа видна любому коду, взаимодействующему с массивом.

  • Использование непрозрачного типа, написав var shapes: [some Shape], создает массив, элементы которого являются некоторым конкретным типом фигуры, и где идентичность этого конкретного типа скрыта.

  • Использование упакованного типа протокола, написав var shapes: [any Shape], создает массив, который может хранить элементы разных типов, и где идентичности этих типов скрыты.

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

Вы можете использовать приведение типа as, когда вы знаете базовый тип упакованного значения. Например:

if let downcastTriangle = vertical.shapes[0] as? Triangle {
    print(downcastTriangle.size)
}
// Prints "5"

Дополнительную информацию см. в <doc:TypeCasting#Downcasting>.

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

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

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

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

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

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(smallTriangle)), который применяет несколько преобразований, является недопустимым, потому что перевернутая фигура не является допустимым аргументом для 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]
}

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

func makeOpaqueContainer<T>(item: T) -> some Container {
    return [item]
}
let opaqueContainer = makeOpaqueContainer(item: 12)
let twelve = opaqueContainer[0]
print(type(of: twelve))
// Prints "Int"

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

This source file is part of the Swift.org open source project
Copyright © 2014 - 2025 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API