以 Rust 为例,聊聊线性类型,以及整个类型系统

本文作者 Rust 小白(别的语言也了解的不多),如有错误务必指正,本文目的在于以小白的视角讲述未来 Rust 类型系统的一些演变方向。类型系统的概念往往是跨语言的,通过了解不同语言的特点,能够帮助我们更好掌握当下在用的工具。


1. 对象应当如何被使用?------ Substructural Type System

首先 Substructural Type System 这个词可能可以被翻译为"子结构类型系统"或"亚结构类型系统",但不管是什么,其名字听起来都很玄学,事实上它的核心是:通过限制对象的使用方式,来增强代码的安全性,并能更好的管理内存。

提到"亚结构"肯定有人要问:那"结构类型系统"(Structural Type System)是什么呢?,事实上,它们几乎没有关系,与"结构类型系统"对应的是"标明类型系统"(Nominal Type System):

  • 标明类型系统:名字叫啥就是啥类型,比较的是类型本身。
  • 结构类型系统:即 Duck Typing,只要类型成员均一致,那么就是相同类型,本质上类型检查只检查 Shape,即类型定义的约束条件。

但 Substructural Typing System 其实是完全不同的一个概念,其关注的是"对象应该如何被使用"。(而非具体是什么结构)下面给出了其具体分类(从宽松到严格):

亚结构类型名 使用规则 内存语义 代表语言/特性
Normal(普通) 随便用几次都行 值可以随意复制、随意丢弃 js、java、py 等多数主流语言
Affine(仿射) 最多用一次 值只能移动(所有权转移),可以不用直接 drop 当前 Rust
Linear(线性) 恰好用一次 值必须被显式消费,不能 drop 也不能 leak Rust 未来的 !Forget
Ordered(有序) 恰好一次,且在原地 值必须被消费,且内存地址从创建到销毁不变 Rust 未来的 !Move

Substructural Type System 通过控制几条"结构规则"来定义不同强度的类型约束:

  • 弱化规则(Weakening):允许引入一个变量但不使用它。即:值可以被随意丢弃。
  • 收缩规则(Contraction):允许同一个变量被使用多次。在 Rust 中,只有可 Copy 的才可以这样。
  • 有序规则(Ordered):不仅恰好使用一次,还必须按照引入顺序使用。在实践中,这意味着值有稳定的内存地址,永远不会被移动。

通过组合"有/无弱化"、"有/无收缩"、"有/无序",我们也可以上述的几种类型系统。

JavaScript、Java 等这些以引用和 GC 为核心的语言,通常都是 Normal 类型:let y = x 只是复制引用,x 仍然可用,GC 负责回收,程序员完全不用操心值的生命周期。

Rust 选择了 Affine 类型系统,这是 Rust 内存安全的根基。这里我也提到了一些 Rust 未来可能的语言设计(其实是我菜,没用过其它有这些特性的语言),后文你将会看到它们如何发挥作用。

2. 为什么 Rust 需要线性类型?

2.1 Rust 的仿射类型(Affine)

Rust 目前是一个仿射类型系统(Affine):

  • 有弱化规则 :Rust 的 RAII 机制(Drop trait)会在值离开作用域时自动析构它。你也可以用 mem::forget 直接泄露它。
  • 受限的收缩规则 :只有实现了 Copy 的类型才能隐式复制,其他类型需要显式 clone()。不实现 Copy 的类型遵循移动语义。

Rust 中非 Copy 类型的值遵循移动语义,每次赋值或传参都是所有权转移,原变量立即失效:

rust 复制代码
let x = String::from("hello");
let y = x;        // x 的所有权转移给了 y
// println!("{x}"); // 编译错误!x 已经被"用掉"了
println!("{y}");   // OK

// 复制必须显式 clone
let a = String::from("hello");
let b = a.clone(); // a 仍然可用
println!("{a} {b}"); // OK

这套机制让编译器能在编译时保证:没有 use-after-free,没有 double-free,没有数据竞争

但仿射类型有一个漏洞:值可以不用就直接丢弃mem::forget / 隐式 drop),这在某些场景下会出问题。这正是线性类型要解决的,编译器要强制你必须消费这个值。

2.2 为什么"可以丢掉"是个问题?

在 Rust 中,mem::forget 是一个安全函数 (不需要 unsafe),它可以让任意值"凭空消失"而不运行析构函数:

rust 复制代码
use std::mem;

let important_handle = acquire_critical_resource();
mem::forget(important_handle); // 析构函数不会运行,资源可能泄露

对于大多数类型来说,这只是内存泄露,这不算什么大事。但对于某些类型(比如持有系统资源的 handle、需要 join 的线程 guard),析构函数不运行可能导致未定义行为

如果我们能让某些类型变成线性类型,编译器就会在编译时保证:这个值必须被消费,不能悄悄丢掉

不过,在开始 Rust 的线性类型之前,我其实很疑惑,为什么 mem::forget 的安全的?为什么 Rust 允许析构函数不被调用?

2.3 mem::forget 为什么是安全的?

就如同上面的例子,这个行为看起来很危险,为什么它不是 unsafe 的?

设计原因 :事实上,Rust 的核心立场是 内存泄露不是未定义行为(UB) 。内存泄露固然不好,但它不会导致悬垂指针、数据竞争、读取未初始化内存等真正的安全问题。因此 mem::forget 本身是"安全"的。

实际用途mem::forget 最常见的用法是在 unsafe 代码中转移资源所有权 。例如 Vec::into_raw_parts 在把 buffer 指针交给调用者后,需要 forget 掉原来的 Vec,否则 Vec 的析构函数会释放那块已经交出去的内存:

rust 复制代码
let v = vec![1, 2, 3];
let (ptr, len, cap) = v.into_raw_parts();
// 内部: mem::forget(v),防止 v 的析构函数释放 ptr 指向的内存
// 现在调用者负责管理 ptr

在 safe Rust 中直接调用 mem::forget 比较少见。但问题是,即使你不直接调用它,还有其他方式可以达到同样效果 ,比如通过 Rc/Arc 配合 RefCell 实现的内部可变性、丢弃线程 JoinHandle 导致 move 的 val 永不销毁等。《Rust 程序设计语言》官方文档也有提及这一点:rustwiki.org/zh-CN/book/...

因此,正是因为存在很多"safe 泄露"的途径,Rust 团队最初做出的决定就是:接受所有类型都可能被泄露,不在类型系统中区分"可泄露"和"不可泄露"的类型 。这个决定让当时的 Rust 语言本身保持了简洁性,但也关上了一扇门:任何依赖析构函数一定运行才能保证安全性的 API,在当前 Rust 中,不一定是完全 safe 的

2.4 Drop 的五个结构性缺陷

即便析构函数能保证运行,Drop trait 自身的设计也有严重局限:

rust 复制代码
pub trait Drop {
    fn drop(&mut self);  // 这就是它的签名,非常受限
}

缺陷一:不能返回值drop 返回 (),无法返回 Result 报告清理过程中的错误。如果文件关闭失败、数据库回滚失败,析构函数只能默默 panic 或吞掉错误:

rust 复制代码
impl Drop for DatabaseConnection {
    fn drop(&mut self) {
        // 如果断开连接失败怎么办?
        // 不能返回 Err(...),只能 panic 或 log
        if let Err(e) = self.disconnect() {
            eprintln!("disconnect failed: {e}"); // 只能这样
        }
    }
}

缺陷二:不能接受参数 。有些资源的清理需要额外信息,比如事务需要一个连接对象来 commit/rollback,但 drop(&mut self) 没有地方传入这些参数:

rust 复制代码
// 理想中我们想写这样的代码,但 Drop 不支持
impl Drop for Transaction {
    fn drop(&mut self, conn: &Connection) {  // 编译错误:签名不对
        self.rollback(conn);
    }
}

缺陷三:只有一种 Drop。一个类型只能有一个析构函数,不能有"commit-析构"和"rollback-析构"两种变体。

缺陷四:不能是 async 的 。在 async 上下文中,清理操作可能需要 .await(比如发送一条断开连接的消息),但 drop 是同步的。目前没有 async Drop

缺陷五:drop 只接受 &mut self,不接受 self 。因为 drop 拿的是可变引用而非所有权,它不能把自己的字段"移出来",只能借用访问。这限制了类型本身析构逻辑的表达能力。

还有很多,正是因为上面这些原因,导致了诸如 DMA 访问、事务、网络、数据库等一系列操作选择了使用自定义 drop 函数 + mem::forget 的逻辑。甚至还有一种典型的 workaround:"析构炸弹"模式:

rust 复制代码
impl Drop for CriticalResource {
    fn drop(&mut self) {
        // 如果正常消费了这个值,应该已经调用过 mem::forget
        // 走到这里说明值被异常丢弃了
        std::process::abort(); // "析构炸弹":直接崩溃
    }
}

impl CriticalResource {
    fn consume(self) {
        // 资源应该走自定义逻辑,正常销毁:做真正的清理工作
        do_cleanup(&self);
        mem::forget(self); // 阻止析构炸弹触发
    }
}

但这是一种动态检查手段,运行时才能发现问题,而 Rust 用户更想要编译时静态保证

2.5 线性类型如何解决这些问题

关键在于思维转变:不再依赖 Drop trait 做清理,而是让"清理"变成一个普通方法调用,但编译器强制你必须调用它。(当然,对于无泄露风险的常规对象,也可以直接让编译器添加 drop)

我们假想一个只实现了 Move(不实现 DestructForget)的类型:

  • 只能被移动(传递所有权)
  • 不能被 mem::forget(没有实现 Forget
  • 不能被隐式 drop(没有实现 Destruct,编译器不会在作用域结束时自动析构它)

这意味着你拿到这个值之后,唯一能做的就是把它传给某个接受 self 的方法,而这个方法就是你定义的"消费函数"。既然消费函数是普通方法,Drop 的所有限制都不存在了:

  1. 解决"不能返回值":消费函数是普通函数,返回类型任意。

    rust 复制代码
    struct FileHandle { fd: RawFd }
    impl Move for FileHandle {}
    
    impl FileHandle {
        pub fn close(self) -> io::Result<()> {
            let FileHandle { fd } = self;
            match unsafe { libc::close(fd) } {
                0 => Ok(()),
                // 关闭失败?返回 Err
                _ => Err(io::Error::last_os_error()),
            }
        }
    }
    
    fn work() -> io::Result<()> {
        let f = FileHandle::open("data.txt")?;
        f.write_all(b"hello")?;
        f.close()?;  // 编译器强制调用;close 失败会被 ? 传播
        Ok(())
        // 如果忘了 close,编译错误,不是运行时泄露
    }
  2. 解决"不能接受参数":消费函数签名完全自由。

    rust 复制代码
    struct Transaction { data: Vec<u8> }
    impl Move for Transaction {}
    
    impl Transaction {
        pub fn commit(self, conn: &Connection) -> Result<(), Error> {
            let Transaction { data } = self;
            conn.send(&data)
        }
        pub fn rollback(self, reason: &str) {
            let Transaction { data } = self;
            log::warn!("rollback: {reason}");
            // data 被 drop(Vec<u8> 实现了 Destruct)
        }
    }

    当前的 Rust 中,你只能把 Connection 存在 Transaction 的字段里以便 drop 时能访问,或者用全局/thread-local 变量绕过,都很丑陋。

  3. 解决"只有一种 Drop" :你可以定义任意多个消费方法,编译器只要求你调用其中一个

    rust 复制代码
    fn process(conn: &Connection) -> Result<(), Error> {
        let tx = Transaction::begin(conn)?;
        if validate(&tx)? {
            tx.commit(conn)?;     // 路径 A:提交
        } else {
            tx.rollback("invalid"); // 路径 B:回滚
        }
        // 两条路径都消费了 tx,编译通过
        // 如果某个分支遗漏了,编译错误
        Ok(())
    }

    理论上,编译器也应当去做控制流分析,保证在每个控制流分支中,tx 都必须被恰好消费一次。

  4. 解决"不能是 async" :消费函数可以是 async fn,这意味着这个类型只能在 async 上下文中被析构(被 .await 消费),如果你试图在 sync 代码中 drop 它,编译器会报错。

    rust 复制代码
    struct AsyncConnection { stream: TcpStream }
    impl Move for AsyncConnection {}
    
    impl AsyncConnection {
        pub async fn close(self) -> io::Result<()> {
            let AsyncConnection { mut stream } = self;
            stream.write_all(b"QUIT\r\n").await?;  // 发送断开消息
            stream.shutdown().await?;              // 等待关闭完成
            Ok(())
        }
    }

    今天 async Drop 之所以设计困难,根本原因是 Drop::drop 的签名是 fn drop(&mut self) 同步的、无返回值的、固定签名。但如果清理逻辑不再走 Drop,而是走普通的 async fn close(self),"async drop"问题就消失了,它从一个语言特性问题变成了一个普通的 API 设计问题。

  5. 解决"drop 只接受 &mut self" :消费函数接受 self(所有权),可以解构、移出字段。

    rust 复制代码
    impl Transaction {
        pub fn into_parts(self) -> (Vec<u8>, Metadata) {
            let Transaction { data, meta } = self;  // 解构,拿到每个字段的所有权
            (data, meta)  // 自由地把字段移出来返回
        }
    }

    今天的 Drop::drop(&mut self) 里,你不能写 let Transaction { data, .. } = self;,因为你只有 &mut self,不能移出字段(除非用 mem::take?)

因此,线性类型的核心思路是"把析构从隐式的编译器行为,变成显式的用户方法调用"。Drop 之所以有这么多限制,是因为它的调用时机和方式由编译器控制,编译器需要在任何可能的退出路径上插入 drop 调用,所以签名必须固定。而线性类型把这个控制权还给了用户:你来决定什么时候、怎么消费这个值,编译器只负责检查你确实消费了

2.6 历史的遗憾

2015 年,Rust 1.0 发布前几个月,thread::scoped API 被发现不安全 (Related Issue),用户可以通过 mem::forget 泄露 JoinGuard,跳过 join,导致 use-after-free。Rust 团队在时间压力下决定:不添加 Leak trait,接受所有类型都可以被泄露的现实,社区中被称为 "Leakpocalypse" 大事件,直接决定了 Rust 的设计哲学。这存在几个历史原因:

  1. 时间紧迫:当时距离 1.0 发布仅剩几个月,并且部分因素是因为 Mozilla 管理层给 Rust 团队施加了巨大的 1.0 发布压力。引入 Leak trait 需要对整个标准库和编译器类型系统进行大规模重构。
  2. 复杂性爆炸: 如果引入 Leak,几乎所有的泛型代码都需要考虑 T: Leak 约束。这会极大增加语言的学习成本和 API 的复杂性。
  3. 无法覆盖所有路径: 即使没有 mem::forget,通过 Rc 循环引用、故意制造死锁,依然可以导致析构函数不运行。

Rust 的一些设计者后来坦言:如果能回到 2015 年,他会同时添加 MoveLeak,而现在想补上这个遗憾极其困难,核心原因是向后兼容性,有这么几种可能的方案:

  1. Leak 作为 auto trait (像 Send):所有类型默认实现 Leak,不想被泄露的类型用 impl !Leak。其问题在于:给 mem::forget 加上 T: Leak 约束是 breaking change,因为今天所有写 mem::forget(x) 的泛型函数都没有 Leak bound。
  2. Leak 作为 ?Trait (像 Sized):默认所有泛型参数隐含 Leak,想支持线性类型的函数写 ?Leak。但问题可能更大:标准库中所有 stable 的关联类型都无法加 ?Leak bound,也是 breaking change。

社区目前也在探索更好的方案,比如通过 opt-in 的方式逐渐向后兼容,在不破坏现有代码的前提下逐步引入新约束。

3. Pin

3.1 Pin 解决了什么问题

当 Rust 引入 async/await 时,遇到了一个核心问题:async 函数的状态机可能是自引用的(包含指向自身数据的引用)。自引用结构不能被移动,否则内部指针会失效。

rust 复制代码
async fn self_reference() {
    let mut array = [1, 2, 3];
    let ptr = &array[0]; // ptr 引用了同函数内的 array
    some_async_op().await; // 挂起,状态机必须保存 array 和 ptr
    // 倘若 array 已经被移动掉了,那么 ptr 指向的地址就会失效
    println!("{}", *ptr);
}

// 状态机内部实现
struct AsyncStateMachine {
    array: [i32; 3],
    // 指针指向上方 array 字段
    ptr: *const i32,
}

由于没有 Move trait,Rust 用 Pin<&mut T> 来表达"这个值已经被固定,不能再移动了":

rust 复制代码
pub trait Future {
    type Output;
    // Pin<&mut Self> 表示 self 被"钉住"了
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

虽然 Pin 能用,但它带来了显著的复杂度:

  1. 概念复杂!Unpin(双重否定)表示"不能 unpin"即"被固定的类型"。很难理解 Pin<&mut T> 只在 T: !Unpin 时才有实际约束这种逻辑。

  2. Pin Projection 需要宏 。要安全地从 Pin<&mut MyStruct> 访问一个字段的 Pin<&mut Field>,需要使用 pin-project 等宏。

  3. 在 trait 签名中传染 。因为 impl Trait 语法无法表达"当 T 被 Pin 时实现某个 trait"的约束(无法在 bound 的左侧使用 Pin),Pin 必须直接出现在方法签名中:

    rust 复制代码
    // 因为 -> impl Future 无法表达 "Pin<&mut T>: Future" 的约束
    // 所以 Future::poll 必须把 Pin 写进签名
    trait Future {
        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
    }
  4. trait 大量重复 如果想让 Iterator 支持自引用迭代器,就需要一个全新的 PinnedIterator trait。对于 std::io 模块,可能需要:Read / PinnedReadWrite / PinnedWriteSeek / PinnedSeek

3.2 Pin vs Move

Pin 把不可移动性视为 val 的属性,即一个值在被 Pin 之前可以移动,之后不行。而 Move 把不可移动性视为类型本身的属性,某些类型天生就不能移动。

rust 复制代码
// Pin:不可移动性是"val"的属性
fn foo<T: Unpin>(t: &mut T);          // 没有 Pin,可以移动
fn foo<T: !Unpin>(t: &mut T);         // 没有 Pin,仍然可以移动!
fn foo<T: !Unpin>(t: Pin<&mut T>);    // 有 Pin,才不能移动

// Move:不可移动性是"类型"的属性
fn foo<T: Move>(t: T);                // T 可以移动
fn foo<T: !Move>(t: &mut T);          // T 不能移动,但可以借用

Move 作为 auto-trait,可以像 Send/Sync 一样与任何其他 trait 自由组合,不需要重复定义 trait,Rust 社区也倾向于用 Move 替代 Pin

4. 构建线性类型系统

4.1 多层级类型系统定义:Forget / Destruct / Move / Pointee

社区提出了一个较为完整的 trait 层次结构,称为"Controlled Destruction":

rust 复制代码
trait Pointee {}          // 可以被指针引用
trait Move: Pointee {}    // 可以被"移动"(重定位)
trait Destruct: Move {}   // 可以被"析构"(drop)
trait Forget: Destruct {} // 可以被"遗忘"(泄露)

默认情况下,泛型参数拥有 Forget 约束(即所有当前行为不变)。通过降低约束级别可以获得更强的类型保证:

rust 复制代码
// 默认,等同于今天的 Rust
fn anything<T: Forget>(a: T, b: T, c: T) {
    std::mem::drop(a);   // OK:可以析构
    std::mem::forget(b); // OK:可以遗忘
    let x = c;           // OK:可以移动
}

// 只有 Destruct,不能 forget
fn must_cleanup<T: Destruct>(a: T, b: T, c: T) {
    std::mem::drop(a);   // OK
    std::mem::forget(b); // 错误!T: Forget 未满足
    let x = c;           // OK
}

// 只有 Move,不能 drop 也不能 forget
fn must_consume<T: Move>(a: T) -> T {
    // 不能 drop,不能 forget,只能移动
    a // 必须返回它
}
  • Pointee (可被指针引用):!Move 的极端形式,值从出生到死亡都在同一个内存位置,甚至不能放在栈上(因为栈会 pop)。适用于建模 video RAM 或 MMIO 寄存器等
  • Move (可被移动):值可以从一个内存位置移动到另一个。!Move 的类型有稳定的内存地址,永远不会被移动。
  • Destruct (析构函数保证运行):值不能被 mem::forget,但可以被正常 drop。这是线性类型的核心。
  • Forget (当前默认行为):值可以被 mem::forget 安全跳过析构,这是今天 Rust 中所有类型都隐式拥有的。

4.2 控制流的挑战

线性类型的"恰好使用一次"在有控制流(if/while/panic)时变得复杂。目前有三种强度的检查思路:

  • 最弱规则:变量在作用域内出现一次即可(不管控制流),但不安全,因为某个分支可能没有消费值。
  • 中间规则 :变量在每个控制流分支中都恰好使用一次。
  • 最强规则 :在程序的所有可能执行路径中都恰好使用一次。
rust 复制代码
// 最弱规则通过,但实际上不安全
fn bad(linear: Linear) -> Option<Linear> {
    if undecidable() {
        Some(linear) // linear 被使用了
    } else {
        None         // linear 没被使用!但 else 分支会自动析构它
    }
}

// 中间规则下,需要每个分支都消费 linear
fn good(linear: Linear) -> Option<Linear> {
    if undecidable() {
        Some(linear)
    } else {
        linear.consume(); // 显式消费
        None
    }
}

// 最强规则下,还需要考虑程序所有可能的运行状态:
// 包括无限循环、panic、甚至 IO 故障等情况
fn handle_file(f: File) {
    while some_infinite_process() {
        // 这里在做一些永不停止的事情
    }
    f.close(); 
}

panic 是最大的难题,如果函数调用可能 panic,线性值的作用域内就存在一条隐式的"提前退出"路径。在这条路径上,值会被 RAII 析构,但这可能不满足线性类型的安全条件。对于这种情况的解决方案是引入不会 panic 的函数标注(即后面的"函数效果"):

rust 复制代码
#[no_panic]
fn safe_operation() {
    // 保证不会 panic
}

fn handle_file(f: File) {
    // 编译器知道这一行一定不会失败
    safe_operation()
    f.close(); 
}

5. Effects、Refinement Types、Pattern Types

5.1 Effect Types

Rust 已经有几种"函数效果":

rust 复制代码
const fn foo() {}  // 编译时可求值
async fn bar() {}  // 异步
// nightly:
gen fn baz() {} // 生成器
try fn qux() {} // 可失败

函数的效果声明了函数本身包含的特性和约束条件(如 async 只能在 async 函数中调用),当然,除此以外,Rust 还可以有更多的效果,如:

  • no_panic: 保证函数不会出现 panic,线性类型系统可以依照这一依据来对控制流进行分析(见上文)
  • no_io:保证不会因为调用 IO 产生副作用。更进一步,可以有 no_host 保证不会去调用宿主主机 API

5.2 Refinement Types 更精确的类型

Pattern types 用 pattern 语法给已有类型附加约束,说实话,有点开始像 TypeScript 了:

rust 复制代码
// 用 pattern 约束定义 NonZeroUsize,不再需要特殊的编译器优化
type NonZeroUsize = usize is 1..;

// 更多例子
type Percentage = u8 is 0..=100;
type AsciiChar = u8 is 0..=127;

5.3 View Types

View types 让编译器理解"不重叠的部分借用",从而能够更方便的分散持有可变引用:

rust 复制代码
struct Foo { a: String, b: String }

// 现在不行,编译器不知道 &mut self.a 和 &mut self.b 不冲突
// 有了 view types,可以同时持有两个不同字段的可变引用
fn process(foo: &mut {a} Foo, bar: &mut {b} Foo) {
    // 分别处理 a 和 b
}

5.4 类型系统各元素关系

这些类型系统特性之间存在关联,它们都是为了保证在编译期获取到更丰富的信息来支撑 Rust 的安全性。

  1. Effect types 回答这个函数能做什么(能 panic 吗?能做 IO 吗?能异步吗?)
  2. Substructural types 回答这个值能怎么用(能丢弃吗?能移动吗?能复制吗?),如,线性类型。
  3. Refinement types 让编译器知道这个类型有什么约束(非零?在某个范围内?)
  4. View Types 让编译器知道"部分借用",能够更好的进行 RAII 推断。

参考资料

相关推荐
在水一缸1 天前
当 AI 编码助手遇上 Rust:深入解析 Zerostack 的极简哲学与实战应用
rust·系统架构·开源项目·轻量化·ai编码助手·zerostack
2401_873479402 天前
风控引擎高并发IP查询方案对比:开源Rust库 vs 商业离线库,怎么选?
tcp/ip·rust·开源·ip
花褪残红青杏小2 天前
Rust图像处理第22节-从RGB到YCbCr: 让亮度和颜色分家
rust·webassembly·图形学
花褪残红青杏小2 天前
Rust图像处理第21节-最小二乘回归:用矩阵求逆解"拟合"问题
rust·webassembly·图形学
k4m7v2pz2 天前
Bevy 0.14.2 玩家精灵不渲染(只有背景在动)排查全记录
macos·rust·bevy
老王生涯2 天前
rust开发环境配置-Windows & GNU
开发语言·windows·rust
Yeauty2 天前
自建 HLS 第一问:fMP4 还是 TS?用 Rust 在进程内把两种都跑出来
开发语言·后端·rust
Flynt3 天前
我用Biome替换ESLint+Prettier在项目上踩了不少坑
rust·eslint·前端工程化
Yeauty3 天前
2026 年在 Rust 里处理音视频,该走哪条路?
rust·ffmpeg·音视频·视频
TDengine (老段)3 天前
TDengine Go 与 Rust 连接器 — 高性能异步访问
大数据·数据库·物联网·golang·rust·时序数据库·tdengine