Подтвердить что ты не робот

Какова разница между типичными параметрами функции и типизированными параметрами протокола?

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

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, используя растущий размер кода для каждого переданного типа, но с быстрой статической отправкой?

4b9b3361

Ответ 1

(я понимаю, что 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 слова. Значения, превышающие это значение, будут выделены в виде кучи, а ссылка на значение будет сохранена в буфере значений (в качестве ссылки должно быть только одно слово).

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

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

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

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

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

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

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

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 поддерживает явно типизированный параметр над общим (см. этот Q & A).

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
}

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

Ответ 2

Если ваш метод 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.