eugenemind.com
← Назад к блогу

17 октября 2026 г. · 16 мин. чтения

some vs any в Swift: в чём разница и когда использовать каждый

#swift #ios #generics #swiftui

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

Затем в Swift 5.1 появился some, а в Swift 5.6 появился any. Теперь вместо привычного Protocol мы всё чаще встречаем some Protocol и any Protocol.

На первый взгляд оба ключевых слова позволяют скрыть конкретную реализацию за протоколом. Но почему Swift предлагает два разных механизма? Почему some не позволяет возвращать разные типы из одной функции, а any позволяет? И действительно ли some всегда быстрее?

Чтобы разобраться, начнём с обычного примера из разработки checkout, а затем посмотрим, как эти механизмы работают в реальных API и что происходит под капотом.

Начнём с обычного протокола

Представим, что мы разрабатываем экран оплаты заказа. Пользователь может оплатить покупку банковской картой или через QR-код.

import Foundation

protocol PaymentMethod {
    func pay(amount: Decimal)
}

struct CardPayment: PaymentMethod {
    func pay(amount: Decimal) {
        print("Card payment: \(amount)")
    }
}

struct QRPayment: PaymentMethod {
    func pay(amount: Decimal) {
        print("QR payment: \(amount)")
    }
}

Здесь всё привычно. PaymentMethod описывает контракт, а CardPayment и QRPayment предоставляют разные реализации.

Теперь напишем функцию, которая выполняет оплату:

func checkout<T: PaymentMethod>(
    using method: T,
    amount: Decimal
) {
    method.pay(amount: amount)
}

Это обычная generic-функция. Вместо конкретного типа мы используем параметр T, который должен соответствовать протоколу PaymentMethod.

checkout(using: CardPayment(), amount: 100)
checkout(using: QRPayment(), amount: 200)

Swift определяет T как CardPayment в первом случае и QRPayment во втором.

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

Но что делать, если нам нужно работать с разными реализациями через один интерфейс?

any: когда конкретный тип может меняться

Предположим, что на экране checkout пользователь может выбрать способ оплаты. Нам нужно хранить доступные способы в одной коллекции:

let paymentMethods: [any PaymentMethod] = [
    CardPayment(),
    QRPayment()
]

Обычный массив Swift имеет один определённый тип элемента. Но CardPayment и QRPayment являются разными структурами. any PaymentMethod позволяет решить эту задачу.

any является ключевым словом Swift, обозначающим existential type (экзистенциальный тип). Это тип, который может содержать значение любой конкретной реализации указанного протокола.

Название existential происходит от математического символа ∃, означающего «существует». Мы знаем, что внутри находится значение некоторого типа, соответствующего PaymentMethod, но не закрепляем этот тип за переменной.

var selectedMethod: any PaymentMethod = CardPayment()

selectedMethod.pay(amount: 100)
selectedMethod = QRPayment()
selectedMethod.pay(amount: 200)

Переменная по-прежнему имеет тип any PaymentMethod, хотя сначала в ней находится CardPayment, а затем QRPayment. Для checkout это удобно: выбранный способ оплаты можно заменить без создания новой ViewModel или изменения её generic-параметров.

Зачем Swift понадобилось явное any?

До Swift 5.6 мы могли написать:

let method: PaymentMethod = CardPayment()

Это уже был existential, хотя в коде он выглядел просто как имя протокола.

В предложении SE-0335, Introduce existential any авторы объясняли проблему так:

“The cost of using existential types should not be hidden”

То есть расходы, связанные с использованием existential types, не должны оставаться скрытыми.

Причём речь шла не только о производительности. Старый синтаксис создавал путаницу между двумя принципиально разными конструкциями:

func checkout<T: PaymentMethod>(_ method: T)
func checkout(_ method: any PaymentMethod)

В первом случае PaymentMethod ограничивает generic-параметр T. Во втором мы принимаем existential, значение, конкретный тип которого скрыт за протоколом.

Авторы SE-0335 отмечали, что внешнее сходство этих конструкций нередко заставляло разработчиков выбирать existential там, где им требовались generics. Проблемы обнаруживались позже, когда код начинал зависеть от конкретного типа или требований Self и associatedtype.

Поэтому any ввели как явное указание на стирание конкретного типа в статическом интерфейсе.

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

И ещё один нюанс: any PaymentMethod не то же самое, что Any. Any может содержать практически любое Swift-значение. А any PaymentMethod ограничивает допустимые значения типами, соответствующими PaymentMethod, и позволяет вызывать требования этого протокола без приведения типа.

some: когда мы хотим скрыть реализацию SDK

Теперь представим, что мы разрабатываем платёжный SDK, которым будут пользоваться другие команды.

Здесь важно различать два уровня кода. Внутри SDK мы можем использовать собственные структуры и менять их устройство. Снаружи SDK разработчики видят только публичный API библиотеки.

В нашем примере PaymentMethod является публичным протоколом, а CardProcessor является внутренней структурой SDK. Для наглядности будем явно указывать модификатор internal, хотя в Swift он используется по умолчанию.

public protocol PaymentMethod {
    func pay(amount: Decimal)
}

internal struct CardProcessor: PaymentMethod {
    func pay(amount: Decimal) {
        print("Processing card payment")
    }
}

Предположим, SDK должен предоставлять публичную функцию для создания стандартного платёжного обработчика.

Вариант 1. Вернуть конкретный тип

Самое очевидное решение:

public func makePaymentMethod() -> CardProcessor {
    CardProcessor()
}

Но такой код не скомпилируется. Публичная функция не может возвращать внутренний тип.

Мы могли бы сделать CardProcessor публичным:

public struct CardProcessor: PaymentMethod {
    public func pay(amount: Decimal) {
        print("Processing card payment")
    }
}

public func makePaymentMethod() -> CardProcessor {
    CardProcessor()
}

Теперь всё работает. Но появилась проблема: мы раскрыли внутреннюю реализацию SDK. Разработчики других приложений могут начать напрямую использовать CardProcessor. Его имя и публичные возможности станут частью контракта нашей библиотеки.

А что, если позднее мы захотим добавить логирование платежей?

internal struct LoggingPayment<Base: PaymentMethod>: PaymentMethod {
    let base: Base

    func pay(amount: Decimal) {
        print("Payment started")
        base.pay(amount: amount)
    }
}

Теперь фактический тип результата будет LoggingPayment<CardProcessor>. Если мы захотим указать его в публичной сигнатуре, придётся сделать публичным и LoggingPayment. Получается, что внутренние детали SDK постепенно становятся частью его публичного интерфейса.

Вариант 2. Вернуть any PaymentMethod

Можно не раскрывать внутренние типы и вернуть existential:

public func makePaymentMethod() -> any PaymentMethod {
    LoggingPayment(base: CardProcessor())
}

Теперь клиентам SDK не нужно знать ни про CardProcessor, ни про LoggingPayment.

Это нормальное решение. Но есть нюанс: возвращаемое значение имеет existential type, хотя фабрика всегда создаёт одну определённую реализацию. Мы применили type erasure там, где возможность менять concrete type внутри результата нам не требуется.

Вариант 3. Использовать some PaymentMethod

Именно здесь появляется some:

public func makePaymentMethod() -> some PaymentMethod {
    LoggingPayment(base: CardProcessor())
}

Мы по-прежнему скрываем внутренние типы SDK, но не превращаем результат в existential.

some является ключевым словом Swift, которое в позиции возвращаемого типа обозначает opaque result type (непрозрачный тип результата).

Функция возвращает один определённый concrete type, выбранный её реализацией. Вызывающий код не знает имени этого типа, но компилятор сохраняет информацию о нём.

let method = makePaymentMethod()
method.pay(amount: 100)

Клиенту не требуется знать, что внутри находится LoggingPayment<CardProcessor>.

Таким образом, без some у нас было два основных варианта: раскрыть конкретный тип или вернуть existential через any. some даёт третий вариант: скрыть конкретный тип от клиента, сохранив его определённость для компилятора.

Правда, здесь есть важная оговорка. Скрытый тип не обязательно можно менять без последствий для бинарной совместимости SDK. В библиотеках с library evolution нужно учитывать правила ABI для opaque result types.

Почему some нельзя использовать для любого результата?

Теперь попробуем сделать фабрику, которая выбирает способ оплаты в зависимости от условия:

func makePaymentMethod(useQR: Bool) -> some PaymentMethod {
    if useQR {
        return QRPayment()
    }

    return CardPayment()
}

Такой код не скомпилируется. Хотя оба типа соответствуют PaymentMethod, у opaque result type должен быть один underlying concrete type. А QRPayment и CardPayment являются разными структурами.

Если функция действительно должна возвращать разные реализации, используем any:

func makePaymentMethod(useQR: Bool) -> any PaymentMethod {
    if useQR {
        return QRPayment()
    }

    return CardPayment()
}

Это и есть практическая разница: some скрывает один определённый тип, а any позволяет работать с разными типами через общий интерфейс.

Простая аналогия: игрушки в коробках

Представим магазин игрушек. В нём продаются машинки, роботы и динозавры. Все они относятся к игрушкам, но это разные типы предметов.

Теперь представим две коробки.

Первая: коробка-сюрприз (any).

Сегодня в ней может оказаться машинка, завтра робот, а послезавтра динозавр. Мы знаем только, что внутри игрушка. Это похоже на any Toy: конкретный тип содержимого может меняться.

Вторая: коробка с секретной моделью (some).

Производитель выпускает игрушку определённой модели, но не сообщает покупателю её название. В каждой такой коробке лежит игрушка одной и той же модели. Экземпляры могут быть разными, но тип игрушки фиксирован.

Это похоже на some Toy: мы не знаем, какой именно тип выбрал производитель, однако за объявлением всегда стоит один определённый тип.

А generics можно представить как коробку, модель игрушки для которой выбирает сам покупатель. Вы заказали коробку для робота, и она работает с роботами. Выбрали машинку, и она работает с машинками.

Главное различие не в том, что коробка закрыта. И some, и any скрывают конкретный тип от пользователя API. Разница в другом: some обещает один определённый тип, а any позволяет содержимому быть разных типов.

Где эта разница влияет на checkout

Вернёмся к экрану оплаты. Допустим, пользователь может переключаться между картой и QR-кодом. Сначала попробуем хранить способ оплаты через generic-параметр:

final class CheckoutViewModel<Method: PaymentMethod> {
    private(set) var selectedMethod: Method

    init(selectedMethod: Method) {
        self.selectedMethod = selectedMethod
    }

    func select(_ method: Method) {
        selectedMethod = method
    }
}

После создания:

let viewModel = CheckoutViewModel(
    selectedMethod: CardPayment()
)

мы получаем CheckoutViewModel<CardPayment>.

viewModel.select(QRPayment()) // Compile-time error

Тип Method зафиксирован. Он не меняется у уже созданного экземпляра.

Если мы действительно хотим переключать способы оплаты, удобнее использовать existential:

final class CheckoutViewModel {
    private(set) var selectedMethod: any PaymentMethod

    init(selectedMethod: any PaymentMethod) {
        self.selectedMethod = selectedMethod
    }

    func select(_ method: any PaymentMethod) {
        selectedMethod = method
    }

    func pay(amount: Decimal) {
        selectedMethod.pay(amount: amount)
    }
}

Теперь пользователь может выбрать другую реализацию:

let viewModel = CheckoutViewModel(
    selectedMethod: CardPayment()
)

viewModel.select(QRPayment())
viewModel.pay(amount: 500)

Здесь any выбран не ради синтаксиса, а потому что он соответствует требованиям экрана. А some полезен на другом уровне, например в фабрике SDK, которая скрывает конкретный обработчик оплаты.

А что, если использовать some в параметрах?

До сих пор мы использовали some в возвращаемом типе функции. Но в Swift его можно встретить и в параметрах:

func checkout(using method: some PaymentMethod) {
    method.pay(amount: 100)
}

На первый взгляд кажется, что это та же идея: функция получает какой-то скрытый тип.

Но здесь есть отличие. В параметрах some является сокращённой записью generics. Предыдущий пример по смыслу эквивалентен следующему:

func checkout<T: PaymentMethod>(using method: T) {
    method.pay(amount: 100)
}

То есть конкретный тип выбирает вызывающий код. Мы можем передать CardPayment или QRPayment, и для каждого вызова T будет соответствовать переданному типу.

Почему тогда не пользоваться только короткой записью? Потому что иногда нам нужно связать типы нескольких параметров.

Например, представим функцию, которая сравнивает два способа оплаты одного типа:

func compare<T: PaymentMethod>(
    _ first: T,
    _ second: T
) {
    // ...
}

Здесь T общий для обоих параметров. Если первым аргументом будет CardPayment, второй тоже должен быть CardPayment.

А теперь напишем:

func compare(
    _ first: some PaymentMethod,
    _ second: some PaymentMethod
) {
    // ...
}

В этом случае каждый some вводит отдельный generic-параметр. Поэтому функция сможет принять CardPayment и QRPayment одновременно.

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

Что происходит под капотом

Теперь, когда мы разобрались с назначением some и any, посмотрим, почему они могут по-разному влиять на производительность.

Вернёмся к нашему checkout:

let method: any PaymentMethod = CardPayment()

Мы создали CardPayment, но сохранили его в переменной типа any PaymentMethod. Для нас это удобно: позже мы можем присвоить этой переменной QRPayment или любую другую реализацию протокола.

Но для Swift такая гибкость означает дополнительную работу. Компилятор не может считать, что в method всегда находится CardPayment. В другой момент там может оказаться структура совершенно другого размера, с другими свойствами и другой реализацией pay(amount:).

Поэтому Swift должен хранить не только само значение, но и информацию, которая позволит правильно с ним работать.

Для обычного existential с value type и одним протоколом на 64-битной платформе используется представление, которое можно упростить до следующей схемы:

┌──────────────────────────────┐
│ Value buffer: 3 слова        │
├──────────────────────────────┤
│ Type metadata pointer        │
├──────────────────────────────┤
│ Protocol witness table ptr   │
└──────────────────────────────┘

Обычно такой контейнер занимает 40 байт. Но это не универсальный размер любого any: например, existential с ограничением на класс устроен иначе.

Первые три машинных слова отведены под хранение значения. Если структура небольшая и подходит для inline-хранения, Swift может разместить её прямо внутри контейнера. Поэтому CardPayment не обязательно требует выделения памяти в куче только из-за того, что мы используем any.

Если значение слишком большое или не подходит для такого размещения, Swift может хранить его косвенно, что иногда требует heap allocation.

Теперь представим, что мы заменили способ оплаты:

var method: any PaymentMethod = CardPayment()
method = QRPayment()

Как Swift понимает, что внутри теперь QRPayment? Для этого используется type metadata, информация о конкретном типе значения.

Но знать тип недостаточно. Когда мы вызываем:

method.pay(amount: 100)

Swift должен найти реализацию pay(amount:), соответствующую этому типу. Здесь появляется Protocol Witness Table, или PWT.

Когда CardPayment соответствует PaymentMethod, Swift формирует информацию о том, как этот тип реализует требования протокола. Для QRPayment существует собственное соответствие. Поэтому при вызове pay(amount:) через existential Swift может использовать witness table, чтобы перейти к нужной реализации.

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

Для этого существует Value Witness Table, или VWT. Она связана с metadata типа и предоставляет операции, необходимые для копирования, перемещения и уничтожения значений.

То есть PWT помогает Swift вызывать требования протокола, а VWT помогает правильно работать с самими значениями.

Теперь становится понятнее, почему existential может иметь дополнительные расходы. Он должен поддерживать хранение разных типов, а вызов методов через протокол в общем случае требует косвенного обращения к реализации.

Но если any использует witness table, означает ли это, что generics и some всегда вызывают методы напрямую? Нет.

Посмотрим на generic-функцию:

func checkout<T: PaymentMethod>(
    using method: T,
    amount: Decimal
) {
    method.pay(amount: amount)
}

Мы знаем, что T соответствует PaymentMethod, но сама функция написана без привязки к CardPayment или QRPayment.

Swift может скомпилировать одну общую реализацию этой функции. В таком случае ей тоже потребуется информация о типе и его соответствии протоколу, а вызов pay(amount:) может выполняться через witness table. То есть generics сами по себе не гарантируют статическую диспетчеризацию.

Однако у компилятора есть возможность оптимизировать такой код. Когда мы вызываем:

checkout(using: CardPayment(), amount: 100)

компилятор может увидеть, что T соответствует CardPayment, и создать специализированную версию функции для этого типа. Тогда ему уже не обязательно искать реализацию pay(amount:) через witness table. Вызов потенциально можно сделать прямым, а иногда и встроить тело метода через inlining.

С some ситуация похожая: underlying type определён, хотя и скрыт от вызывающего кода. Это сохраняет возможности для оптимизации.

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

Поэтому правильнее говорить не о том, что some быстрый, а any медленный, а о том, что generics и opaque types сохраняют больше информации о конкретных типах и тем самым дают компилятору больше возможностей для специализации.

Насколько это важно на практике? Для нашего checkout, скорее всего, почти неважно. Время сетевого запроса на оплату будет несравнимо больше расходов на вызов метода через witness table.

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

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

Почему some View так часто встречается в SwiftUI

Пожалуй, самый известный пример использования some в SwiftUI связан со свойством body.

struct CheckoutView: View {
    var body: some View {
        VStack {
            Text("Checkout")

            Button("Pay") {
                // ...
            }
        }
    }
}

На первый взгляд кажется, что body просто возвращает что-то, соответствующее View.

Но у SwiftUI есть важная особенность: интерфейс строится из конкретных типов. Text является структурой. Button тоже структура, причём generic. VStack хранит содержимое через свой generic-параметр. А модификаторы могут создавать новые типы, оборачивающие предыдущие представления.

В результате даже небольшой экран может иметь довольно сложный concrete type. Если бы мы должны были указывать этот тип вручную, код стал бы неудобным и хрупким.

some View решает проблему: мы не раскрываем сложную структуру типа в сигнатуре body, но Swift сохраняет знание о ней. Это важно не только для удобства. Конкретные типы позволяют компилятору проверять связи между компонентами интерфейса и дают возможности для оптимизации.

Почему нельзя просто написать any View?

Теперь попробуем заменить some на any:

struct CheckoutView: View {
    var body: any View {
        Text("Checkout")
    }
}

Код не удовлетворит требованиям View.

На первый взгляд это странно: Text соответствует View, а any View ведь может содержать Text.

Проблема в том, что сам existential type any View в общем случае не соответствует протоколу View.

Это разные утверждения:

  • Внутри any View находится значение типа, соответствующего View.
  • Сам тип any View соответствует View.

Первое верно. Второе в данном случае неверно.

У протокола View есть связанный тип Body. Чтобы тип соответствовал View, компилятор должен установить, какой именно Body у этой реализации.

А any View может содержать разные представления с разными типами Body. Нельзя просто выбрать один Body, который описывал бы все возможные значения existential. Именно поэтому any View нельзя механически подставить вместо some View.

Тогда зачем существует AnyView?

В SwiftUI есть специальная структура AnyView. Она позволяет выполнить type erasure, то есть скрыть конкретный тип представления за общим интерфейсом. Но в отличие от any View, AnyView сама соответствует протоколу View.

func makePaymentScreen(useQR: Bool) -> AnyView {
    if useQR {
        return AnyView(Text("Scan QR code"))
    }

    return AnyView(Button("Pay by card") {
        // ...
    })
}

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

Обратите внимание, что здесь мы не возвращаем some View, хотя внутренние представления различаются. Мы явно оборачиваем их в один общий concrete type.

Однако в современном SwiftUI далеко не всегда нужно использовать AnyView. Для условных веток внутри @ViewBuilder обычно достаточно обычного if:

struct PaymentScreen: View {
    let useQR: Bool

    var body: some View {
        if useQR {
            Text("Scan QR code")
        } else {
            Button("Pay by card") {
                // ...
            }
        }
    }
}

Этот код работает, хотя ветки возвращают разные представления.

Почему здесь нет ошибки, как в нашей функции с some PaymentMethod? Потому что body использует @ViewBuilder. Result builder преобразует условные ветки в одну составную структуру с определённым concrete type, например с использованием _ConditionalContent.

Для компилятора результат всё ещё имеет один underlying type, поэтому требование some View выполняется.

AnyView же действительно стирает тип представления. Apple отдельно отмечает, что при изменении типа представления внутри AnyView прежняя иерархия уничтожается и создаётся новая. Поэтому без необходимости оборачивать весь интерфейс в AnyView не стоит.

Получается интересная картина: some View скрывает сложный, но определённый тип представления; any View представляет existential, который нельзя напрямую использовать как полноценную реализацию View в body; а AnyView является специальной структурой SwiftUI, реализующей type erasure и самостоятельно соответствующей View.

Источники