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

swift generics swift-protocols

1321 просмотра

2 ответа

518 Репутация автора

Дан протокол без каких-либо связанных типов:

protocol SomeProtocol
{
    var someProperty: Int { get }
}

В чем разница между этими двумя функциями на практике (имеется в виду не «одна является общей, а другая нет»)? Они генерируют другой код, имеют ли они разные характеристики времени выполнения? Меняются ли эти различия, когда протокол или функции становятся нетривиальными? (поскольку компилятор, вероятно, может встроить что-то вроде этого)

func generic<T: SomeProtocol>(some: T) -> Int
{
    return some.someProperty
}

func nonGeneric(some: SomeProtocol) -> Int
{
    return some.someProperty
}

Я в основном спрашиваю о различиях в том, что делает компилятор, я понимаю последствия обоих языков. По сути, nonGenericподразумевает ли постоянный размер кода, но более медленную динамическую диспетчеризацию, по сравнению genericс использованием растущего размера кода для каждого переданного типа, но с быстрой статической диспетчеризацией?

Автор: JHZ Источник Размещён: 18.07.2016 09:45

Ответы (2)


1 плюс

8554 Репутация автора

Если в вашем genericметоде задействовано более одного параметра T, будет разница.

func generic<T: SomeProtocol>(some: T, someOther: T) -> Int
{
    return some.someProperty
}

В методе выше someи someOtherдолжны быть одного типа. Они могут быть любого типа, который соответствует SomeProtocol, но они должны быть того же типа.

Однако без дженериков:

func nonGeneric(some: SomeProtocol, someOther: SomeProtocol) -> Int
{
    return some.someProperty
}

someи someOtherмогут быть разных типов, если они соответствуют SomeProtocol.

Автор: tktsubota Размещён: 18.07.2016 09:54

21 плюса

56333 Репутация автора

Решение

(Я понимаю, что OP меньше спрашивает о языковых последствиях и больше о том, что делает компилятор - но я чувствую, что стоит также перечислить общие различия между общими и протокольными параметрами функций)

1. Общий заполнитель, ограниченный протоколом, должен удовлетворять конкретному типу

Это является следствием того, что протоколы не соответствуют друг другу , поэтому вы не можете вызывать generic(some:)с SomeProtocolтипизированным аргументом.

struct Foo : SomeProtocol {
    var someProperty: Int
}

// of course the solution here is to remove the redundant 'SomeProtocol' type annotation
// and let foo be of type Foo, but this problem is applicable anywhere an
// 'anything that conforms to SomeProtocol' typed variable is required.
let foo : SomeProtocol = Foo(someProperty: 42)

generic(some: something) // compiler error: cannot invoke 'generic' with an argument list
                         // of type '(some: SomeProtocol)'

Это происходит потому , что общая функция ожидает аргумент некоторого типа , Tкоторый соответствует SomeProtocol- но SomeProtocolэто не тип , который соответствует SomeProtocol.

Не-родовая функция , однако, с типом параметра SomeProtocol, будет принимать в fooкачестве аргумента:

nonGeneric(some: foo) // compiles fine

Это потому, что он принимает «все, что может быть напечатано как SomeProtocol», а не «определенный тип, который соответствует SomeProtocol».

2. Специализация

Как описано в этом фантастическом выступлении WWDC , «экзистенциальный контейнер» используется для представления значения типа протокола.

Этот контейнер состоит из:

  • Буфер значений для хранения самого значения длиной 3 слова. Значения, превышающие это, будут выделяться в куче, а ссылка на значение будет сохранена в буфере значений (поскольку ссылка имеет размер всего 1 слово).

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

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

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

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

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

  • Для каждого ограничения протокола для данного заполнителя функция принимает параметр указателя таблицы-свидетеля протокола.

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

3. Отправление требований протокола

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

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

Несмотря на это, простые функции типа протокола могут извлечь выгоду из встраивания. В таких случаях, компилятор находится в состоянии устранить накладные расходы буфера значений и значений протокола и таблицы свидетелей (это можно увидеть, исследуя SIL , излучаемую в -O сборке), что позволяет ему статический направить методы так же, как общие функции. Однако, в отличие от общей специализации, эта оптимизация не гарантируется для данной функции (если только вы не применяете @inline(__always)атрибут - но обычно лучше, чтобы компилятор решил это).

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

4. Разрешение перегрузки

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

struct Foo : SomeProtocol {
    var someProperty: Int
}

func bar<T : SomeProtocol>(_ some: T) {
    print("generic")
}

func bar(_ some: SomeProtocol) {
    print("protocol-typed")
}

bar(Foo(someProperty: 5)) // protocol-typed

Это потому, что Swift предпочитает явно типизированный параметр по сравнению с обычным (см. Этот раздел вопросов и ответов ).

5. Общие заполнители обеспечивают одинаковый тип

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

Функция:

func generic<T : SomeProtocol>(a: T, b: T) -> T {
    return a.someProperty < b.someProperty ? b : a
}

принимает два аргумента и возвращает результат того же конкретного типа, где этот тип соответствует SomeProtocol.

Однако функция:

func nongeneric(a: SomeProtocol, b: SomeProtocol) -> SomeProtocol {
    return a.someProperty < b.someProperty ? b : a
}

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

Автор: Hamish Размещён: 05.01.2017 04:57
Вопросы из категории :
32x32