值语义与共享语义
本文档定义 Dujie 在语言层面对“赋值、传参、返回、修改、共享”的基本语义。
本文是类型系统总纲的专题文档,服务于 UI DSL 的当前语义设计,并为未来可能出现的响应式能力保留边界。它约束的是用户可见语义,而不是 Rust 后端的具体实现方式。
问题背景
当前文档中,曾经使用“值类型 / 引用类型”来解释语言行为,并把 string、list<T>、map<K, V>、opt<T>、iter<T>、widget、struct 等都归到“引用类型”里。
这种做法有几个问题:
它把用户可见语义和 Rust 后端实现强绑定
它会制造隐藏别名,降低局部可推理性
它不利于 UI DSL 的状态建模
它会让未来响应式系统和普通数据模型混在一起
对于 Dujie 这样的 UI DSL,普通数据和值的行为应尽量稳定、直接、可局部推导。共享、订阅、依赖传播等能力如果未来需要引入,也应由独立机制显式承担,而不是通过“所有容器类型和结构体默认共享可变状态”间接得到。
设计目标
让赋值、传参、返回值语义对用户可预测
让普通数据建模不依赖隐藏别名
为未来可能的响应式能力保留边界,但不通过普通容器类型偷带共享语义
保留后端使用结构共享、写时复制、引用计数等优化的空间
核心结论
1. Dujie 不对用户暴露通用“引用类型”分类
语言层不再把普通类型分为“值类型”和“引用类型”两大公开类别,并据此要求用户理解别名传播。
2. 普通 Dujie 值默认采用值语义
除非未来某类类型被明确指定为特殊引用型或响应式类型,否则普通 Dujie 值在以下场景都采用值语义:
- 变量赋值
- 函数传参
- 函数返回
- 容器和结构体字段传播
也就是说,这些操作得到的是“逻辑上独立的值”。后续对其中一个值的修改,不应影响另一个值。
3. 存储共享可以作为实现优化,但不能成为用户可观察语义
后端可以使用以下技术降低拷贝成本:
- 结构共享
- 写时复制(copy-on-write)
- 引用计数
- 持久化数据结构
但这些都属于实现策略,不应改变用户在语言层观察到的结果。
4. 未来若引入响应式共享,也不由普通类型承担
如果未来 Dujie 需要引入响应式系统,那么:
- 依赖收集
- 更新传播
- 显式共享状态
- 订阅关系
都应通过独立的响应式原语表达,而不是让 list、map、struct 默认共享底层可变状态。
术语
值语义
值语义指:
- 赋值、传参、返回时得到逻辑上独立的值
- 之后对其中一个值的修改,不影响另一个值
共享实现
共享实现指:
- 编译器或运行时内部可以复用底层存储
- 但用户不能通过普通语言操作观察到“隐藏别名”
可变绑定
var 表示该绑定可被更新。
这里的“更新”包括:
- 重新赋值
- 通过该绑定修改其当前值的可变部分,例如 list/map/struct
let 表示该绑定不可更新。
容器类型与结构体
本文中的“容器类型”专指:
list<T>map<K, V>opt<T>iter<T>
struct 不归入容器类型。它是名义类型,但在值语义讨论中同样属于“可包含其他值的聚合数据”。
按类型分类的语义规则
1. 基础类型
int、float、bool、rune、string 都按普通值处理。
其中:
string是不可变值rune表示单个 Unicode 码点- 基础类型之间不存在“共享修改”的语言语义
string 不再被视为用户层面的“引用类型”。
2. 容器类型与结构体
list<T>、map<K, V>、struct 在语言层都视为普通数据值。
它们可以有更新操作,但这些更新只作用于当前值本身,不通过赋值关系隐式传播到其他变量。
例子:list
var a = [1, 2, 3];
var b = a;
b[0] = 10;
// a == [1, 2, 3]
// b == [10, 2, 3]
例子:map
var a = {"x": 1};
var b = a;
b["x"] = 2;
// a == {"x": 1}
// b == {"x": 2}
例子:struct
var a = Pair { key: "x", value: [1, 2, 3] };
var b = a;
b.value[0] = 10;
// a.value == [1, 2, 3]
// b.value == [10, 2, 3]
3. opt<T>
opt<T> 是值包装类型。
它只表达“有值 / 无值”,不额外引入共享语义。
其值语义由 T 的普通值语义继承而来。
4. widget
widget 表示 UI 组件值。
它在语言层应视为不可变值,而不是可共享可变对象。
原因:
- UI 组件树更适合值式描述
- 这更利于后续做 diff、重建和响应式更新
- 这避免把“组件节点句柄”暴露成可变引用对象
5. iter<T>
iter<T> 不应再被归入“引用类型”讨论。
它更接近一种瞬时遍历对象,而不是持久数据容器。
当前只先约定:
iter<T>不承载普通共享修改语义- 它不是用来表达共享可变状态的类型
关于“是否可重复消费、是否单次遍历、赋值后如何观察其状态”等问题,后续另开文档处理。
6. any
any 不改变值语义与共享语义的基本判断。
它只擦除静态类型信息,不自动把普通值变成共享引用对象。
函数传参与返回值
普通函数参数和返回值也采用值语义。
例子:函数内部修改不应外溢
func append_one(xs: list<int>) -> list<int> {
var out = xs;
out.push(1);
return out;
}
var a = [1, 2];
var b = append_one(a);
// a == [1, 2]
// b == [1, 2, 1]
这意味着:
- 普通参数不是隐式引用参数
- 如果想表达“调用后外部状态被更新”,未来应使用显式机制,而不是依赖别名传播
对未来响应式系统的约束
本设计对未来响应式系统有一个明确要求:
普通数据值和响应式状态必须区分。
也就是说,未来响应式系统不应通过下面这种方式构建:
- 把 list/map/struct 默认做成共享可变引用
- 再依赖别名变化触发更新
更合理的方向是:
- 普通值保持值语义
- 响应式能力由专门的状态类型或响应式原语承担
这样可以避免“我只是传了一个值,为什么另一个地方的 UI 也被隐式改掉”这类问题。
允许的实现策略
本设计允许编译器或运行时采用多种内部策略实现值语义,包括:
- 小对象直接拷贝
- 聚合对象的结构共享
- 写时复制
- 引用计数包装
- 针对
widget的持久化树结构
但这些策略只能影响性能,不能改变语言层可见行为。
明确拒绝的设计
当前明确拒绝以下语言级设计:
1. 普通聚合类型默认共享可变状态
也就是拒绝把 list、map、struct 的普通赋值定义为“只复制引用”。
2. 用 Rust 后端表示直接反推语言语义
例如:
- 因为后端用了
Arc<T>,所以语言里就认为它是引用类型 - 因为后端用了
Vec<T>,所以语言里就必须暴露某种 Rust 风格移动规则
这些都不应成立。
3. 用普通值模型代替未来响应式模型
响应式系统需要显式设计,不应伪装成普通值赋值语义的一部分。
对现有文档的影响
00.guide/03.data-types
后续应重写其中“值类型 / 引用类型”的用户说明,不再把大量类型直接定义为共享引用。
01.design/06.struct-design
该文档目前更适合被视为“某种后端实现探索”,而不是结构体的最终语言语义定义。
widget 与未来响应式设计
后续讨论 widget 和响应式系统时,应默认以“组件值”而不是“组件引用对象”为前提。
未决问题
以下问题后续继续讨论: