Вторая функция дает вам возможность, которая не используется в данном конкретном случае: она фиксирует тип получателя в параметре типа T
, так что вы можете использовать его где-нибудь еще в подпись, как в типах параметров или типе возвращаемого значения, или в теле функции.
В качестве довольно синтетического примера listOf(this, this)
внутри второй функции будет напечатано как List<T?>
, сохраняя при этом знание, что тип элементов совпадает с типом получателя, тогда как то же выражение в первой функции будет List<Any?>
.
Первая функция не позволяет использовать тип получателя в общем случае для хранения элементов этого типа, принятия дополнительных элементов того же типа в качестве параметров или использования типа получателя в типе возвращаемого значения функции, в то время как вторая функция позволяет все это.
Эти функции эквивалентны с точки зрения времени выполнения, так как обобщения стираются из байт-кода JVM при компиляции кода, поэтому вы не сможете определить тип T
во время выполнения и действовать в зависимости от этого, если вы не конвертируете функцию в функцию inline
с параметром типа reified
.
В качестве очень важного особого случая захват типа с сайта вызова в параметр типа позволяет функции более высокого порядка принимать другую функцию, используя T
в своей подписи. Стандартная библиотека имеет набор функций определения объема (run
, apply
, let
, also
), которые показывают разницу.
Предположим, что подпись also
не использовала дженерики и выглядела так:
fun Any?.also(block: (Any?) -> Unit): Any? { ... }
Эта функция может быть вызвана для любого объекта, но ее подпись не показывает, что это объект-получатель, который передается в block
и возвращается из функции - компилятор не сможет обеспечить безопасность типов и например, разрешить вызов члена объекта-получателя без проверки типа:
val s: String = "abc"
// won't compile: `it` is typed as `Any?`, the returned value is `Any?`, too
val ss1: String = (s + s).also { println(it.length) }
// this will work, but it's too noisy
val ss2: String = (s + s).also { println((it as String).length) } as String
Теперь, захват параметра type - это точно способ показать, что это один и тот же тип во всех трех местах. Мы изменяем подпись следующим образом:
fun <T : Any?> T.also(block: (T) -> Unit): T { ... }
И теперь компилятор может выводить типы, зная, что это тот же тип T
везде, где он появляется:
val s: String = "abc"
// OK!
val ss: String = (s + s).also { println(it.length) }