AI Library · 知识库

# 定义数据类型:给用户、配置和坐标命名

学习目标:用字段把相关数据放在一起,区分数据形状、值比较与对象身份。

前置知识与范围:变量、整数、布尔值和字段访问。本篇只定义内部已知的数据,不解析 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,否则会移除相应断言。异常未被预期分支消费、断言失败或编译失败均不是成功。

# 相关阅读

创建有效对象 · 方法与封装 · 有限状态建模:带数据的代数状态