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 подстановка без рантайм-накладных расходов.