expect 與 actual 宣告
expect 與 actual 宣告讓您可以從 Kotlin Multiplatform 模組存取平台特定的 API。 您可以在通用程式碼中提供與平台無關的 API。
本文說明
expect與actual宣告的語言機制。有關使用平台特定 API 的不同方式的一般建議,請參閱使用平台特定 API。
expect 與 actual 宣告的規則
要定義 expect 與 actual 宣告,請遵循以下規則:
- 在 common 原始碼集中,宣告一個標準的 Kotlin 結構。這可以是函式、屬性、類別、介面、列舉或註解。
- 使用
expect關鍵字標記此結構。這就是您的 expect 宣告。這些宣告可以在通用程式碼中使用,但不應包含任何實作。相反地,平台特定的程式碼會提供此實作。 - 在每個平台特定的原始碼集中,於相同的套件中宣告相同的結構,並使用
actual關鍵字標記。這就是您的 actual 宣告,它通常包含使用平台特定程式庫的實作。
在為特定目標進行編譯期間,編譯器會嘗試將它找到的每個 actual 宣告與通用程式碼中相應的 expect 宣告進行比對。編譯器會確保:
- common 原始碼集中的每個
expect宣告在每個平台特定的原始碼集中都有一個匹配的actual宣告。 expect宣告不包含任何實作。- 每個
actual宣告與對應的expect宣告共享相同的套件,例如org.mygroup.myapp.MyType。
在為不同平台產生結果程式碼時,Kotlin 編譯器會合併彼此對應的 expect 與 actual 宣告。它會為每個平台產生一個帶有其實際實作的宣告。通用程式碼中對 expect 宣告的每次使用都會呼叫結果平台程式碼中正確的 actual 宣告。
當您使用在不同目標平台之間共享的中間原始碼集時,可以宣告 actual 宣告。例如,假設 iosMain 作為在 iosArm64Main 和 iosSimulatorArm64Main 平台原始碼集之間共享的中間原始碼集。通常只有 iosMain 包含 actual 宣告,而平台原始碼集則不包含。然後,Kotlin 編譯器將使用這些 actual 宣告來產生相應平台的結果程式碼。
IDE 會協助處理常見問題,包括:
- 遺漏宣告
- 包含實作的
expect宣告 - 宣告簽章不符
- 不同套件中的宣告
您還可以使用 IDE 從 expect 導覽至 actual 宣告。選取裝訂邊圖示以檢視 actual 宣告,或使用快速鍵。

使用 expect 與 actual 宣告的不同方法
讓我們探索使用 expect/actual 機制的不同選項,以解決存取平台 API 的問題,同時仍提供在通用程式碼中處理它們的方法。
考慮一個 Kotlin Multiplatform 專案,您需要在其中實作 Identity 型別,該型別應包含使用者的登入名稱和目前的處理程序 ID。該專案具有 commonMain、jvmMain 和 nativeMain 原始碼集,以使應用程式在 JVM 和 iOS 等原生環境中運作。
expect 與 actual 函式
您可以定義一個 Identity 型別和一個工廠函式 buildIdentity(),該函式在 common 原始碼集中宣告,並在平台原始碼集中以不同方式實作:
在
commonMain中,宣告一個簡單型別並預期一個工廠函式:kotlinpackage identity class Identity(val userName: String, val processID: Long) expect fun buildIdentity(): Identity在
jvmMain原始碼集中,使用標準 Java 程式庫實作解決方案:kotlinpackage identity import java.lang.System import java.lang.ProcessHandle actual fun buildIdentity() = Identity( System.getProperty("user.name") ?: "None", ProcessHandle.current().pid() )在
nativeMain原始碼集中,使用原生相依性透過 POSIX 實作解決方案:kotlinpackage identity import kotlinx.cinterop.toKString import platform.posix.getlogin import platform.posix.getpid actual fun buildIdentity() = Identity( getlogin()?.toKString() ?: "None", getpid().toLong() )在這裡,平台函式會傳回平台特定的
Identity執行個體。
從 Kotlin 1.9.0 開始,使用
getlogin()和getpid()函式需要@OptIn註解。
具有 expect 與 actual 函式的介面
如果工廠函式變得太大,請考慮使用通用的 Identity 介面,並在不同平台上進行不同的實作。
buildIdentity() 工廠函式應傳回 Identity,但這次,它是實作通用介面的物件:
在
commonMain中,定義Identity介面和buildIdentity()工廠函式:kotlin// 在 commonMain 原始碼集中: expect fun buildIdentity(): Identity interface Identity { val userName: String val processID: Long }建立介面的平台特定實作,而無需額外使用
expect與actual宣告:kotlin// 在 jvmMain 原始碼集中: actual fun buildIdentity(): Identity = JVMIdentity() class JVMIdentity( override val userName: String = System.getProperty("user.name") ?: "none", override val processID: Long = ProcessHandle.current().pid() ) : Identitykotlin// 在 nativeMain 原始碼集中: actual fun buildIdentity(): Identity = NativeIdentity() class NativeIdentity( override val userName: String = getlogin()?.toKString() ?: "None", override val processID: Long = getpid().toLong() ) : Identity
這些平台函式會傳回平台特定的 Identity 執行個體,這些執行個體實作為 JVMIdentity 和 NativeIdentity 平台型別。
expect 與 actual 屬性
您可以修改前面的範例,並預期一個 val 屬性來儲存 Identity。
將此屬性標記為 expect val,然後在平台原始碼集中將其實例化(actualize):
//在 commonMain 原始碼集中:
expect val identity: Identity
interface Identity {
val userName: String
val processID: Long
}//在 jvmMain 原始碼集中:
actual val identity: Identity = JVMIdentity()
class JVMIdentity(
override val userName: String = System.getProperty("user.name") ?: "none",
override val processID: Long = ProcessHandle.current().pid()
) : Identity//在 nativeMain 原始碼集中:
actual val identity: Identity = NativeIdentity()
class NativeIdentity(
override val userName: String = getlogin()?.toKString() ?: "None",
override val processID: Long = getpid().toLong()
) : Identityexpect 與 actual 物件
當 IdentityBuilder 在每個平台上預期為單例(singleton)時,您可以將其定義為一個 expect object,並讓平台將其實例化:
// 在 commonMain 原始碼集中:
expect object IdentityBuilder {
fun build(): Identity
}
class Identity(
val userName: String,
val processID: Long
)// 在 jvmMain 原始碼集中:
actual object IdentityBuilder {
actual fun build() = Identity(
System.getProperty("user.name") ?: "none",
ProcessHandle.current().pid()
)
}// 在 nativeMain 原始碼集中:
actual object IdentityBuilder {
actual fun build() = Identity(
getlogin()?.toKString() ?: "None",
getpid().toLong()
)
}相依注入建議
為了建立鬆散耦合的架構,許多 Kotlin 專案採用相依注入(DI)架構。DI 架構允許根據目前環境將相依性注入到組件中。
例如,您可能在測試和生產環境中注入不同的相依性,或者在部署到雲端與在本機代管時注入不同的相依性。只要相依性是透過介面表達的,就可以在編譯時期或執行時期注入任意數量的不同實作。
當相依性是平台特定時,同樣的原則也適用。在通用程式碼中,組件可以使用一般的 Kotlin 介面來表達其相依性。然後可以配置 DI 架構以注入平台特定的實作,例如來自 JVM 或 iOS 模組的實作。
這意味著 expect 與 actual 宣告僅在 DI 架構的配置中需要。有關範例,請參閱使用平台特定 API。
透過這種方法,您只需使用介面和工廠函式即可採用 Kotlin Multiplatform。如果您已經在專案中使用 DI 架構來管理相依性,我們建議使用相同的方法來管理平台相依性。
expect 與 actual 類別
expect與actual類別目前處於 Beta 階段。它們幾乎已經穩定,但未來可能需要遷移步驟。我們將盡力減少您需要進行的任何進一步更改。
您可以使用 expect 與 actual 類別來實作相同的解決方案:
// 在 commonMain 原始碼集中:
expect class Identity() {
val userName: String
val processID: Int
}// 在 jvmMain 原始碼集中:
actual class Identity {
actual val userName: String = System.getProperty("user.name") ?: "None"
actual val processID: Long = ProcessHandle.current().pid()
}// 在 nativeMain 原始碼集中:
actual class Identity {
actual val userName: String = getlogin()?.toKString() ?: "None"
actual val processID: Long = getpid().toLong()
}您可能已經在演示材料中看過這種方法。但是,不建議在可以使用介面的簡單情況下使用類別。
使用介面,您不會將設計限制為每個目標平台一個實作。此外,在測試中替換虛假實作或在單個平台上提供多個實作要容易得多。
作為一般規則,請盡可能依賴標準語言結構,而不是使用 expect 與 actual 宣告。
如果您確實決定使用 expect 與 actual 類別,Kotlin 編譯器會針對該功能的 Beta 狀態發出警告。要隱藏此警告,請將以下編譯器選項添加到您的 Gradle 組建檔案中:
kotlin {
compilerOptions {
// 套用於所有 Kotlin 原始碼集的通用編譯器選項
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關鍵字宣告一個類別:kotlinexpect class CommonIdentity() { val userName: String val processID: Long }在
nativeMain中,提供一個實作功能的actual宣告:kotlinactual class CommonIdentity { actual val userName = getlogin()?.toKString() ?: "None" actual val processID = getpid().toLong() }在
jvmMain中,提供一個繼承自平台特定基底類別的actual宣告:kotlinactual class CommonIdentity : Identity() { actual val userName = login actual val processID = pid }
在這裡,CommonIdentity 型別與您自己的設計相容,同時利用了 JVM 上現有的型別。
在架構中的應用
作為架構作者,您可能也會發現 expect 與 actual 宣告對您的架構很有用。
如果上面的範例是架構的一部分,使用者必須從 CommonIdentity 衍生出一個型別來提供顯示名稱。
在這種情況下,expect 宣告是抽象的,並宣告了一個抽象方法:
// 在架構程式碼庫的 commonMain 中:
expect abstract class CommonIdentity() {
val userName: String
val processID: Long
abstract val displayName: String
}同樣地,actual 實作也是抽象的,並宣告了 displayName 方法:
// 在架構程式碼庫的 nativeMain 中:
actual abstract class CommonIdentity {
actual val userName = getlogin()?.toKString() ?: "None"
actual val processID = getpid().toLong()
actual abstract val displayName: String
}// 在架構程式碼庫的 jvmMain 中:
actual abstract class CommonIdentity : Identity() {
actual val userName = login
actual val processID = pid
actual abstract val displayName: String
}架構使用者需要編寫繼承自 expect 宣告的通用程式碼,並自行實作缺少的方法:
// 在使用者程式碼庫的 commonMain 中:
class MyCommonIdentity : CommonIdentity() {
override val displayName = "Admin"
}進階使用案例
關於 expect 與 actual 宣告有許多特殊情況。
使用型別別名來滿足 actual 宣告
actual 宣告的實作不需要從頭開始編寫。它可以是現有的型別,例如第三方程式庫提供的類別。
只要該型別符合與 expect 宣告相關的所有要求,您就可以使用它。例如,考慮這兩個 expect 宣告:
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 列舉可用於實作第一個 expect 宣告,而 java.time.LocalDate 類別可用於實作第二個。但是,無法直接將 actual 關鍵字添加到這些型別。
相反地,您可以使用型別別名來連接 expect 宣告和平台特定的型別:
actual typealias Month = java.time.Month
actual typealias MyDate = java.time.LocalDate在這種情況下,在與 expect 宣告相同的套件中定義 typealias 宣告,並在其他地方建立被參照的類別。
由於
LocalDate型別使用Month列舉,您需要在通用程式碼中將兩者都宣告為expect類別。
actual 宣告中擴展的可見性
您可以使 actual 實作比相應的 expect 宣告更具可見性。如果您不想對通用用戶端將 API 公開為 public,這將非常有用。
目前,Kotlin 編譯器在可見性更改的情況下會發出錯誤。您可以透過在 actual typealias 宣告中套用 @Suppress("ACTUAL_WITHOUT_EXPECT") 來隱藏此錯誤。從 Kotlin 2.0 開始,此限制將不再適用。
例如,如果您在 common 原始碼集中宣告以下 expect 宣告:
internal expect class Messenger {
fun sendMessage(message: String)
}您也可以在平台特定的原始碼集中使用以下 actual 實作:
@Suppress("ACTUAL_WITHOUT_EXPECT")
public actual typealias Messenger = MyMessenger在這裡,一個 internal 的 expect 類別具有一個使用型別別名的現有 public MyMessenger 的 actual 實作。
實例化(Actualization)時額外的列舉項目
當在 common 原始碼集中使用 expect 宣告列舉時,每個平台模組都應有一個相應的 actual 宣告。這些宣告必須包含相同的列舉常數,但它們也可以包含額外的常數。
當您使用現有的平台列舉將預期的列舉實例化時,這非常有用。例如,考慮 common 原始碼集中的以下列舉:
// 在 commonMain 原始碼集中:
expect enum class Department { IT, HR, Sales }當您在平台原始碼集中為 Department 提供 actual 宣告時,您可以添加額外的常數:
// 在 jvmMain 原始碼集中:
actual enum class Department { IT, HR, Sales, Legal }// 在 nativeMain 原始碼集中:
actual enum class Department { IT, HR, Sales, Marketing }但是,在這種情況下,平台原始碼集中的這些額外常數將與通用程式碼中的常數不匹配。因此,編譯器要求您處理所有額外的情況。
在 Department 上實作 when 結構的函式需要一個 else 子句:
// 需要 else 子句:
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")
}
}expect 註解類別
expect 與 actual 宣告可以與註解一起使用。例如,您可以宣告一個 @XmlSerializable 註解,該註解在每個平台原始碼集中必須有一個相應的 actual 宣告:
@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。
