Дополнительное продвижение Swift против универсального разрешения перегрузки

Пожалуйста, рассмотрите следующий код:

protocol P {}
class X {}
class Y: P {}

func foo<T>(_ closure: (T) -> Void) { print(type(of: closure)) }
func foo<T>(_ closure: (T) -> Void) where T: P { print(type(of: closure)) }

let xClosure: (X?) -> Void = { _ in }
foo(xClosure)   //  prints "(Optional<X>) -> ()"
let yClosure: (Y?) -> Void = { _ in }
foo(yClosure)   //  prints "(Y) -> ()"

Почему вызов foo(yClosure) разрешается в версию foo, ограниченную T: P? Я понимаю, почему эта версия печатает то, что печатает, но я не понимаю, почему она вызывается вместо другой.

Мне кажется, что версия без P лучше подойдет для T == (Y?) -> Void. Конечно, ограниченная версия более специфична, но требует преобразования (неявное преобразование из (Y?) -> Void в (Y) -> Void), в то время как не-P версия может быть вызвана без преобразования.

Есть ли способ исправить этот код таким образом, чтобы P-ограниченная версия вызывалась только в том случае, если тип параметра переданного замыкания напрямую соответствует P без каких-либо неявных преобразований?


person imre    schedule 11.06.2020    source источник
comment
Это действительно странно. На самом деле я бы не ожидал, что это вообще скомпилируется, поскольку вы определяете аргументы закрытия с необязательными параметрами. Еще более странно то, что хотя yClosure определяет аргумент закрытия как необязательный, при выводе он утверждает, что аргумент не является необязательным. Хотя это может не ответить на ваш вопрос, вы можете определить еще две функции foo, которые принимают закрытие с необязательными параметрами. Это даст вам результаты, которые вы ищете. Но я не думаю, что это должно быть необходимо.   -  person Rob    schedule 11.06.2020
comment
@RobertCrabtree Как только компилятор выбирает P-ограниченную версию foo, я думаю, что потеря необязательности может быть объяснена. Y? не соответствует P, но Y соответствует, так что эту версию foo можно вызвать, только выполнив T == Y (в отличие от T == Y?) и преобразовав замыкание из (Y?) -> Void в (Y) -> Void. Это преобразование допустимо и может происходить неявно (let cl: (Y) -> Void = yClosure тоже работает, а также forums.swift.org/t/implicit-promotion-of-Optional/12381/4). Но я все еще не понимаю, почему эта перегрузка foo выбирается в первую очередь.   -  person imre    schedule 11.06.2020
comment
@RobertCrabtree Добавление дополнительных перегрузок для closure: (T?) -> Void работает, но не идеально, потому что в реальном коде, где я столкнулся с этим, замыкание имеет несколько параметров, любой из них может быть необязательным, поэтому это приведет к комбинаторному взрыву. Но в этом простом случае это работает.   -  person imre    schedule 11.06.2020


Ответы (1)


Судя по моим экспериментам, специфичность всегда превосходит конверсию дисперсии. Например:

func bar<T>(_ x: [Int], _ y: T) { print("A") }
func bar<T: P>(_ x: [Any], _ y: T) { print("B") }

bar([1], Y()) // A

bar является более конкретным, но требует преобразования дисперсии из [Int] в [Any].

Почему вы можете преобразовать из (Y?) -> Void в (P) -> Void, см. swift">это. Обратите внимание, что Y является подтипом Y? благодаря магии компилятора.

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

У меня есть обходной путь, и я признаю, что это действительно уродливо - создайте свой собственный тип Optional. Назовем его Maybe<T>:

enum Maybe<T> {
    case some(T)
    case none

    // implement all the Optional methods if you want
}

Теперь ваш (Maybe<Y>) -> Void не будет преобразован в (P) -> Void. Обычно я бы не рекомендовал это, но поскольку вы сказали:

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

Я подумал, что новое изобретение Optional может того стоит.

person Sweeper    schedule 29.09.2020