# 定义数据类型:给用户、配置和坐标命名
学习目标:用字段把相关数据放在一起,区分数据形状、值比较与对象身份。
前置知识与范围:变量、整数、布尔值和字段访问。本篇只定义内部已知的数据,不解析 JSON,不建立数据库实体。
# 问题与边界
用一个用户快照保存名字 Ada、配置中的启用标记和二维坐标 (0, -2)。读取字段必须得到原值,再创建一个名字相同、坐标为 (3, -2) 的快照,原快照仍保持 (0, -2)。坐标取本例可精确表示的小整数,名字是非空内部字符串;这里没有额外业务校验,也没有失败返回值。负坐标合法,不应因“计数通常非负”而拒绝。
类、结构体、记录和普通对象都能描述这些数据。目标是把字段关系写清楚,不是让所有语言都变成同一种对象模型;非法外部输入的处理见创建有效对象。
# 关键差异
| 语言机制 | 本例选择 | 不能据此推出 |
|---|---|---|
| Rust / Go / C++ 结构体 | 具名字段,嵌套坐标 | 所有赋值都深复制引用字段 |
| Java record / Kotlin data class | 自动生成常见数据操作 | 引用的集合深度不可变 |
| JS 普通对象 / TS 对象类型 | 结构化数据,无须构造类 | TS 类型会校验运行时 JSON |
| Python dataclass | 生成初始化和字段比较 | 字段注解会拒绝错误类型 |
八语言采用同一小数据集,但不要求相等运算符含义相同:代码直接检查字段。复制或重建只涉及标量与不可变名字,不扩展到任意对象图。
# 各语言实现
各程序独立保存到各自目录,不互相拼接;默认只依赖标准库。正常结束、无标准输出表示程序内检查均未报告失败。这是预期行为,不是已经运行通过的声明。
# Rust
Rust#[derive(Clone, Copy)]
struct Point { x: i32, y: i32 }
#[derive(Clone, Copy)]
struct Config { enabled: bool }
struct User { name: &'static str, config: Config, position: Point }
fn main() {
let first = User {
name: "Ada", config: Config { enabled: true },
position: Point { x: 0, y: -2 },
};
let moved = User {
name: first.name, config: first.config,
position: Point { x: 3, ..first.position },
};
assert_eq!(first.position.x, 0);
assert_eq!((moved.position.x, moved.position.y), (3, -2));
assert_eq!(moved.name, "Ada");
assert!(moved.config.enabled);
}
本例坐标与配置只含可复制标量,因此显式派生 Copy;用户名字借用静态字符串,不为固定常量额外分配。含 String 的结构体不能照搬 Copy,更新语法还可能移动未显式提供的字段。参见 结构体与更新语法。
# Go
Gopackage main
type Point struct { X, Y int }
type Config struct { Enabled bool }
type User struct { Name string; Config Config; Position Point }
func main() {
first := User{"Ada", Config{true}, Point{0, -2}}
moved := first
moved.Position.X = 3
if first.Position.X != 0 || moved.Position.X != 3 ||
moved.Position.Y != -2 || moved.Name != "Ada" || !moved.Config.Enabled {
panic("data contract")
}
}
结构体赋值复制字段,嵌套的 Point 也是值;若改成指针、切片或 map,复制外层不会复制它们指向的数据。大写字段可被其他包访问,和构造校验是不同问题。参见 Go 结构体。
# Java
Javapublic class Main {
record Point(int x, int y) {}
record Config(boolean enabled) {}
record User(String name, Config config, Point position) {}
public static void main(String[] args) {
var first = new User("Ada", new Config(true), new Point(0, -2));
var moved = new User(first.name(), first.config(), new Point(3, first.position().y()));
if (first.position().x() != 0 || moved.position().x() != 3 ||
moved.position().y() != -2 || !moved.name().equals("Ada") ||
!moved.config().enabled()) throw new AssertionError("data contract");
}
}
record 的组件通过访问器读取,自动生成基于组件的 equals;它仍然是引用类型,== 不是字段比较。这里组件也是不可变记录,不代表包含可变列表的 record 自动深度冻结。参见 JLS record classes。
# JavaScript
JavaScriptconst first = {
name: "Ada", config: { enabled: true }, position: { x: 0, y: -2 },
};
const moved = { ...first, position: { ...first.position, x: 3 } };
if (first.position.x !== 0 || moved.position.x !== 3 ||
moved.position.y !== -2 || moved.name !== "Ada" || !moved.config.enabled) {
throw new Error("data contract");
}
对象展开是浅复制,因此修改坐标前还要重建 position;只写 { ...first } 再改内层坐标会改到原对象。const 只限制绑定重赋值。参见 ECMAScript 对象初始化器。
# TypeScript
TypeScripttype Point = { x: number; y: number };
type Config = { enabled: boolean };
type User = { name: string; config: Config; position: Point };
const first: User = {
name: "Ada", config: { enabled: true }, position: { x: 0, y: -2 },
};
const moved = { ...first, position: { ...first.position, x: 3 } };
if (first.position.x !== 0 || moved.position.x !== 3 ||
moved.position.y !== -2 || moved.name !== "Ada" || !moved.config.enabled) {
throw new Error("data contract");
}
结构类型为字段提供编译期约束,不引入运行时 User 构造函数。额外属性、断言或外部 JSON 的规则不能简化为“注解后一定合法”。参见 对象类型。
# Kotlin
Kotlindata class Point(val x: Int, val y: Int)
data class Config(val enabled: Boolean)
data class User(val name: String, val config: Config, val position: Point)
fun main() {
val first = User("Ada", Config(true), Point(0, -2))
val moved = first.copy(position = first.position.copy(x = 3))
check(first.position.x == 0)
check(moved.position == Point(3, -2))
check(moved.name == "Ada" && moved.config.enabled)
}
data class 生成 copy 和相等性操作;copy 仍是浅复制,所以这里显式复制嵌套坐标。val 防止重赋值,不承诺引用对象不可变。参见 Kotlin 数据类。
# Python
Pythonfrom dataclasses import dataclass, replace
@dataclass(frozen=True)
class Point:
x: int
y: int
@dataclass(frozen=True)
class Config:
enabled: bool
@dataclass(frozen=True)
class User:
name: str
config: Config
position: Point
first = User("Ada", Config(True), Point(0, -2))
moved = replace(first, position=replace(first.position, x=3))
assert first.position.x == 0
assert moved.position == Point(3, -2)
assert moved.name == "Ada" and moved.config.enabled
frozen=True 阻止通常的字段赋值;注解不做运行时类型校验。若字段含列表,冻结外层仍不禁止列表修改。replace 通过构造新实例实现更新。参见 dataclasses。
# C++
C++#include <cassert>
#include <string_view>
struct Point { int x; int y; };
struct Config { bool enabled; };
struct User { std::string_view name; Config config; Point position; };
int main() {
const User first{"Ada", {true}, {0, -2}};
User moved = first;
moved.position.x = 3;
assert(first.position.x == 0);
assert(moved.position.x == 3 && moved.position.y == -2);
assert(moved.name == "Ada" && moved.config.enabled);
}
struct 默认成员公开,聚合初始化不等于验证。string_view 在此引用静态字符串字面量,生命周期足够;不要把临时 std::string 的视图存入长寿命用户。参见 C++ 工作草案:类与 string_view,在线草案持续更新,本例只使用 C++17 已有机制。
# 迁移误区与适用限制
把“结构体复制”和“对象复制”当成深复制 → 若用户持有可变集合,修改副本可能影响原值 → 先列出所有权与共享关系,再决定重建哪一层。
把生成的相等性当作业务身份 → 两个同名用户不一定同一账号 → 真有身份需求时引入明确 ID,而不是给本例无故增加字段。
普通数据载体适合快照;需要始终保持范围约束时,不应开放任意字段写入,应转向受控构造与方法。
# 运行与验证
版本条件统一见示例基线,实际执行情况以验证记录为准。本篇编写阶段未执行示例,不能将静态阅读当成运行结果。每种语言使用独立目录,避免同名入口或编译输出互相覆盖;不需要网络、文件数据或第三方依赖。
程序内检查覆盖:零和负坐标的保存、重建后的新坐标、原对象不变、嵌套配置与名字保持。这里没有运行时输入解析失败用例,因为接口前置条件不包含不可信输入。。Java 使用显式 AssertionError 而非默认关闭的 assert;Python 不要以 -O 运行,C++ 不要定义 NDEBUG,否则会移除相应断言。异常未被预期分支消费、断言失败或编译失败均不是成功。