Ожидаемые и фактические объявления
Ожидаемые и фактические объявления позволяют получать доступ к API конкретных платформ из модулей Kotlin Multiplatform. В общем коде можно предоставлять API, не зависящие от платформы.
Правила для ожидаемых и фактических объявлений
Чтобы определить ожидаемые и фактические объявления, следуйте этим правилам:
В общем наборе исходного кода объявите стандартную конструкцию Kotlin. Это может быть функция, свойство, класс, интерфейс, перечисление или аннотация.
Пометьте эту конструкцию ключевым словом
expect. Это ваше ожидаемое объявление. Эти объявления можно использовать в общем коде, но они не должны содержать реализацию. Вместо этого реализацию предоставляет код для конкретной платформы.В каждом наборе исходного кода для конкретной платформы объявите такую же конструкцию в том же пакете и пометьте её ключевым словом
actual. Это ваше фактическое объявление, которое обычно содержит реализацию с использованием библиотек конкретной платформы.
При компиляции для определённой целевой платформы компилятор пытается сопоставить каждое найденное фактическое объявление с соответствующим ожидаемым объявлением в общем коде. Компилятор проверяет, что:
Для каждого ожидаемого объявления в общем наборе исходного кода есть соответствующее фактическое объявление в каждом наборе исходного кода для конкретной платформы.
Ожидаемые объявления не содержат реализации.
Каждое фактическое объявление находится в том же пакете, что и соответствующее ожидаемое объявление, например
org.mygroup.myapp.MyType.
При генерации итогового кода для разных платформ компилятор Kotlin объединяет соответствующие друг другу ожидаемые и фактические объявления. Для каждой платформы он генерирует одно объявление с фактической реализацией. Каждый вызов ожидаемого объявления из общего кода обращается к правильному фактическому объявлению в итоговом коде платформы.
Фактические объявления можно объявлять при использовании промежуточных наборов исходного кода, общих для разных целевых платформ. Например, рассмотрим iosMain — промежуточный набор исходного кода, общий для наборов исходного кода платформ iosArm64Main и iosSimulatorArm64Main. Обычно фактические объявления находятся только в iosMain, а не в наборах исходного кода платформ. Затем компилятор Kotlin использует эти фактические объявления для создания итогового кода соответствующих платформ.
IDE помогает решать распространённые проблемы, в том числе:
Отсутствующие объявления
Ожидаемые объявления с реализациями
Несовпадающие сигнатуры объявлений
Объявления в разных пакетах
Также можно использовать IDE для перехода от ожидаемых объявлений к фактическим. Выберите значок на поле редактора, чтобы просмотреть фактические объявления, или воспользуйтесь сочетаниями клавиш.
Различные способы использования ожидаемых и фактических объявлений
Рассмотрим различные способы применения механизма expect/actual для доступа к API платформ и работы с ними в общем коде.
Рассмотрим проект Kotlin Multiplatform, в котором нужно реализовать тип Identity, содержащий имя пользователя для входа и идентификатор текущего процесса. В проекте есть наборы исходного кода commonMain, jvmMain и nativeMain, необходимые для работы приложения на JVM и в нативных средах, таких как iOS.
Ожидаемые и фактические функции
Можно определить тип Identity и фабричную функцию buildIdentity(), объявленную в общем наборе исходного кода и реализованную по-разному в наборах исходного кода платформ:
-
В
commonMainобъявите простой тип и ожидаемую фабричную функцию:package identity class Identity(val userName: String, val processID: Long) expect fun buildIdentity(): Identity
-
В наборе исходного кода
jvmMainреализуйте решение с использованием стандартных библиотек Java:package identity import java.lang.System import java.lang.ProcessHandle actual fun buildIdentity() = Identity( System.getProperty("user.name") ?: "None", ProcessHandle.current().pid() ) -
В наборе исходного кода
nativeMainреализуйте решение с помощью POSIX и нативных зависимостей:package identity import kotlinx.cinterop.toKString import platform.posix.getlogin import platform.posix.getpid actual fun buildIdentity() = Identity( getlogin()?.toKString() ?: "None", getpid().toLong() )
В этом примере функции платформ возвращают экземпляры Identity, специфичные для платформы.
Интерфейсы с ожидаемыми и фактическими функциями
Если фабричная функция становится слишком большой, рассмотрите возможность использования общего интерфейса Identity с разными реализациями для разных платформ.
Фабричная функция buildIdentity() должна возвращать Identity, но на этот раз это будет объект, реализующий общий интерфейс:
-
В
commonMainопределите интерфейсIdentityи фабричную функциюbuildIdentity():// In the commonMain source set: expect fun buildIdentity(): Identity interface Identity { val userName: String val processID: Long } -
Создайте реализации интерфейса для конкретных платформ без дополнительного использования ожидаемых и фактических объявлений:
// In the jvmMain source set: actual fun buildIdentity(): Identity = JVMIdentity() class JVMIdentity( override val userName: String = System.getProperty("user.name") ?: "none", override val processID: Long = ProcessHandle.current().pid() ) : Identity// In the nativeMain source set: actual fun buildIdentity(): Identity = NativeIdentity() class NativeIdentity( override val userName: String = getlogin()?.toKString() ?: "None", override val processID: Long = getpid().toLong() ) : Identity
Эти функции платформ возвращают экземпляры Identity, специфичные для платформы и реализованные с использованием типов платформы JVMIdentity и NativeIdentity.
Ожидаемые и фактические свойства
Можно изменить предыдущий пример и объявить ожидаемое свойство val для хранения Identity.
Пометьте это свойство как expect val, а затем реализуйте его в наборах исходного кода платформ:
//In commonMain source set:
expect val identity: Identity
interface Identity {
val userName: String
val processID: Long
}
//In jvmMain source set:
actual val identity: Identity = JVMIdentity()
class JVMIdentity(
override val userName: String = System.getProperty("user.name") ?: "none",
override val processID: Long = ProcessHandle.current().pid()
) : Identity
//In nativeMain source set:
actual val identity: Identity = NativeIdentity()
class NativeIdentity(
override val userName: String = getlogin()?.toKString() ?: "None",
override val processID: Long = getpid().toLong()
) : Identity
Ожидаемые и фактические объекты
Если IdentityBuilder должен быть синглтоном на каждой платформе, можно объявить его как ожидаемый объект и реализовать для каждой платформы:
// In the commonMain source set:
expect object IdentityBuilder {
fun build(): Identity
}
class Identity(
val userName: String,
val processID: Long
)
// In the jvmMain source set:
actual object IdentityBuilder {
actual fun build() = Identity(
System.getProperty("user.name") ?: "none",
ProcessHandle.current().pid()
)
}
// In the nativeMain source set:
actual object IdentityBuilder {
actual fun build() = Identity(
getlogin()?.toKString() ?: "None",
getpid().toLong()
)
}
Рекомендации по внедрению зависимостей
Для создания слабосвязанной архитектуры во многих проектах Kotlin используют фреймворк внедрения зависимостей (DI). Фреймворк DI позволяет внедрять зависимости в компоненты с учётом текущего окружения.
Например, можно внедрять разные зависимости при тестировании и в production, а также при развёртывании в облаке и при локальном размещении. Если зависимость выражена через интерфейс, можно внедрить любое количество различных реализаций — во время компиляции или выполнения.
Тот же принцип применим, если зависимости относятся к конкретным платформам. В общем коде компонент может выражать свои зависимости с помощью обычных интерфейсов Kotlin. Затем фреймворк DI можно настроить для внедрения реализации, специфичной для платформы, например из модуля JVM или iOS.
Это означает, что ожидаемые и фактические объявления нужны только в конфигурации фреймворка DI. Примеры см. в разделе Использование API конкретных платформ.
Такой подход позволяет использовать Kotlin Multiplatform, просто применяя интерфейсы и фабричные функции. Если вы уже используете фреймворк DI для управления зависимостями в проекте, рекомендуем применять тот же подход для управления зависимостями платформ.
Ожидаемые и фактические классы
Для реализации того же решения можно использовать ожидаемые и фактические классы:
// In the commonMain source set:
expect class Identity() {
val userName: String
val processID: Int
}
// In the jvmMain source set:
actual class Identity {
actual val userName: String = System.getProperty("user.name") ?: "None"
actual val processID: Long = ProcessHandle.current().pid()
}
// In the nativeMain source set:
actual class Identity {
actual val userName: String = getlogin()?.toKString() ?: "None"
actual val processID: Long = getpid().toLong()
}
Возможно, вы уже встречали этот подход в демонстрационных материалах. Однако не рекомендуется использовать классы в простых случаях, когда достаточно интерфейсов.
Интерфейсы не ограничивают ваш дизайн одной реализацией для каждой целевой платформы. Кроме того, в тестах гораздо проще подменить реализацию фиктивной или предоставить несколько реализаций для одной платформы.
Как правило, по возможности используйте стандартные языковые конструкции вместо ожидаемых и фактических объявлений.
Если вы всё же решите использовать ожидаемые и фактические классы, компилятор Kotlin предупредит вас о бета-статусе этой функции. Чтобы отключить это предупреждение, добавьте следующий параметр компилятора в файл сборки Gradle:
kotlin {
compilerOptions {
// Common compiler options applied to all Kotlin source sets
freeCompilerArgs.add("-Xexpect-actual-classes")
}
}
Наследование от классов платформы
В некоторых случаях использование ключевого слова expect с классами может быть оптимальным решением. Допустим, тип Identity уже существует на JVM:
open class Identity {
val login: String = System.getProperty("user.name") ?: "none"
val pid: Long = ProcessHandle.current().pid()
}
Чтобы встроить тип Identity в существующую кодовую базу и фреймворки, его реализация может наследоваться от этого типа и повторно использовать его функциональность:
-
Чтобы решить эту задачу, объявите класс в
commonMainс помощью ключевого словаexpect:expect class CommonIdentity() { val userName: String val processID: Long } -
В
nativeMainпредоставьте фактическое объявление с реализацией этой функциональности:actual class CommonIdentity { actual val userName = getlogin()?.toKString() ?: "None" actual val processID = getpid().toLong() } -
В
jvmMainпредоставьте фактическое объявление, наследуемое от базового класса конкретной платформы:actual class CommonIdentity : Identity() { actual val userName = login actual val processID = pid }
В этом случае тип CommonIdentity соответствует вашему дизайну и при этом использует существующий тип на JVM.
Использование во фреймворках
Если вы создаёте фреймворк, ожидаемые и фактические объявления тоже могут оказаться полезными.
Если приведённый выше пример является частью фреймворка, пользователь должен унаследовать тип CommonIdentity, чтобы задать отображаемое имя.
В этом случае ожидаемое объявление является абстрактным и объявляет абстрактный метод:
// In commonMain of the framework codebase:
expect abstract class CommonIdentity() {
val userName: String
val processID: Long
abstract val displayName: String
}
Аналогично, фактические реализации являются абстрактными и объявляют метод displayName:
// In nativeMain of the framework codebase:
actual abstract class CommonIdentity {
actual val userName = getlogin()?.toKString() ?: "None"
actual val processID = getpid().toLong()
actual abstract val displayName: String
}
// In jvmMain of the framework codebase:
actual abstract class CommonIdentity : Identity() {
actual val userName = login
actual val processID = pid
actual abstract val displayName: String
}
Пользователям фреймворка нужно написать общий код, унаследованный от ожидаемого объявления, и самостоятельно реализовать недостающий метод:
// In commonMain of the users' codebase:
class MyCommonIdentity : CommonIdentity() {
override val displayName = "Admin"
}
Дополнительные варианты использования
С ожидаемыми и фактическими объявлениями связано несколько особых случаев.
Использование псевдонимов типов для фактических объявлений
Реализацию фактического объявления не обязательно писать с нуля. Это может быть существующий тип, например класс из сторонней библиотеки.
Этот тип можно использовать, если он отвечает всем требованиям ожидаемого объявления. Например, рассмотрим следующие два ожидаемых объявления:
expect enum class Month {
JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY,
AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER
}
expect class MyDate {
fun getYear(): Int
fun getMonth(): Month
fun getDayOfMonth(): Int
}
В модуле JVM для реализации первого ожидаемого объявления можно использовать перечисление java.time.Month, а для реализации второго — класс java.time.LocalDate. Однако добавить ключевое слово actual непосредственно к этим типам нельзя.
Вместо этого можно использовать псевдонимы типов, чтобы связать ожидаемые объявления с типами конкретной платформы:
actual typealias Month = java.time.Month actual typealias MyDate = java.time.LocalDate
В этом случае объявите typealias в том же пакете, что и ожидаемое объявление, а класс, на который он ссылается, создайте в другом месте.
Более широкая видимость фактических объявлений
Фактические реализации могут быть более видимыми, чем соответствующее ожидаемое объявление. Это полезно, если вы не хотите предоставлять свой API клиентам общего кода как общедоступный.
В настоящее время компилятор Kotlin выдаёт ошибку при изменении видимости. Эту ошибку можно отключить, применив @Suppress("ACTUAL_WITHOUT_EXPECT") к объявлению фактического псевдонима типа. Начиная с Kotlin 2.0 это ограничение будет снято.
Например, если в общем наборе исходного кода объявить следующее ожидаемое объявление:
internal expect class Messenger {
fun sendMessage(message: String)
}
В наборе исходного кода для конкретной платформы также можно использовать следующую фактическую реализацию:
@Suppress("ACTUAL_WITHOUT_EXPECT")
public actual typealias Messenger = MyMessenger
В этом случае фактическая реализация внутреннего ожидаемого класса использует существующий общедоступный MyMessenger с помощью псевдонимов типов.
Дополнительные элементы перечисления при реализации
Если перечисление объявлено с помощью expect в общем наборе исходного кода, в каждом модуле платформы должно быть соответствующее объявление actual. Эти объявления должны содержать те же константы перечисления, но могут включать и дополнительные.
Это полезно, когда ожидаемое перечисление реализуется с помощью существующего перечисления платформы. Например, рассмотрим следующее перечисление в общем наборе исходного кода:
// In the commonMain source set:
expect enum class Department { IT, HR, Sales }
При создании фактического объявления для Department в наборах исходного кода платформ можно добавить дополнительные константы:
// In the jvmMain source set:
actual enum class Department { IT, HR, Sales, Legal }
// In the nativeMain source set:
actual enum class Department { IT, HR, Sales, Marketing }
Однако в этом случае дополнительные константы в наборах исходного кода платформ не будут соответствовать константам в общем коде. Поэтому компилятор требует обработать все дополнительные случаи.
Функция, реализующая конструкцию when в Department, требует ветви else:
// An else clause is required:
fun matchOnDepartment(dept: Department) {
when (dept) {
Department.IT -> println("The IT Department")
Department.HR -> println("The HR Department")
Department.Sales -> println("The Sales Department")
else -> println("Some other department")
}
}
Ожидаемые классы аннотаций
Ожидаемые и фактические объявления можно использовать с аннотациями. Например, можно объявить аннотацию @XmlSerializable, для которой должно быть соответствующее фактическое объявление в каждом наборе исходного кода платформы:
@Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) expect annotation class XmlSerializable() @XmlSerializable class Person(val name: String, val age: Int)
На конкретной платформе может быть полезно повторно использовать существующие типы. Например, на JVM можно определить аннотацию с помощью существующего типа из спецификации JAXB:
import javax.xml.bind.annotation.XmlRootElement actual typealias XmlSerializable = XmlRootElement
При использовании expect с классами аннотаций следует учитывать ещё один момент. Аннотации используются для добавления метаданных к коду и не представлены в сигнатурах как типы. Поэтому фактический класс для ожидаемой аннотации не обязателен на платформе, где она никогда не используется.
Объявление actual нужно предоставлять только на платформах, где используется аннотация. По умолчанию такое поведение отключено, и для него требуется пометить тип аннотацией OptionalExpectation.
Возьмите объявленную выше аннотацию @XmlSerializable и добавьте OptionalExpectation:
@OptIn(ExperimentalMultiplatform::class) @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) @OptionalExpectation expect annotation class XmlSerializable()
Если на платформе, где фактическое объявление не требуется, оно отсутствует, компилятор не выдаст ошибку.
Что дальше?
Общие рекомендации по различным способам использования API конкретных платформ см. в разделе Использование API конкретных платформ.
© 2010–2026 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/multiplatform/multiplatform-expect-actual.html