$ sudo teach IT

Зачем это вообще нужно

Когда переменная или результат функции описаны протоколом, компилятору мало знать «это что-то, умеющее рисовать себя» — ему нужно понимать, сколько памяти выделить и как быстро находить нужный метод. Протокол сам по себе — это список требований, а не конкретный тип с известным размером. Поэтому там, где вы пишете тип-протокол, Swift просит уточнить: вы имеете в виду ровно один конкретный тип, который просто скрыт от читателя (some), или любой из подходящих типов, который определится только во время выполнения программы (any)?

Разница похожа на заказ такси. «Приедет одна и та же машина каждый раз, просто я не говорю вам номер» — это some. «Приедет любая машина из парка, какая свободна прямо сейчас» — это any. Второй вариант гибче, но обходится дороже: диспетчеру приходится каждый раз искать машину заново.

some: непрозрачный тип (opaque type)

Официальный термин — opaque type, «непрозрачный тип». Ключевое слово some перед протоколом означает: функция возвращает значение какого-то одного конкретного типа, соответствующего этому протоколу, но снаружи имя этого типа не видно. Компилятор при этом обязан точно знать, какой это тип — просто не сообщает его вызывающему коду.

protocol Shape {
    func area() -> Double
}

struct Circle: Shape {
    let radius: Double
    func area() -> Double { radius * radius * 3.14159 }
}

func makeUnitCircle() -> some Shape {
    return Circle(radius: 1)
}

Функция всегда возвращает именно Circle, просто снаружи вызывающий код видит только «нечто, соответствующее Shape». Из-за этого some накладывает жёсткое условие: внутри функции все пути возврата обязаны отдавать значения ОДНОГО и того же конкретного типа. Так компилятор может подставить настоящий тип на этапе компиляции, без лишней работы во время выполнения.

func makeShape(big: Bool) -> some Shape {
    if big {
        return Circle(radius: 10)
    } else {
        return Circle(radius: 1) // тот же тип Circle — можно
    }
}

А вот если бы во второй ветке появился, скажем, Square — другая структура, тоже соответствующая Shape, — компиляция упадёт: some обещает ровно один тип, а тут их два.

any: экзистенциальный тип

Ключевое слово any перед протоколом означает обратное: тип значения решится только во время выполнения программы, и это может быть любой тип, который соответствует протоколу. Официально такой тип называется экзистенциальным (existential type) — он «стирает» конкретный тип и хранит рядом со значением служебную информацию о том, какие у него методы и как их вызывать. Это называется стиранием типов (type erasure).

struct Square: Shape {
    let side: Double
    func area() -> Double { side * side }
}

func randomShape(big: Bool) -> any Shape {
    if big {
        return Circle(radius: 10)
    } else {
        return Square(side: 2) // другой тип — и это нормально
    }
}

Здесь разные ветки возвращают разные конкретные типы, и это работает, потому что any Shape не обещает «один тип» — он обещает «что-то, что умеет отвечать на вопросы протокола Shape, а какой это тип, разберёмся при вызове». За эту свободу приходится платить: вызов метода у значения типа any идёт через дополнительный слой (динамическая диспетчеризация), а само значение может занимать больше памяти, чем нужно для конкретной структуры. По сравнению с some это немного медленнее, но на практике для обычного кода разница обычно незаметна — важно понимать компромисс, а не бояться any.

Тот же принцип нужен, чтобы держать в одном массиве значения разных типов, объединённых общим протоколом:

let shapes: [any Shape] = [Circle(radius: 1), Square(side: 2)]
for shape in shapes {
    print(shape.area())
}

Массив однородный по объявленному типу элемента (any Shape), но внутри лежат разные конкретные структуры. Так и запоминайте: нужен ОДИН тип, скрытый от читателя, — some. Нужно хранить или возвращать РАЗНЫЕ типы под одной вывеской — any.

associatedtype: заглушка типа внутри протокола

Иногда протокол не может обойтись без собственного «типа-переменной». Представьте протокол для стека: он должен уметь класть элемент и доставать элемент, но не может заранее сказать, элементы какого типа там будут — Int, String или что-то своё. Для этого в протоколе объявляют associatedtype — заглушку, которую каждый конкретный тип, принимающий протокол, обязан заполнить своим типом.

protocol Container {
    associatedtype Item
    mutating func add(_ item: Item)
    var count: Int { get }
}

struct IntStack: Container {
    private var items: [Int] = []
    mutating func add(_ item: Int) { items.append(item) }
    var count: Int { items.count }
}

Компилятор смотрит на реализацию add(_ item: Int) и сам понимает: для IntStack заглушка Item — это Int. Явно писать это нигде не нужно, вывод происходит автоматически.

А вот здесь и начинается связь с темой урока: протокол с associatedtype нельзя просто взять и использовать как обычный тип переменной — компилятор не знает, какой именно Item иметь в виду, а без этого он не может проверить типы при вызове методов. Написать let c: Container = IntStack() не получится. Зато можно написать функцию с some: она вернёт один конкретный тип, для которого Item уже определён.

func makeIntContainer() -> some Container {
    return IntStack()
}

Обобщённая функция с ограничением через where или простым синтаксисом <T: Container> — ещё один способ работать с протоколом, у которого есть associatedtype, но это уже дженерики, о которых шла речь до этого урока. Разница в том, что дженерик — это отдельная копия кода для каждого использованного типа, известная целиком на этапе компиляции, а some в возвращаемом значении — способ спрятать этот конкретный тип от вызывающего кода, оставив ту же скорость работы.

Как выбирать на практике

  • Функция всегда строит один и тот же тип результата, и хочется скрыть детали реализации от вызывающего кода — берите some. Это быстрее и чаще всего достаточно.
  • Нужно вернуть или сохранить значения РАЗНЫХ конкретных типов под одним протоколом — например, разные обработчики команд в одном массиве — берите any.
  • Протокол описывает контейнер или что-то с типом-параметром внутри (через associatedtype) — использовать его как простой тип переменной уже нельзя, нужен либо дженерик, либо some у конкретной реализующей функции.

Частые ошибки

Функция с some Protocol возвращает в разных ветках разные конкретные типы. Компилятор откажется компилировать код с сообщением о несовпадающих типах непрозрачного результата — решение: либо привести оба пути к одному типу, либо явно поменять some на any, если различие типов действительно нужно.

Попытка объявить переменную просто именем протокола с associatedtype без some или дженерика — компилятор укажет, что протокол можно использовать только как ограничение обобщённого типа, потому что associatedtype не определён без конкретной реализации.

Использование any там, где на самом деле всегда один и тот же тип — код скомпилируется и будет работать, но это лишние накладные расходы без всякой пользы: если тип всегда один, честнее и быстрее написать some.

Резюме

  • some Protocol — непрозрачный тип: компилятор точно знает конкретный тип, читателю он не показан, все пути возврата обязаны совпадать по типу.
  • any Protocol — экзистенциальный тип: конкретный тип определяется во время выполнения, можно смешивать разные типы под одним протоколом, но это чуть медленнее из-за стирания типов.
  • associatedtype — заглушка типа внутри протокола, которую заполняет каждая конкретная реализация; протокол с такой заглушкой нельзя использовать как простой тип переменной.
  • Правило выбора: один и тот же тип всегда — some; разные типы под одной вывеской — any; протокол с параметром типа — дженерик или some у конкретной функции.

Проверьте себя

4 вопроса

Приветствие на нужном языке

Протокол Greeter объявляет один метод — greet(), возвращающий строку приветствия. В заготовке уже есть две структуры, которые ему соответствуют: EnglishGreeter здоровается по-английски, RussianGreeter — по-русски.

Реализуйте функцию makeGreeter(english:). Она должна возвращать значение типа any Greeter: если параметр english равен true, верните EnglishGreeter(), иначе — RussianGreeter().

Обратите внимание, что возвращаемый тип уже написан как any Greeter, а не some Greeter — подумайте, почему тут нужен именно такой вариант, прежде чем менять тело функции.