Какие функции вызывает Swift? Часть 4: Generics.

Это русский перевод статьи airspeedvelocity “Which function does Swift call? Part 4: Generics”.

Это 4 часть из серии статей, посвященных тому, как выбираются “перегруженные” (overloaded) функции в Swift. Часть 1 рассматривает “перегрузку” (overloading) по типу возвращаемого значения, часть 2 о том, как различные функции с простым одним аргументом выбираются по принципу “наиболее подходящей” функции, и часть 3 раскрывает этот принцип более детально в плане протоколов.

Мы начали серию статей с вопроса, почему мы получаем тип Range для 1…5 вместо типа  ClosedInterval? Сегодня мы разберем как  generics вписываются в иерархию “наиболее подходящей” функции, и у нас наконец будет достаточно информации для ответа на вопрос.

Generics – это более низкий приоритет

Generics понижают порядок приоритета. Запомните, что Swift любит типы данных как можно более специфические, а generics, как раз, являются наименее специфическими. Функции с аргументами, которые не являются generic (даже если это протоколы) всегда предпочтительнее функций с generic аргументами:

// Эта функция f будет подходить аргументу любого типа,
// но она имеет очень низкий приоритет
func f<T>(t: T) {
    print("T")
}

// Эта f имеет более специфический аргумент - его тип String, так что
// она будет предпочтительнее при передачи аргумента типа String.
func f(s: String) {
    print("String")
}

// Эта функция f берет специфический протокол, так что она будет
// предпочтительнее, чем generic версия (но не над версией функции
// f, которая берет действительный тип Bool)
func f(b: BooleanType) {
    print("BooleanType")
}

// напечатается "String"
f("wotcha")

//напечатается "BooleanType"
f(true)

// напечатается "T"
f(1)

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

Делаем Generic Placeholders более специфическими с помощью ограничений (Constraints)

Для generic параметров вы можете применять различные ограничения. Вам может понадобиться, чтобы этот тип параметра подтверждал протоколы, или их ассоциированные типы были равны определенным фиксированным типам, или подтверждали протоколы и т.д.

В этом случае также как и раньше, правило доминирования остается тем же: чем более специфической вы делаете функцию, тем она становится более предпочтительной при “перегрузке” (overload).

Это означает, что generic функция с placeholder, который имеет ограничения  (например, подтверждение протокола) будет предпочтительна перед функцией, у которой этого ограничения нет:

func f<T>(t: T) {
    print("T")
}

func f<T: IntegerType>(t: T) {
    print("T: IntegerType")
}

// напечатается "T: IntegerType"
f(1)

Если выбор между двумя функциями и обе имеют ограничения на свои placeholder, но одна более специфична, чем другая, то будет выбрана более специфичная. Эти правила нам уже знакомы – они полная копия правил для”перегрузки” не generic функций.

Например, если один протокол наследуется от другого, то ограничение в виде наследуемого протокола является предпочтительной:

func g<T: IntegerType>(t: T) {
    print("IntegerType")
}

func g<T: SignedIntegerType>(t: T) {
    print("SignedIntegerType")
}

// prints "SignedIntegerType"
g(1)

Или если выбор между подтверждением одного или двух протоколов, предпочтением пользуется та функция, которая подтверждает два протокола:

func f<B: BooleanType>(b: B) {
    print("BooleanType")
}

func f<B: protocol<BooleanType,Printable>>(b: B) {
    print("BooleanType and Printable")
}

// напечатается "BooleanType and Printable"
f(true)

Предложение where предоставляет placeholders возможность ограничивать их ассоциированные типы (associated types). Эти дополнительные ограничения делают функцию более специфической  и поднимают ее приоритет при “перегрузке”:

// аргумент должен быть IntegerType
func f<T: IntegerType>(t: T) {
    print("IntegerType")
}

// аргумент должен быть IntegerType
// И его Distance должен быть IntegerType
func f<T: IntegerType where T.Distance: IntegerType>(t: T) {
    print("T.Distance: IntegerType")
}

// напечатается "T.Distance: IntegerType"
f(1)

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

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

func f<T: IntegerType where T.Distance: IntegerType>(t: T) {
    print("T.Distance: IntegerType")
}

// where T.Distance: SignedIntegerType это более специфично, чем
// where just T.Distance: IntegerType
func f<T: IntegerType where T.Distance: SignedIntegerType>(t: T) {
    print("T.Distance: SignedIntegerType")
}

// так что напечатается "T.Distance: SignedIntegerType"
f(1)

Where  предложения также позволяют проверять значение на равенство определенному типу, но не только тому, что подтверждает протокол:

func f<T: IntegerType where T.Distance == Int>(t: T) {
    print("Distance == Int")
}

func f<T: IntegerType where T.Distance: IntegerType>(t: T) {
    print("Distance: IntegerType")
}

// напечатается "Distance == Int"
f(1)

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

Есть один интересный момент относительно проверки равенства в where предложениях. Вы можете проверять на равенство протоколу. Но, возможно, это все таки не то, что бы вы хотели, так как равенство протоколу – это не тоже самое, что подтверждение протокола:

// create a protocol that requires the
// implementor defines an associated type
protocol P {
    typealias L
}

// implement P and choose Ints as the
// associated type
struct S: P {
    typealias L = Int
}
let s = S()

// Ints are printable so this will match
func f<T: P where T.L: Printable>(t: T) {
    print("T.L: Printable")
}

// == is more specific than :, so this will
// be the one that's called, right?
func f<T: P where T.L == Printable>(t: T) {
    print("T.L == Printable")
}

// nope! prints "T.L: Printable"
f(s)

// here is a struct where L really is == Printable
struct S2: P {
    typealias L = Printable
}
let s2 = S2()

// and this time, yes, it prints "T.L == Printable"
f(s2)

В действительности очень тяжело это сделать в аварийном порядке – вы можете только проверять эквивалентность типов, если протокол, на равенство которому вы проверяете не имеет Self или не требуется ассоциативным типом (например, протокол не требует typealias, или использует Self как аргумент функции), вы также не можете использовать такие протоколы как не generic аргументы. Эти правила исключены из большинства протоколов в стандартной библиотеке  (Printable – это один из горстки протоколов, в которых это не исключено).

Остерегайтесь непредвиденных “перегрузок” функций

Итак, если предлагается выбор между generic “перегрузкой” и не- generic , Swift выбирает неgeneric. Если выбор происходит между двумя generic функциями, существует набор правил, которые очень похожи на аналогичные правила при выборе между неgeneric функциями.

Тем не менее, это не абсолютно точная параллель – есть некоторые различия. Например, если задан выбор между наследуемыми ограничениями и еще большими ограничениями (“глубина” против “широты”), то в противоположность неgeneric протоколам, Swift в действительности выберет “широту”:

protocol P { }
protocol Q { }

protocol A: P { }

struct S: A, Q { }
let s = S()

// эта f для наследуемого протокола
// (глубже чем просто P)
func f(p: A) {
    print("A")
}

// это f для еще большего количества протоколов
// (шире чем просто P)
func f(p: protocol<P, Q>) {
    print("P and Q")
}

// error: Ambiguous use of 'f'
f(s)

// тем не менее, если вы вместо этого определите подобную ситуацию
// с generics :

func g<T: A>(t: T) {
    print("T: A")
}

func g<T: protocol<P, Q>>(t: T) {
    print("T: P and Q")
}

// будет выбран второй вариант
// напечатается "T: P and Q"
g(s)

Давайте рассмотрим такой же пример с некоторыми реальными типами из стандартной библиотеки:

func f<T: SignedIntegerType>(t: T) {
    print("SignedIntegerType")
}

// IntegerType менее специфичен чем SignedIntegerType,
// но "ширина" бьют "глубину", так что этот должена быть вызвана эта f:
func f<T: protocol<IntegerType, Printable>>(t: T) {
    print("T: P and Q")
}

// напечатается "SignedIntegerType"
// подождите, но как это так?
f(1)

Если пример с P и Q работает одним способом, то в чем разница между SignedIntegerType и Printable?

Дело в том, что  тип IntegerType сам по себе подтверждает протокол Printable (между прочим, _IntegerType). Так как он и так подтверждает его, то  Printable в протоколе является избыточным и может быть проигнорирован – он не делает эту версию функции f более специфической, чем T , которое просто подтверждает протокол IntegerType. Так как SignedIntegerType наследуется от IntegerType, то он и является более специфическим, и будет выбран при “перегрузке”.

Я указал на это не потому, что есть нечеткость в отношении иерархии протоколов из стандартной библиотеке, а потому что очень легко получить  непредвиденную версию “перегрузки” функции. По этой причине, существует хорошее правило: никогда не “перегружать” функции с различной функциональностью. “Перегружают” для оптимизации производительности , или для распространения функции на новый пользовательский тип данных, или для снабжения  вызывающей функции более мощным, но в основе своей эквивалентным типом (как с  Range и ClosedInterval). Но “перегрузка” с целью изменения исходной функциональности рано или поздно проявится неблагоприятно.

В любом случае, с этой мини-лекцией у нас уже достаточно детальной информации для ответа на первоначальный вопрос – почему выбирается Range ? Мы рассмотрим это в следующей статье.

Leave a Reply

Your email address will not be published. Required fields are marked *