← Programming Core

KOTLIN CORE · 108

Null Safety、Collections、Data Class:把「可能沒有」變成型別的一部分

真實 mobile app 很多資料不保證立即存在:最後 GPS fix、使用者名稱、server response、permission result。Kotlin 的 nullable type 逼你在使用前處理這種不確定性。

Learning outcomes

1. Nullable type 是不同的型別

var lastStreetName: String? = null

// val length = lastStreetName.length
// compile error

Compiler 不允許你把「可能為 null」當成「一定有 String」使用。

2. 三種處理 null 的方式

// Safe call
val length = lastStreetName?.length

// Elvis fallback
val displayName =
    lastStreetName ?: "Unknown road"

// Explicit branch
if (lastStreetName != null) {
    println(lastStreetName.length)
}

進入 null check 後,compiler 可以 smart cast,把該 branch 內的值視為 non-null。

3. !! 的真正意思:把風險從 compiler 推回 runtime

val length = lastStreetName!!.length

!! 不是「處理 null」,而是「我保證不是 null;如果我錯了,就允許 runtime crash」。除非你有非常強的 invariant,否則不應拿它當預設寫法。

4. Data class:把 domain data 聚合成一個明確模型

data class TripPoint(
    val latitude: Double,
    val longitude: Double,
    val speedKmh: Double?,
    val timestampMs: Long
)

speedKmh: Double? 明確表達「有些 sample 可能沒有可信速度」。這比用 -1.0 當神秘 sentinel value 更容易維護。

5. copy():建立修改後的新值

data class TripState(
    val distanceKm: Double,
    val elapsedSeconds: Long,
    val running: Boolean
)

val old = TripState(
    distanceKm = 1.2,
    elapsedSeconds = 300,
    running = true
)

val updated = old.copy(
    distanceKm = 1.5
)

data class 常配合 immutable state model:不是到處 mutate 同一個 object,而是產生下一個 state value。

6. Collections:不是只背 map/filter

val points: List<TripPoint> = ...

val validSpeeds =
    points
        .mapNotNull { it.speedKmh }
        .filter { it >= 0.0 }

val average =
    validSpeeds.average()
List<TripPoint>mapNotNullList<Double>filtervalid speedsaverageDouble

7. Read-only collection 不等於永遠 immutable

val mutable = mutableListOf(1, 2)
val readOnly: List<Int> = mutable

mutable.add(3)

println(readOnly) // 仍可能看到 3

Kotlin 的 List interface 是 read-only view;不要自動把它理解成深度 immutable object graph。

8. Project checkpoint:Trip Meter Domain Core v2

data class TripState(
    val distanceKm: Double,
    val elapsedSeconds: Long,
    val lastPoint: TripPoint?
)

fun displaySpeed(state: TripState): String =
    state.lastPoint
        ?.speedKmh
        ?.let { "%.1f km/h".format(it) }
        ?: "--"

fun acceptedPoints(
    points: List<TripPoint>
): List<TripPoint> =
    points.filter { point ->
        point.speedKmh == null
            || point.speedKmh >= 0.0
    }

下一課會處理「location / network 不是立刻回來」這件事:coroutines。

9. Debug evidence:NullPointerException 出現時先找誰把 nullable 假裝成 non-null

如果 stack trace 落在 !!,真正問題通常不是「Kotlin null safety 失效」,而是程式碼主動繞過了 null safety。

Knowledge check

  1. String 與 String? 對 compiler 有什麼不同?
  2. 為什麼 !! 不是 validation?
  3. List 為什麼不代表 underlying object 永遠 immutable?
  4. 設計一個 nullable GPS accuracy 欄位,決定顯示 fallback 的規則。