屬性
在 Kotlin 中,屬性允許您儲存和管理資料,而無需編寫函式來存取或更改這些資料。您可以在 類別、介面、物件、伴生物件 中使用屬性,甚至在這些結構之外作為頂層屬性。
每個屬性都有一個名稱、一個型別,以及一個自動生成的 get() 函式,稱為取得器。您可以使用取得器來讀取屬性的值。如果該屬性是可變的,它也會有一個 set() 函式,稱為設定器,允許您更改屬性的值。
取得器和設定器統稱為 存取器。
宣告屬性
屬性可以是可變的 (var) 或唯讀的 (val)。 您可以在 .kt 檔案中將它們宣告為頂層屬性。可以將頂層屬性視為屬於某個套件的全域變數:
// File: Constants.kt
package my.app
val pi = 3.14159
var counter = 0您也可以在類別、介面或物件內部宣告屬性:
// Class with properties
class Address {
var name: String = "Holmes, Sherlock"
var street: String = "Baker"
var city: String = "London"
}
// Interface with a property
interface ContactInfo {
val email: String
}
// Object with properties
object Company {
var name: String = "Detective Inc."
val country: String = "UK"
}
// Class implementing the interface
class PersonContact : ContactInfo {
override val email: String = "[email protected]"
}要使用屬性,請透過其名稱引用:
class Address {
var name: String = "Holmes, Sherlock"
var street: String = "Baker"
var city: String = "London"
}
interface ContactInfo {
val email: String
}
object Company {
var name: String = "Detective Inc."
val country: String = "UK"
}
class PersonContact : ContactInfo {
override val email: String = "[email protected]"
}
fun copyAddress(address: Address): Address {
val result = Address()
// 存取 result 實例中的屬性
result.name = address.name
result.street = address.street
result.city = address.city
return result
}
fun main() {
val sherlockAddress = Address()
val copy = copyAddress(sherlockAddress)
// 存取 copy 實例中的屬性
println("Copied address: ${copy.name}, ${copy.street}, ${copy.city}")
// 複製的地址:Holmes, Sherlock, Baker, London
// 存取 Company 物件中的屬性
println("Company: ${Company.name} in ${Company.country}")
// 公司:Detective Inc. 在 UK
val contact = PersonContact()
// 存取 contact 實例中的屬性
println("Email: ${contact.email}")
// 電子郵件:[email protected]
}在 Kotlin 中,我們建議您在宣告屬性時就將其初始化,以確保程式碼安全且易於閱讀。然而,在特殊情況下,您可以稍後初始化它們。
如果編譯器可以從初始化器或取得器的回傳型別推斷出屬性型別,則宣告屬性型別是可選的:
var initialized = 1 // 推斷的型別是 Int
var allByDefault // 錯誤:屬性必須初始化。自訂取得器與設定器
預設情況下,Kotlin 會自動生成取得器和設定器。當您需要額外邏輯時,例如驗證、格式化或基於其他屬性的計算,您可以定義自己的自訂存取器。
自訂取得器會在每次存取屬性時執行:
class Rectangle(val width: Int, val height: Int) {
val area: Int
get() = this.width * this.height
}
fun main() {
val rectangle = Rectangle(3, 4)
println("Width=${rectangle.width}, height=${rectangle.height}, area=${rectangle.area}")
}如果編譯器可以從取得器推斷型別,您可以省略它:
val area get() = this.width * this.height自訂設定器會在每次您為屬性賦值時執行,除了其初始化之外。依慣例,設定器參數的名稱是 value,但您可以選擇不同的名稱:
class Point(var x: Int, var y: Int) {
var coordinates: String
get() = "$x,$y"
set(value) {
val parts = value.split(",")
x = parts[0].toInt()
y = parts[1].toInt()
}
}
fun main() {
val location = Point(1, 2)
println(location.coordinates)
// 1,2
location.coordinates = "10,20"
println("${location.x}, ${location.y}")
// 10, 20
}更改可見性或新增註解
在 Kotlin 中,您可以更改存取器可見性或新增註解,而無需替換預設實作。您不需要在 {} 主體內進行這些更改。
要更改存取器的可見性,請在 get 或 set 關鍵字之前使用修飾符:
class BankAccount(initialBalance: Int) {
var balance: Int = initialBalance
// 只有類別可以修改餘額
private set
fun deposit(amount: Int) {
if (amount > 0) balance += amount
}
fun withdraw(amount: Int) {
if (amount > 0 && amount <= balance) balance -= amount
}
}
fun main() {
val account = BankAccount(100)
println("Initial balance: ${account.balance}")
// 初始餘額:100
account.deposit(50)
println("After deposit: ${account.balance}")
// 存款後:150
account.withdraw(70)
println("After withdrawal: ${account.balance}")
// 提款後:80
// account.balance = 1000
// 錯誤:無法賦值,因為設定器是私有的
}要註解存取器,請在 get 或 set 關鍵字之前使用註解:
// 定義一個可套用於取得器的註解
@Target(AnnotationTarget.PROPERTY_GETTER)
annotation class Inject
class Service {
var dependency: String = "Default Service"
// 註解取得器
@Inject get
}
fun main() {
val service = Service()
println(service.dependency)
// 預設服務
println(service::dependency.getter.annotations)
// [@Inject()]
println(service::dependency.setter.annotations)
// []
}此範例使用反射來顯示取得器和設定器上存在哪些註解。
幕後欄位
在 Kotlin 中,存取器使用幕後欄位在記憶體中儲存屬性的值。當您想為取得器或設定器新增額外邏輯,或者想在屬性更改時觸發額外動作時,幕後欄位會很有用。
您無法直接宣告幕後欄位。Kotlin 僅在必要時生成它們。您可以在存取器中使用 field 關鍵字引用幕後欄位。
Kotlin 僅在您使用預設取得器或設定器,或者在至少一個自訂存取器中使用 field 時才會生成幕後欄位。
例如,isEmpty 屬性沒有幕後欄位,因為它使用一個沒有 field 關鍵字的自訂取得器:
val isEmpty: Boolean
get() = this.size == 0在此範例中,score 屬性有一個幕後欄位,因為設定器使用了 field 關鍵字:
class Scoreboard {
var score: Int = 0
set(value) {
field = value
// 更新值時新增日誌
println("Score updated to $field")
}
}
fun main() {
val board = Scoreboard()
board.score = 10
// 分數更新為 10
board.score = 20
// 分數更新為 20
}幕後屬性
有時,您可能需要比使用幕後欄位所能提供的更具彈性。例如,如果您有一個 API,希望能夠在內部修改屬性,但不在外部修改。在這種情況下,您可以使用一種稱為 幕後屬性 的編碼模式。
在以下範例中,ShoppingCart 類別有一個 items 屬性,代表購物車中的所有物品。您希望 items 屬性在類別外部是唯讀的,但仍允許使用者以一種「核准」的方式直接修改 items 屬性。為了實現這一點,您可以定義一個名為 _items 的私有幕後屬性,以及一個名為 items 的公共屬性,該屬性委託給幕後屬性的值。
class ShoppingCart {
// 幕後屬性
private val _items = mutableListOf<String>()
// 公共唯讀視圖
val items: List<String>
get() = _items
fun addItem(item: String) {
_items.add(item)
}
fun removeItem(item: String) {
_items.remove(item)
}
}
fun main() {
val cart = ShoppingCart()
cart.addItem("Apple")
cart.addItem("Banana")
println(cart.items)
// [Apple, Banana]
cart.removeItem("Apple")
println(cart.items)
// [Banana]
}在此範例中,使用者只能透過 addItem() 函式將物品新增到購物車中,但仍可以存取 items 屬性來查看其中包含的物品。
在命名幕後屬性時使用開頭的底線,以遵循 Kotlin 編碼慣例。
在 JVM 上,編譯器會優化對具有預設存取器的私有屬性的存取,以避免函式呼叫的開銷。
當您希望多個公共屬性共享一個狀態時,幕後屬性也很有用。例如:
class Temperature {
// 儲存攝氏溫度的幕後屬性
private var _celsius: Double = 0.0
var celsius: Double
get() = _celsius
set(value) { _celsius = value }
var fahrenheit: Double
get() = _celsius * 9 / 5 + 32
set(value) { _celsius = (value - 32) * 5 / 9 }
}
fun main() {
val temp = Temperature()
temp.celsius = 25.0
println("${temp.celsius}°C = ${temp.fahrenheit}°F")
// 25.0°C = 77.0°F
temp.fahrenheit = 212.0
println("${temp.celsius}°C = ${temp.fahrenheit}°F")
// 100.0°C = 212.0°F
}在此範例中,_celsius 幕後屬性被 celsius 和 fahrenheit 這兩個屬性存取。這種設置提供了一個單一的事實來源,並具有兩個公共視圖。
編譯期常數
如果唯讀屬性的值在編譯時已知,請使用 const 修飾符將其標記為 編譯期常數。編譯期常數在編譯時會被內聯,因此每個引用都會被替換為其實際值。它們被更有效地存取,因為沒有呼叫取得器:
// File: AppConfig.kt
package com.example
// 編譯期常數
const val MAX_LOGIN_ATTEMPTS = 3編譯期常數必須滿足以下要求:
編譯期常數仍然有一個幕後欄位,因此您可以使用反射與其互動。
您也可以在註解中使用這些屬性:
const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"
@Deprecated(SUBSYSTEM_DEPRECATED) fun processLegacyOrders() { ... }延遲初始化屬性與變數
通常,您必須在建構函式中初始化屬性。然而,這樣做並不總是方便。例如,您可能透過依賴注入或在單元測試的設定方法中初始化屬性。
為了處理這些情況,請使用 lateinit 修飾符標記屬性:
public class OrderServiceTest {
lateinit var orderService: OrderService
@SetUp fun setup() {
orderService = OrderService()
}
@Test fun processesOrderSuccessfully() {
// 直接呼叫 orderService,無需檢查 null 或初始化
orderService.processOrder()
}
}您可以在宣告為以下情況的 var 屬性上使用 lateinit 修飾符:
- 頂層屬性。
- 區域變數。
- 類別主體內的屬性。
對於類別屬性:
- 您不能在主要建構函式中宣告它們。
- 它們不能有自訂取得器或設定器。
在所有情況下,屬性或變數必須是非可空的,並且不能是基本型別。
如果您在初始化 lateinit 屬性之前存取它,Kotlin 會拋出一個特定例外,該例外會清楚地識別被存取的未初始化屬性:
class ReportGenerator {
lateinit var report: String
fun printReport() {
// 因為在初始化之前存取,所以會拋出例外
println(report)
}
}
fun main() {
val generator = ReportGenerator()
generator.printReport()
// 執行緒 "main" 中的例外 kotlin.UninitializedPropertyAccessException: lateinit 屬性 report 尚未初始化
}要檢查 lateinit var 是否已初始化,請在該屬性的引用上使用 isInitialized 屬性:
class WeatherStation {
lateinit var latestReading: String
fun printReading() {
// 檢查屬性是否已初始化
if (this::latestReading.isInitialized) {
println("Latest reading: $latestReading")
} else {
println("No reading available")
}
}
}
fun main() {
val station = WeatherStation()
station.printReading()
// 無可用讀數
station.latestReading = "22°C, sunny"
station.printReading()
// 最新讀數:22°C,晴朗
}您只能在您已能在程式碼中存取的屬性上使用 isInitialized。該屬性必須在同一類別、外部類別或作為同一檔案中的頂層屬性宣告。
覆寫屬性
請參閱覆寫屬性。
委託屬性
為了重複使用邏輯並減少程式碼重複,您可以將屬性的取得和設定職責委託給一個單獨的物件。
委託存取器行為可使屬性的存取器邏輯集中化,使其更容易重複使用。這種方法在實作以下行為時很有用,例如:
- 延遲計算值。
- 透過給定鍵從映射中讀取。
- 存取資料庫。
- 在存取屬性時通知監聽器。
您可以自行在函式庫中實作這些常見行為,或使用外部函式庫提供的現有委託。如需更多資訊,請參閱委託屬性。
