Ответ 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
. Фактические конкретные типы, которые передаются и возвращаются, необязательно должны быть одинаковыми.