Что в сигнатуре тебе моей?
Есть в kotlin-stdlib функция
text.ifEmpty { default }.
Реализована она крайне просто:
if (isEmpty()) defaultValue() else this.
Интересна же функция тем, как она обходится с типами:
"".ifEmpty { "" } // String
"".ifEmpty { null } // String?
stringBuilder.ifEmpty { "" } // CharSequence & …
"".ifEmpty { 1 } // Comparable<*> & Serializable
Такой вывод типов следует из сигнатуры. Давайте разберём поэтапно, в чём там волшебство.
Например, если бы сигнатура была
String.ifEmpty(() -> String): String
то такое использование было бы невозможно:
stringBuilder.ifEmpty { null }
❗️ ресивер должен быть строкой, а не билдером,
❗️ лямбда должна возвращать строку, а не null.
То есть к функции можно сформулировать такие требования:
🔹 тип ресивера должен быть как можно шире. Функция
isEmpty() объявлена у
CharSequence — он-то нам и нужен,
🔹 тип дефолта должен быть ещё шире: там хоть
null, хоть попугаи, хоть козявки — мы с ними никак не взаимодействуем,
🔹 возвращаемый тип должен сужаться по типам параметров: если там нет нуллов — их не должно быть на выходе и так далее.
Пусть будет такая сигнатура:
fun <C : CharSequence, T, R> C.ifEmpty(() -> T): R.
Что будем писать в
where?
Объединений и пересечений в Котлине нет, то есть на месте
R нельзя написать ни
C & T, ни
C | T.
Интуитивно хочется написать
R : C, R : T. Но это неверно: тут написано «возврат у́же, чем ресивер, и у́же, чем дефолт». У нас же требование противоположное:
C и
T должны «пролезть» на выход, через
R. Правильное напрваление —
C : R, T : R. Правда, компилятор корёжит, что
C и так
: CharSequence, куда тут ещё баунд.
Настоящая сигнатура:
@kotlin.internal.InlineOnly
public inline fun <C, R> C.ifEmpty(
defaultValue: () -> R,
): R where C : CharSequence, C : R {
Ошибку компиляции здесь затыкает приватная аннотация
InlineOnly. Нам так делать нельзя,
зато можно @Suppress("BOUNDS_NOT_ALLOWED_IF_BOUNDED_BY_TYPE_PARAMETER").
Обсуждение синтаксиса, который позволял бы писать подобные сигнатуры без изврата:
KT-209