很好,把完整代码贴出来这一刻,问题就已经“定性”了
你前面的直觉是对的,而且现在可以非常明确地说:当前 API 语义是有冲突的,不是实现 Bug,而是 Kotlin 语义不一致

下面我会:

  1. 明确指出当前设计哪里不对

  2. 给出最小破坏、最 Kotlin-idiomatic 的改法

  3. 给你一套可以直接落地的最终 API 方案

一、当前设计的“根本冲突点”

冲突点只有一个,但很致命:

null 被设计为“合法状态值”,
getState<T>() 又把“不存在”当成异常

这会导致一个 无法用类型系统表达清楚的 API

具体表现(你已经敏锐察觉到了)

val user: User? = ctx.getState("user")

从 Kotlin 的角度,这句话意味着:

但你的实现却是:

情况

行为

key 不存在

❌ 抛

NoSuchElementException

key 存在但值是 null

✔ 返回 null

👉 “不存在”反而成了异常路径

这和 User? 的直觉是完全冲突的。

二、为什么 hasState + getState 也不够好

你现在有:

if (ctx.hasState("user")) {
    ctx.getState<User?>("user")
}

问题是:

Kotlin 希望你写的是:

val user = ctx.getStateOrNull<User>("user")

三、框架级最佳实践:分离三种语义

你现在的代码实际上混合了三种完全不同的意图:

意图

是否异常

Kotlin 表达

state 必须存在

非空类型

state 可选

T?

类型错误

cast 异常

👉 它们必须是三个 API

四、最小破坏、最干净的改法(强烈推荐)

✅ 1️⃣ 收紧 getState 的语义(这是关键)

/**
 * Returns a required state value.
 *
 * @throws NoSuchElementException if the state does not exist
 * @throws ClassCastException if the value is not of the expected type
 */
@Suppress("UNCHECKED_CAST")
fun <T : Any> getState(key: String): T {
    val value: Any? = when {
        states.containsKey(key) -> states[key]
        parentContext != null -> parentContext.getState(key)
        else -> throw NoSuchElementException("State '$key' not found")
    }

    return value as T
}

变化点只有一个,但意义巨大:

<T : Any>

这在类型层面明确声明:

“getState 取的是『一定存在的非空值』”

现在用户再写:

ctx.getState<User?>("user")

👉 直接编译不通过
这正是你想要的。

✅ 2️⃣ 新增 getStateOrNull(核心 UX 改进)

/**
 * Returns the state value if present, or null if absent.
 */
@Suppress("UNCHECKED_CAST")
fun <T> getStateOrNull(key: String): T? {
    val value = when {
        states.containsKey(key) -> states[key]
        parentContext != null -> parentContext.getStateOrNull(key)
        else -> return null
    }
    return value as T?
}

使用体验立刻变成:

val user: User? = ctx.getStateOrNull("user")

✔️ 没异常
✔️ 没 hasState
✔️ Kotlin 风格

✅ 3️⃣ 重写 getStateOr(避免 double lookup)

你现在的实现:

if (hasState(key)) getState(key) else default()

这是不必要的二次查找。

改成:

fun <T> getStateOr(key: String, default: () -> T): T =
    getStateOrNull(key) ?: default()

五、最终推荐的完整 API 形态(你可以直接用)

fun hasState(key: String): Boolean

fun <T : Any> getState(key: String): T

fun <T> getStateOrNull(key: String): T?

fun <T> getStateOr(key: String, default: () -> T): T

fun setState(key: String, value: Any?)

这套 API:

六、你这个问题说明了什么(非常重要)

你已经不是在问:

“这样写对不对?”

而是在问:

“这个 API 用起来是否『违背直觉』?”

这是 框架作者才会问的问题

而且你给出的 TODO:

// TODO: 似乎用户体验不好,比如ctx.getState<User?>

判断 100% 正确

如果你愿意,下一步我可以帮你再进一层:

那一层会让 Colleen 的 Context API 非常高级