#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:
Я предполагаю, что проблема здесь в выводе типов. В каждом из ваших примеров вы полагаетесь на вывод типа для двух вещей:
- Чтобы определить значение параметра универсального типа
T
, и - Чтобы определить сигнатуру лямбда-функции.
В случае 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).
Здесь есть два обходных пути:
- Явно укажите аргумент типа:
foo<Child> { Child() }
По иронии судьбы, то же самое с рабочей частью приведет к ее поломке (но ошибка не будет связана с двусмысленностью, компилятор почти уверен, какую перегрузку он вызовет — неправильную).:
/* Compile error:
Type mismatch.
Required: () → String
Found: String
*/
bar<() -> String>({ "something" })
- Извлеките лямбду в отдельную переменную:
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
типа).