# 泛型数据结构:分页结果保留元素类型
学习目标:把分页元数据与元素类型分开,让商品页和名称页共用容器而不丢失元素信息。
前置知识:记录或类、列表、可空值;这里只表达服务已经返回的结果,不实现网络请求。
# 问题与边界
Page<T> 包含 items 和 next。items 是有序序列,next 是不透明的非空游标或缺失;缺失表示已到末页,不能用 items 是否为空推断。空列表且仍有 next 也是合法状态,例如服务过滤后的页。本题不验证游标、不计算总数、不读取下一页,不声称 next 是 URL。演示一个整数页、一个字符串末页和一个空但可继续的页;调用方只在 next 存在时使用它,末页显式输出 end。
# 关键差异
| 语言机制 | 元素信息 | 容器与可变性 |
|---|---|---|
| Rust/Go/C++ 泛型结构 | 编译时保留 T | 所有权/切片共享/值语义不同 |
| Java/Kotlin 泛型类 | 静态 T,JVM 上通常擦除 | 只读引用不等于元素深度不可变 |
| TS 泛型类型别名 | 静态 T,运行时擦除 | readonly 仅限制类型化访问 |
| Python Generic | 静态工具可见 T | dataclass 不自动验证字段 |
| JS 普通对象 | 运行时值保留 | 没有同等静态泛型契约 |
# 各语言实现
# Rust
成功时预期输出为 "end\n"(\n 表示换行)。
Ruststruct Page<T> {
items: Vec<T>,
next: Option<String>,
}
fn main() {
let numbers = Page { items: vec![7], next: Some("cursor-2".into()) };
assert_eq!(numbers.items[0] + 1, 8);
let empty = Page::<i32> { items: vec![], next: Some("cursor-3".into()) };
assert!(empty.items.is_empty() && empty.next.is_some());
let last = Page { items: vec![String::from("book")], next: None };
assert_eq!(last.items[0].len(), 4);
match last.next { Some(cursor) => println!("next: {cursor}"), None => println!("end") }
}
Page 持有 Vec;泛型并不强制深复制。将 numbers.items 整体移出会改变后续可访问状态。Rust 泛型结构体。
# Go
成功时预期输出为 "end\n"(\n 表示换行)。
Gopackage main
import "fmt"
type Page[T any] struct { Items []T; Next *string }
func main() {
cursor := "cursor-2"
numbers := Page[int]{Items: []int{7}, Next: &cursor}
empty := Page[int]{Next: &cursor}
last := Page[string]{Items: []string{"book"}}
if numbers.Items[0]+1 != 8 || len(empty.Items) != 0 || empty.Next == nil || len(last.Items[0]) != 4 { panic("page") }
if last.Next == nil { fmt.Println("end") } else { fmt.Println("next:", *last.Next) }
}
any 允许存储任意元素,不提供比较能力。复制 Page 只复制切片描述符与指针,底层元素仍可共享;本例不修改它们。Go 类型定义。
# Java
成功时预期输出为 "end\n"(\n 表示换行)。
Javaimport java.util.List;
import java.util.Optional;
public class Main {
record Page<T>(List<T> items, Optional<String> next) {}
public static void main(String[] args) {
Page<Integer> numbers = new Page<>(List.of(7), Optional.of("cursor-2"));
Page<Integer> empty = new Page<>(List.of(), Optional.of("cursor-3"));
Page<String> last = new Page<>(List.of("book"), Optional.empty());
if (numbers.items().get(0)+1 != 8 || !empty.items().isEmpty() || empty.next().isEmpty() || last.items().get(0).length() != 4) throw new AssertionError();
if (last.next().isPresent()) System.out.println("next: " + last.next().get());
else System.out.println("end");
}
}
record 自动生成访问器,不会替你冻结传入的可变列表。此驱动用 List.of,但字段 T 仍可能是可变对象。类型擦除意味着不能靠运行时 instanceof Page<String> 检查元素类型。Java 类型擦除。
# JavaScript
成功时预期输出为 "end\n"(\n 表示换行)。
JavaScriptfunction page(items, next) { return { items, next }; }
const numbers = page([7], "cursor-2");
const empty = page([], "cursor-3");
const last = page(["book"], null);
if (numbers.items[0]+1 !== 8 || empty.items.length !== 0 || empty.next === null || last.items[0].length !== 4) throw new Error("page");
if (last.next === null) console.log("end"); else console.log("next: " + last.next);
对象字面量没有复制 items。将结果叫作 Page 不能阻止调用方混放元素,边界校验应放在 JSON 等外部数据入口,而非假装对象名称有检查能力。ECMAScript 对象初始化。
# TypeScript
TypeScripttype Page<T> = { readonly items: readonly T[]; readonly next: string | null };
function page<T>(items: readonly T[], next: string | null): Page<T> { return { items, next }; }
const numbers = page([7], "cursor-2");
const empty = page([], "cursor-3");
const last = page(["book"], null);
if (numbers.items[0]+1 !== 8 || empty.items.length !== 0 || empty.next === null || last.items[0].length !== 4) throw new Error("page");
if (last.next === null) console.log("end"); else console.log("next: " + last.next);
Page<number> 与 Page<string> 的 items 保留不同类型。readonly 不是 Object.freeze,也不是深不可变;禁止的仅是这条类型化路径上的赋值。TypeScript 只读属性。
# Kotlin
成功时预期输出为 "end\n"(\n 表示换行)。
Kotlindata class Page<T>(val items: List<T>, val next: String?)
fun main() {
val numbers = Page(listOf(7), "cursor-2")
val empty = Page(emptyList<Int>(), "cursor-3")
val last = Page(listOf("book"), null)
check(numbers.items[0]+1 == 8)
check(empty.items.isEmpty() && empty.next != null)
check(last.items[0].length == 4)
val cursor = last.next
if (cursor == null) println("end") else println("next: $cursor")
}
List 提供只读接口,但其他引用仍可能修改底层集合。此处保持 Page 不变;没有为了“泛型”强行添加继承或工厂。Kotlin 集合概览。
# Python
成功时预期输出为 "end\n"(\n 表示换行)。
Pythonfrom dataclasses import dataclass
from typing import Generic, TypeVar
T = TypeVar("T")
@dataclass(frozen=True)
class Page(Generic[T]):
items: tuple[T, ...]
next: str | None
numbers = Page((7,), "cursor-2")
empty: Page[int] = Page((), "cursor-3")
last = Page(("book",), None)
assert numbers.items[0]+1 == 8
assert not empty.items and empty.next is not None
assert len(last.items[0]) == 4
if last.next is None:
print("end")
else:
print("next: " + last.next)
本例用 tuple 避免列表内容修改,frozen 防止常规字段重绑定;都不能让任意 T 自动深不可变。dataclass 装饰器也不会执行泛型类型验证。dataclasses。
# C++
成功时预期输出为 "end\n"(\n 表示换行)。
C++#include <cassert>
#include <iostream>
#include <optional>
#include <string>
#include <vector>
template<class T> struct Page {
std::vector<T> items;
std::optional<std::string> next;
};
int main() {
Page<int> numbers{{7}, std::string("cursor-2")};
Page<int> empty{{}, std::string("cursor-3")};
Page<std::string> last{{"book"}, std::nullopt};
assert(numbers.items[0]+1 == 8);
assert(empty.items.empty() && empty.next);
assert(last.items[0].size() == 4);
if (last.next) std::cout << "next: " << *last.next << '\n';
else std::cout << "end\n";
}
vector 与 string 是值成员;复制此 Page 会复制容器内容,和 Go 切片、JS 数组别名不同。函数只读页面时应按 const 引用传入。本例没有为复用类型而引入虚函数。C++ 工作草案:类模板持续更新;代码只使用 C++17 能力。
# 迁移误区与适用限制
- 将“没有元素”当成末页 → 本文 empty 明确仍有 next;应该读服务约定的结束标记。
- 把
Page<T>直接反序列化为可信 T → TS/Python 注解与 JVM 擦除不验证 JSON;必须在外部输入边界建立可信字段。 - 将只读接口当成快照 → 原列表的别名仍可修改内容;需要快照时由 API 明确复制责任,而非随意加入隐式复制。
- 盲目给 Page 加协变声明 → 若以后提供写入 T 的方法就可能破坏安全性。先根据读写用途决定 variance;本例不依赖跨类型 Page 赋值。
# 运行与验证
每种语言在独立目录按上节文件名和命令执行。Rust/C++ 的可执行程序在 PowerShell 中用 .\main.exe,POSIX shell 中用 ./main。基线见主题首页,实际执行证据集中记入验证记录。本文给出预期语义与检查,不把编写或阅读核对宣称为运行通过;各程序应由验证流程执行,类型错误反例的文字说明不等于已运行负向编译测试。