expect и actual: объявление и реализация

Сердце KMP: общий код объявляет «что должно быть», платформы дают «как именно».

expect/actual — пара ключевых слов: expect объявляет API в общем коде без реализации, а actual предоставляет конкретную реализацию в каждом платформенном наборе.

Зачем нужен механизм

Часть API концептуально общая, но реализуется по-разному. «Какая сейчас платформа?», «дай текущее время», «сгенерируй UUID» — общий код хочет это вызывать, но реализация на Android и iOS разная. expect/actual позволяет объявить такую функцию один раз, а тело дать на каждой платформе. Это компиляторный механизм: связывание expect и actual проверяется на этапе сборки.

Простой пример: функция платформы

В общем коде объявляем ожидаемую функцию:

// commonMain
expect fun platformName(): String

На Android даём реализацию:

// androidMain
actual fun platformName(): String =
    "Android " + android.os.Build.VERSION.SDK_INT

На iOS — свою:

// iosMain
import platform.UIKit.UIDevice
actual fun platformName(): String =
    UIDevice.currentDevice.systemName() + " " +
    UIDevice.currentDevice.systemVersion

Общий код просто вызывает platformName(), а компилятор каждой цели подставит свою actual.

Правила связывания

Сигнатура actual обязана точно соответствовать expect: имя, параметры, тип возврата, пакет. Каждая цель обязана иметь actual — если для iOS реализацию забыли, сборка iOS упадёт с ошибкой «expected declaration has no actual». Это страховка: общий API не может остаться без реализации ни на одной платформе.

Как работает под капотом

Компилятор обрабатывает каждую цель отдельно (помните про объединение source set'ов). Для цели Android он берёт expect из commonMain и сшивает его с actual из androidMain, получая полноценную функцию. Никакого рантайм-диспетчинга нет: к моменту выполнения expect уже «растворился», осталась конкретная реализация. Поэтому expect/actual не несёт накладных расходов — это не виртуальный вызов, а compile-time подстановка.

Частые ошибки

Первая — несовпадение сигнатур: добавили параметр в actual, и связывание сломалось. Вторая — забыть реализацию для одной из целей; Android соберётся, iOS — нет. Третья — пытаться положить expect с телом: expect-функция тела не имеет, иначе это не expect. Держите expect чисто декларативным.

Итоги

  • expect объявляет API в общем коде, actual реализует его на платформе.
  • Каждая цель обязана иметь actual, иначе сборка падает.
  • Сигнатуры expect и actual должны точно совпадать.
  • Это compile-time подстановка без рантайм-накладных расходов.
Проверьте себя
1. Что произойдёт, если для iOS забыть actual-реализацию expect-функции?
AФункция вернёт null
BСборка iOS упадёт с ошибкой об отсутствии actual
CБудет использована Android-реализация
DНичего, всё соберётся
2. Есть ли у expect/actual рантайм-накладные расходы вроде виртуального вызова?
AДа, это виртуальный вызов
BНет, это compile-time подстановка конкретной реализации
CДа, через рефлексию
DЗависит от платформы