Перегрузка функции неоднозначна с ковариацией типа, но не неоднозначна с общим

#kotlin

Вопрос:

Предположим, у меня есть следующий код.

 open class Parent
class Child: Parent()

fun <T: Parent> foo(obj: T): T = obj
inline fun <T: Parent> foo(block: () -> T): T = foo(block())

fun <T> bar(obj: T): T = obj
inline fun <T> bar(block: () -> T): T = bar(block())

fun main() {
    /* Compile error:
    Overload resolution ambiguity. All these functions match.
        public inline fun <T : Parent> foo (block: () → TypeVariable(T)): TypeVariable(T) defined in examples in file example.kt
        public fun <T : Parent> foo (obj: TypeVariable(T)): TypeVariable(T) defined in examples in file example.kt
     */
    foo { Child() }

    // Works
    bar { "something" }
}
 

Первый вызов функции ( foo ) выдает мне ошибку компиляции, в которой говорится, что разрешение перегрузки неоднозначно. Но почему это так, если блок имеет тип () -> T , а это не подтип Parent ?

Не должна ли эта ошибка возникать при втором вызове функции ( bar )? Почему этого не происходит?

Ответ №1:

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

  1. Чтобы определить значение параметра универсального типа T , и
  2. Чтобы определить сигнатуру лямбда-функции.

В случае foo , разрешение перегрузки зависит от знания значения общего параметра T . Это потому T , что ограничено. Фактическое значение T может повлиять на то, допустимо ли вызывать этот метод или нет, в зависимости от того T , является ли он подтипом Parent .

Проблема в том, что T необходимо сделать вывод на основе значения передаваемого параметра, а тип передаваемого параметра необходимо определить на основе некоторой информации о том, какая перегрузка была выбрана и/или каково значение T ! Я подозреваю, что это создает циклическую зависимость, которая предотвращает завершение разрешения перегрузки.

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

 foo<Parent> { Child() } // works
foo({ Child() } as () -> Parent) // works
 

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

 val lambda = { Child() } // type is inferred as () -> Child
foo(lambda) // works, because lambda already has an inferred type
 

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

Комментарии:

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

2. Примечание: это справедливо, даже если T это переменная свободного типа без каких-либо явных ограничений, так как каждый тип в Kotlin имеет неявное ограничение kotlin.Nothing <: T <: kotlin.Any?

3. Кстати, если бы тип для bar функций был определен с явным ограничением T : Any , он все равно работал бы

Ответ №2:

Похоже на ошибку(ы) в компиляторе (все еще присутствует в версии 1.6.0-M1).

Здесь есть два обходных пути:

  1. Явно укажите аргумент типа:
 foo<Child> { Child() }
 

По иронии судьбы, то же самое с рабочей частью приведет к ее поломке (но ошибка не будет связана с двусмысленностью, компилятор почти уверен, какую перегрузку он вызовет — неправильную).:

 /* Compile error:
Type mismatch.
   Required: () → String
   Found: String
*/
bar<() -> String>({ "something" })
 
  1. Извлеките лямбду в отдельную переменную:
 val lambda = { Child() }
foo(lambda)
 

Это также исправит сломанную деталь:

 val block = { "something" }
bar<() -> String>(block)
 

Комментарии:

1. Тип во втором блоке кода неверен, это было бы bar<String>({ "something" })

2. Да, вероятно, вы изначально намеревались сделать это (тогда вы получите вторую перегрузку), но совершенно законно также указать () -> String тип (чтобы получить первый).

3. Да, но намерение состоит в том, чтобы получить второй

4. Я хочу сказать, что компилятор должен правильно обрабатывать оба случая. Если тип был явно указан как String и был передан аргумент () -> String типа, то неоднозначность должна была быть устранена в пользу второй перегрузки (поскольку для первой требовался аргумент String типа). Если тип был явно указан как () -> String и был передан аргумент () -> String типа, то — в пользу первого (потому что для второго потребовался бы аргумент () -> () -> String типа).