AI Library · 知识库

# 泛型数据结构:分页结果保留元素类型

学习目标:把分页元数据与元素类型分开,让商品页和名称页共用容器而不丢失元素信息。

前置知识:记录或类、列表、可空值;这里只表达服务已经返回的结果,不实现网络请求。

# 问题与边界

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 能力。

# 迁移误区与适用限制

# 运行与验证

每种语言在独立目录按上节文件名和命令执行。Rust/C++ 的可执行程序在 PowerShell 中用 .\main.exe,POSIX shell 中用 ./main。基线见主题首页,实际执行证据集中记入验证记录。本文给出预期语义与检查,不把编写或阅读核对宣称为运行通过;各程序应由验证流程执行,类型错误反例的文字说明不等于已运行负向编译测试。

# 相关阅读