Обобщения Kotlin: нелогичный вывод типов и проверка без ключевого слова - PullRequest
3 голосов
/ 19 апреля 2019

Я недавно изучал Kotlin, хотя у меня были вопросы с ковариантным типом.

Пример кода здесь. У меня Option и Option2 оба имеют параметр типа T и расширение run.

Я мог понять первые два run в validation(), поскольку они ведут себя как Java. Но почему третья строка компилируется? Option<T> является инвариантом в T. Мы не можем передать экземпляр Option<C> туда, где ожидается Option<B>.

После того, как я добавил ключевое слово out для T, теперь все они могут скомпилироваться. Почему?

open class A
open class B : A()
open class C : B()


class Option<T>(val item: T)

fun <T> Option<T>.run(func: (Int) -> Option<T>): Option<T> = func(1)


class Option1<out T>(val item: T) //out keyword

fun <T> Option1<T>.run(func: (Int) -> Option1<T>): Option1<T> = func(1)


fun validation() {
    val opt: Option<B> = Option(B())
    opt.run { Option(A()) } //won't compile as expected
    opt.run { Option(B()) } //return type is Option<B>
    opt.run { Option(C()) } //return type is Option<B>; why could this compile?

    val opt1: Option1<B> = Option1(B())
    opt1.run { Option1(A()) } //return type is Option<A>; why could this compile?
    opt1.run { Option1(B()) } //return type is Option<B>
    opt1.run { Option1(C()) } //return type is Option<B>
}

1 Ответ

4 голосов
/ 19 апреля 2019
  • opt.run { Option(C()) } //return type is Option<B>; why could this compile?

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

    val func: (Int) -> Option<B> = { Option(C()) }
    opt.run(func)
    

    Первая строка верна, потому что:

    • ожидается, что лямбда вернет Option<B> (с точно B, поскольку Option является инвариантом),
    • поэтому вызов конструктора Option(item: T): Option<T> должен принимать B,
    • передаваемый аргумент: C(),
    • , когда C : B, C() передаетпроверка на B,
    • и т. д. Option(C()) также может быть набрана как Option<B> и проходит проверку,
    • OK, лямбда-проверка проходит проверку на (Int) -> Option<B>.


    Проверка работоспособности: что если вы замените первую строку следующим образом?

    val func: (Int) -> Option<B> = { Option(C()) as Option<C> }
    

    Тогда она не будет скомпилирована, так как теперь выражение внутри лямбды набранокак Option<C>, который не является подтипом Option<B>.


  • opt1.run { Option1(A()) } //return type is Option<A>; why could this compile?

    В этом примере тип, которыйЛер выбрал для T это не B, это A.Компилятору разрешено делать это из-за ковариации параметра типа T.

    • opt1 is Option1<B>
    • Option1<out T> ковариантно относительно T, что позволяет заменять T любым супертипом B,

      Это разрешено, поскольку для любого Z такого, что B : Z, opt1 также можно трактовать как Option1<out Z> благодаря модификатору out, и компилятор может затем проверить тип вызова по типу получателя Option1<Z>.

    • подстановка для T будетбыть наименее распространенным супертипом B и любым другим X таким образом, что лямбда возвращает Option1<X>,

    • лямбда-выражение возвращает Option1<A>,
    • найти наименее распространенный супертипB и A,
    • , учитывая, что B : A, наименее распространенный супертип - A
    • , заменитель T := A.


    Проверка работоспособности: что, если вы измените выражение следующим образом?

    opt1.run { Option1(0) }
    

    Это все равно будет успешно скомпилировано, но выводит ответрН типа будет Option1<Any>.Это совершенно разумно в соответствии с вышеизложенным, потому что наименее распространенный супертип B и Int равен Any.


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

...