Непрозрачные типы (opaque types) в Swift и ключевое слово some

Непрозрачные типы (opaque types) являются важной особенностью языка Swift. С помощью ключевого слова some, обозначающего непрозрачный тип, вы можете скрыть конкретный тип возвращаемого значения для вычисляемого свойства или функции. И это позволит вам писать гибкий, лаконичный и надежный код.

Что обозначает some?

Новая функция в Swift 5.1 — это ключевое слово some. Возможно, вы уже видели его как часть шаблона SwiftUI для View:

struct MyFirstView: View {
    var body: some View {
        Text("Привет!")
    }
}

Мы видим ключевое слово some прямо перед типом View вычисляемого свойства body. Оно указывает на то, что body имеет непрозрачный тип. Мы скрываем информацию о типе от кода MyFirstView.

Реализация вычисляемого свойства определяет конкретный тип body. При этом тип, который использует MyFirstView, остается скрытым.

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

Непрозрачные типы и дженерики похожи между собой:

  • При использовании заполнителя универсального типа вызывающая функция определяет конкретный тип заполнителя.
  • В случае непрозрачных типов реализация определяет конкретный тип.

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

func addition<T: Numeric>(a: T, b: T) -> T {
    return a + b
}
 
// Складываем значения типа Int
let resultA = addition(a: 42, b: 99)
 
// Складываем значения типа Double
let resultB = addition(a: 3.1415, b: 1.618)

В приведенном выше коде мы используем универсальный заполнитель T. Когда параметры a и b имеют тип T, функция возвращает значение типа T, при условии, что тип T соответствует cпротоколу Numeriс. В результате мы можем использовать эту функцию для добавления чисел типа Int, Double и т. д.

  • Тип заполнителя T является только заполнителем. Когда код компилируется, Swift заменяет его конкретным типом, таким как Int или Double. При тестировании типа а или b во время выполнения, вы увидите, что они будут иметь конкретные типы, то есть Int, Double и т. д.
  • Вызов функции addition(a:b:) определяет конкретный тип заполнителя T.

Давайте сравним это с непрозрачными типами.

Сначала определим протокол Shape и создадим две структуры, которые соответствуют данному протоколу:

protocol Shape {
    func describe() -> String
}
 
struct Square: Shape {
    func describe() -> String {
        return "Я квадрат."
    }
}
 
struct Circle: Shape {
    func describe() -> String {
        return "Я круг."
    }
}

Затем мы создадим функцию makeShape():

func makeShape() -> some Shape {
    return Circle()
}

Функция makeShape() возвращает значение типа Shape. Также здесь использует ключевое слово some для обозначения, что это непрозрачный тип. Определение того, какой конкретный тип возвращается, зависит от функции. В приведенном выше коде мы возвращаем значение типа Circle.

let shape = makeShape()
print(shape.describe())
// Я круг.

Почему мы просто не использовали протокол? В конце концов, если мы удалим ключевое слово some, приведенный код по-прежнему будет работать. Зачем вообще использовать some?

Непрозрачные типы и типы протоколов существенно различаются: непрозрачные типы сохраняют идентичность типов в отличие от типов протоколов.

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

Типы протоколов и ассоциированные типы

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

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

Добавим ассоциированный тип к протоколу Shape:

protocol Shape {
    associatedtype Color
    var color: Color { get }
    func describe() -> String
}

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

Далее мы создаем две реализации протокола Shape:

struct Square: Shape {
    var color: String
    func describe() -> String {
        return "Я квадрат"
    }
}
 
struct Circle: Shape {
    var color: Int
    func describe() -> String {
        return "Я круг."
    }
}

Как и прежде, мы создали две структуры Square и Circle, которые соответствуют протоколу Shape. Обе структуры реализуют свойство color как того требует протокол. Они также присваивают color конкретный тип: Square — String, а Circle — Int.

Создадим функцию, которая будет использовать протокол Shape в качестве возвращаемого типа:

func makeShape() -> Shape {
    return Square(color: "Желтый")
}

Если вы запустите приведенный выше код, вы получите ошибку компиляции:

protocol 'Shape' can only be used as a generic constraint because it has Self or associated type requirements

Эта ошибка означает, что Swift не может конкретизировать ассоциированный тип Color, основываясь на типе возвращаемого значения функции Shape. У протокола Shape есть ассоциированный тип Color, который используется для свойства color. Но какой конкретный тип color здесь должен быть использован?

Мы можем ясно видеть, что на основе реализации функции makeShape() конкретный тип для Color — это String. Однако Swift не может полагаться на эту информацию, потому что она является частью реализации функции, а не ее объявления.

Swift не может быть уверен, что функция makeShape() всегда будет возвращать Shape с ассоциированным типом String. Ассоциированный тип может быть каким угодно типом.

Так что же нам делать? Мы добавляем ключевое слово some в объявление функции:

func makeShape() -> some Shape {
    return Square(color: "Желтый")
}

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

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

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

Почему непрозрачные типы полезны?

Непрозрачные типы позволяют нам использовать протоколы с ассоциироваными типами в качестве возвращаемых типов. Как мы видели в предыдущих примерах, из-за ключевого слова some функция makeShape() может возвращать значение типа Shape, которое использует ассоциированный тип. Без использования ключевого слова some мы столкнемся с ошибкой компиляции.

Непрозрачные типы сохраняют идентичность типов, в отличие от типов протоколов. Вы можете сравнить одно возвращаемое значение makeShape() с другим, используя оператор ==, если вы использовали ключевое слово some.

protocol Shape: Equatable {
    ...
}
 
func makeShape() -> some Shape {
    return Square(color: "Синий")
}
 
let aShape = makeShape()
let anotherShape = makeShape()
 
print(aShape == anotherShape)
// true

Протокол Equatable, который соответствует Shape, использует функцию == для сравнения двух значений друг с другом. Объявление функции по умолчанию выглядит следующим образом:

static func == (lhs: Self, rhs: Self) -> Bool

Self — это еще один заполнитель, который ссылается на имя текущего типа. Так же, как заполнитель T может ссылаться на Int в универсальной функции, Self относится к конкретному типу при использовании в протоколе Shape. Мы добавляем some в объявление makeShape(). Во время компиляции Swift выясняет, что конкретный тип возвращаемого значения для makeShape() должен быть Square.

Непрозрачные типы имеют важное значение для SwiftUI.

var body: some View {
    VStack(alignment: .leading) {
        Text("Привет, как дела?")
            .font(.headline)
        Text("Все хорошо!")
            .font(.subheadline)
    }
}

Конкретный тип этого view, объявленного как some View, — ViewVStack>VStack. SwiftUI использует дженерик структуры, такие как VStack, для создания сложных типов.

Если вы укажете конкретное свойство body, вам придется обновлять этот тип каждый раз, когда изменяется view. Поэтому проще объявить свойство View как непрозрачный тип и позволить конкретной реализации определить его тип.

Читайте также:
Комментарии (1)
Добавить комментарий

Ваш адрес email не будет опубликован.