Какие функции вызывает Swift? Часть 2: Единственные аргументы.

Это русский перевод статьи airspeedvelocity 

Which function does Swift call? Part 2: Single Arguments

В предыдущей статье мы видели, что Swift разрешает “перегрузку” ( overloading ) функций просто по типу возвращаемого значения. С такого типа “перегрузками” явное объявление типа говорит Swift какую функцию вы хотите вызвать:

// r будет иметь тип Range<Int>
let r = 1...5

// если вы хотите тип ClosedInterval<Int>, вам следует
// явно указать:
let integer_interval: ClosedInterval = 1...5

Но все это не объясняет, почему мы получаем тип Range по умолчанию, хотя не указывали, какой тип возвращаемого значения мы хотим. Для ответа на этот вопрос нам следует посмотреть на аргументы функции. И мы начнем с простейших функций, у которых один аргумент.

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

Согласно правилу “большого пальца”, Swift любит выбирать наиболее “специфическую” функцию, возможно, основываясь на передаваемых параметрах. Любимая вещь Swift, вещь, которая “бьет” все другие функции с “единственными” аргументами, это функция с аргументом точно такого типа как мы передаем при вызове.

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

Давайте посмотрим это в действии:

protocol P { }
protocol Q: P { }

struct S: Q { }

// эта функция будет вызвана
// если привызове аргумент будет типа  S
func f(s: S) { print("S") }

// если это не будет определено, эта функция будет следующим
// выбором при вызове с аргументом типа S, так как Q это
// более специфично, чем P:
func f(p: Q) { print("Q") }

// и наконец эта функция
func f(p: P) { print("P") }

f(S())  // печатает S

class C: P { }
class D: C { }
class E: D { }

// эта функция будет выбрана
func f(d: D) { print("D") }

// если предыдущая функция не определена, то следующий выбор будет
func f(c: C) { print("C") }

// если ничего не определено, предыдущая версия
//с протоколом P будет вызвана 

f(E())  // печатается D

Эта приоритетная “перегрузка” позволяет вам писать более специфические функции для специальных типов. Например, предположим, что нам нужна “перегруженная” версия для функции  contains, которая есть у типа ClosedInterval, и которая может осуществлять проверку принадлежности значения диапазону за время, независящее от величины этого диапазона, а не за время линейно возрастающее по мере увеличения диапазона, которое показывают обычные comparators.

Звучит очень похоже на знакомую нам концепцию –  полиморфизм. Но не стоит слишком увлекаться. Важно понимать, что:

“Перегруженные” (overloaded) функции выбираются во время компиляции

Во всех случаях, представленных выше какая функция будет выбрана определяется статически, а не динамически, во время компиляции, а не в run-time. Это означает, что функция определяется по типу переменной, не по тому, куда переменная указывает:

class C { }
class D: C { }

// функция, аргументом которой является базовый класс
func f(c: C) { print("C") }
// функция, аргументом которой является наследуемый (child) класс
func f(d: D) { print("D") }

// объект x ссылается на тип D,
// но x имеет тип C
let x: C = D()

// какая функция будет вызвана определяется
// типом x, а не тем, на что она указывает
f(x)    // напечатает "C" а не "D"

// тоже самое происходит с протоколами...
protocol P { }
struct S: P { }

func f(s: S) { print("S") }
func f(p: P) { print("P") }

let p: P = S()

// несмотря на то, что p указывает на объект типа S,
// будет вызвана версия, которая в качестве аргумента использует P:
f(p)    // напечатает "P" а не "S"

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

class C {
    func f() { print("C") }
}

class D: C {
    override func f() { print("D") }
}

// функция: которая берет child class

// the object x references is of type D,
// but x is of type C:
let x: C = D()

// which method to call is determined by
// what x points to, and not what type it is
x.f()    // Prints "D" _not_ "C"

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

Недостаток этого подхода в следующем:

Функции, которые в качестве аргументов используют протоколы, могут быть неоднозначны

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

protocol P { }
protocol Q { }

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

func f(p: P) { print("P") }
func f(p: Q) { print("Q") }

// error: аргумент должен быть либо P, либо Q
f(s)

// вам необходимо убрать неоднозначность, либо:
f(s as P)  // печатается P

// либо:
let q: Q = S()
f(q)       // печатается Q

// тоже самое происходит с классами против протоколов:
class C  { }
class D: C, P { }

func f(c: C) { print("C") }

// error: аргумент должен быть либо C, либо P
f(D())
// вы должны определить, либо:
f(D() as P)
// либо:
f(D() as C)

Выбор типа ClosedInterval по умолчанию для Integers

Итак, зная теперь, что функция с аргументов ввиде struct будет выбрана в приоритетном порядке по сравнению с функцией с протоколом в виде аргумента, мы могли бы написать версию оператора ... , которая берет в качестве аргументов  struct (Int), и которая “бьет” текущую версию, которая берет в качестве аргументов протоколы  (Comparable для создания ClosedInterval, и ForwardIndex для создания Range):

func ...(start: Int, end: Int) -> ClosedInterval<Int> {
    return ClosedInterval(start, end)
}

// x is now of type ClosedInterval
let x = 1...5

Однако это не является очень уж привлекательным. Если вы передадите функции аргументы другого целого типа, вы опять получите  Range:

let i: Int32 = 1, j: Int32 = 5

// r will be of type Range still:
let r = i...j

Было бы лучше написать generic версию, которая брала бы IntegerType.

И все равно это не объясняет, почему диапазоны Range являются более предпочтительными, чем закрытые интервалы СlosedInterval. Протоколы Comparable и ForwardIndex не наследуются друг от друга. Почему они не являются неоднозначными как в нашем выше приведенном с многожественным наследованием протоколов?

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

Leave a Reply

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