---
title: "rust 概念"
date: "2026-07"
category: "随笔"
tags: ["随笔"]
description: "rust本身比较有意思的地方是，他是一个强类型-内存安全的语言，所以说很多东西都会规定的很死，通过一定的动态牺牲，带来了较大的安全性和稳健性..."
source: "https://enriquemark.com/zh-hans/posts/rust%20%E6%A6%82%E5%BF%B5"
---

rust本身比较有意思的地方是，他是一个强类型-内存安全的语言，所以说很多东西都会规定的很死，通过一定的动态牺牲，带来了较大的安全性和稳健性。

比如变量上，rust这边的变量默认就是常量，需要 `let mut` 额外声明的才是可变的（mutable）。而还有关于内存的明确操作，关于字符串这里就能看出来：

```rust
// 这是一个指向字符串视图的指针，是纯粹只读的
let s1: &str = "hello";
// 这才是实际的字符串对象，可以修改
let s2: String = String::from("hello");
```

而rust最关键的概念还在于「所有权」这个核心，所有值都有一个所有者，而变量则在赋值的那一刻拿到这个所有权。就所有权来说，这里分为两个情况：所有者转移自身的所有权，背后发生的操作是 `move`  代表着的就是所有者的转换；其次则是 `copy` ，这里没有发生所有权转移，而是一模一样的直接复制，原值也仍然有效，这里同时存在两个值和两个所有者。从所有权的角度看，`copy` 是直接创建了一个新的值（连带它的所有者），而旧的仍然存在。`move` 则不然，旧的所有者会失去所有权。对于有些对象来说，想要达到 `copy` 的效果必须显示的去 `clone` ，不然默认就是转移，旧变量在赋值给新变量的那一刻，他的所有权就会转移给新的变量。

理清了所有权之后，就能更好地理解它的相关延展，比如拿走和借用：

```rust
fn consume(s: String) {
    println!("{}", s);
}

fn main() {
    let a = String::from("hello");
    consume(a);
    // println!("{}", a); // error：a 已经被 move
}
```

这里展现了一个初看会让人困惑的操作，那就是在ts里，对象或者说值（本质也是对象）本身是可以被所有人重复使用的。但是在rust这里，任何对象都必须先明确所有权，只有具备权限者才能进行其范围内的操作。`s: String` 在这里意味着所有权转移，也即s成为了唯一可操作此对象的所有者，a因为不具备任何权限，因此它的任何操作也是失效的，除非它进行一次所有权的转移操作 `let a = s`，接过s的所有权，其后续操作才能成为合法操作。

但这里有一个部分会让人非常难以理解，那就是乍一看，这里似乎把一个公用的对象给直接私有化了，那岂不是我其他函数都用不了 `String` 对象了？但其实不是这样，所有权有「作用域」的限制，一旦离开s的作用域（比如这里就是函数内部），那么它的所有权就会被 `drop`，新作用域中，a需要重新获得所有权，才能使用——比如 `a: String`，它之所以操作失败不是因为s私有化了这个所有权，挤占了其他人使用，而是因为a自身没有在用之前先取得所有权。

随着 [rust在ai开发中的优势](/zh-hans/posts/rust在ai开发中的优势) 继续说，有一个容易让人混淆的部分，就是双冒号（::）和点(.)的区别。这个概念在其他语言下是混用的，但在rust的哲学之下就需要明确区分。前者意味着命名空间导航，即针对于「结构」的索引，即「不需要具体值就存在的东西」，一般的抽象蓝图，除非在蓝图里就预定义好常量的值，否则默认取到的就是成员本身；而后者是对具体「值」的调用，即「必须有具体值才存在的东西」，对蓝图的实际实现。

```rust
struct Point {
    x: i32,
    y: i32,
}

impl Point {
	// 关联函数(没有 self)，这个类比的话很像class中的静态方法，可以在非实例化的情况下直接使用
    fn new(x: i32, y: i32) -> Point {    
        Point { x, y }
    }
    // 方法(有 self)，这意味着必须先有了值才能去使用这个方法
    fn distance(&self) -> f64 {          
        ((self.x * self.x + self.y * self.y) as f64).sqrt()
    }
}

let p = Point::new(3, 5);   // :: 从类型 Point 取关联函数 new  ✅
p.x                          // .  从实例 p 取字段            ✅
p.distance()                 // .  在实例 p 上调方法          ✅
Point::x                     // ❌ 非法,x 是字段不是关联项
```

这个地方需要明确防止一个误会，那就是双冒号取的是类型成员，而不是类型内的字段，因为只有「成员」才有路径身份，而「字段」是没有的。

```rust
Point::new(3, 5)        // new 是关联函数,有路径身份 ✅
Point::ORIGIN_Y         // ORIGIN_Y 是关联常量,有路径身份 ✅
Point::y                // y 是字段,没有路径身份,处处非法 ❌
```

而这里就要顺带引出一个概念，`impl`乍看之下很像是类，但实际上并不是。核心点就在于rust从根本上ban了继承，因此这个地方的`impl`只是一个「给类型附加方法」的结构体，而 `&self`从字面意思上就指向这个类型`Point`自身。

因此，上面说的像静态方法的「关联函数」，乍一看很像是类的构造函数，毕竟new的时候返回了一个Point实例。但实际上并非如此。跟一般类的构造函数规定只能有一个不同，`fn new`在整个 `impl` 里面，跟其他方法相比是完全平等的。这就意味着写几个都行，也意味着它根本不是一个「构造函数」，只是普通的静态（鉴于rust里没有类这一说，因此这里是类比意义上）工厂方法。

而关于原先OOP的其他概念，rust仍旧有所留存，比如封装，就通过 `pub` 这个保留关键词来指定暴露想要的接口，而相较于其它语言需要明确声明 `private`，它在类型内的属性是默认私有的。

```rust
mod geometry {
    pub struct Point {
        x: i32,        // 私有字段
        pub y: i32,    // 公开字段
    }
}

// 在模块外面:
let p = geometry::Point { ... };
p.y    // ✅ 能访问,因为 y 是 pub
p.x    // ❌ 编译错误:x 是私有的,模块外碰不到
```

而 `trait` 则是一个比较重要的概念，直接等价于其他语言中的 `interface` ，即接口（契约）的概念。本质上是对行为结构的抽象定义，也是rust中DI能顺利施行的重要因素——通过接口定义好行为结构后，就可以使后续的组合面向接口而非具体实现编程。但rust的`trait`本身可以带默认实现（看上去有点像类了，但这个显然跟刚才的一样，是预定义好的方法）。

```rust
trait Animal {
    fn speak(&self) -> String;
    fn greet(&self) -> String {           // 默认实现
        format!("我说:{}", self.speak())  // 复用 speak,谁实现 Animal 都白捡这个 greet
    }
}
```

而这玩意儿最强也最独特的地方似乎给标准库，或者说任何其他已有的类型加实现。

```rust
trait Describe {
    fn describe(&self) -> String;
}
impl Describe for i32 {                            // 给标准库的 i32 加行为!
    fn describe(&self) -> String { format!("我是数字 {}", self) }
}

42.describe()    // ✅ "我是数字 42"
```

其次则是可以明确的区分编译时和运行时，这里稍微有点复杂，涉及到不同语言彼此之间对于多态的派发策略，即到底是编译期间就直接定死，还是运行时去查一个虚拟表来确定此泛型指向什么具体的函数——大部分语言对于接口上的此类泛型传入处理，默认都是动态派发的，还没法选，因此有一个查表开销和运行时的混合问题；而rust在这里明确的通过 `T` 和 `dyn` 的区分，或者说，通过设计了一套「泛型 和 trait object」的两条路，将两者显式化了。这本身就是rust的设计哲学的再一次体现，那就是「将隐含的东西显现化」，无论是通过所有权概念体现的内存控制，还是显式边界，此处只是又一次贯彻。

```rust
// 静态派发(编译期多态):泛型 + trait 约束
// 此时的 T 最终是永久单态化的，即编译时定了什么，最终就是什么，不然报错，不放到运行时才决定
fn make_speak<T: Animal>(a: T) -> String {
    a.speak()                    // T 是任何实现了 Animal 的类型,编译期定死,零开销
}

// 动态派发(运行时多态):trait object，&代表引用，一个指向a的指针的指针
// 这里会根据运行时决定，此处的 dyn Animal 取决于运行时到底给入什么
fn make_speak_dyn(a: &dyn Animal) -> String {
    a.speak()                    // 运行时才知道具体是 Dog 还是 Cat,查 vtable
}

// 注意 vec![...] 只是个演示用的list构造，模拟的是运行时给入多个类型的情况
let zoo: Vec<Box<dyn Animal>> = vec![Box::new(Dog), Box::new(Cat)];
for animal in &zoo {
    println!("{}", animal.speak());   // 同一个循环,Dog 喵 Cat 各说各的 —— 经典多态
}
```

然后另一个则是`trait`最不同于`interface`的点，那就是它可以写成空的。

```rust
// Send / Sync 大致长这样(简化):
trait Send {}     // 空的!没有任何方法
trait Sync {}
```

这里的 Send 和 Sync ，或者说本质上这种空的 trait 是一个会自动连锁传播的标记，可以在编译期就给所属行为打上一个标签。这种标签使得编写者可以显式控制和声明特定操作，从而在该操作不该发生的地方在编译期就拦截。比如Rc标准库，他的用途是建立索引，让一份数据被多个所有者共享，而其内部会维护一个所有者计数器——对这东西的修改会存在并发竞争问题。那么此时 Send 标签就能发挥作用。

Rc 的编写者只需要显式声明此处是 !Send 的，而跨线程操作库的 `thread::spawn` 再予以配合，要求只有 Send 标签的操作才能够通过。那么通过这种跨方法约定，就可以直接将危险操作拦截在编译器，而不至于到了运行时才暴露。这个操作往广了说可以非常灵活，比如并发，复制，只要能加标签同时要在另一个地方设置门槛时，都可以用类似机制。从编译上就让不符合门槛的操作直接报错，绝对不让你到运行时才出问题。只要一方设置了标签，那么实现了这个接口的子对象都会自动被传播到。这个特性是 Send / Sync 特有的，叫做auto trait，会自动传播到所有消费方。

上面那个自动传播机制是 rust 的特色，其他语言没有，但除此之外的其他自定义空壳标签就没有这个传播机制了。比如我写了个表现，但是后来忘了设置查票者或者给下游盖章，那这一套就无效了。换言之，这套标签机制的有效性取决于编写者的设计和对自身代码的边界把控能力。如果毫无设计，该出的问题还是会出现。但如果这样，rust 最额外的优势在哪里呢？核心点在于「编译期」保证。rust 的空 trait 一旦加上，后续没有那就是没有，直接报错，但类似的比如 java，就得到运行期才能暴露问题。

```rust
// ① 定义标签(空壳)
trait A {}

// ② 给某个类型盖章
struct Dog;
impl A for Dog {}          // 为 Dog 装上 A

struct Cat;
// Cat 不盖章

// ③ 下游:在函数签名里"查票"—— <T: A> 就是那道卡
fn need_ticket<T: A>(x: T) {
    // 能进到这个函数体里的 x,类型必然带 A。编译器已经替你保证了。
}

// ④ 消费方尝试
need_ticket(Dog);   // ✅ Dog 有 A,过
need_ticket(Cat);   // ❌ Cat 没 A,编译报错:the trait `A` is not implemented for `Cat`

fn need_ticket<T: A>(x: T) {}              // 写法1:泛型参数上直接标
fn need_ticket<T>(x: T) where T: A {}      // 写法2:where 子句,复杂约束时更清爽
fn need_ticket(x: impl A) {}               // 写法3:impl Trait 语法,最简洁
```

rust 这里比ts的优势在于，这个标签没办法被下游轻易的用 as 断言盖过去。如果非得写，必须显式声明，把该对象的类型明确指定到标签上，代价很明确，这意味着下游全部都知道你加了这个标签（因为是空的，所以不影响行为，但会明确打上章，而很难暗度陈仓）；而 as 断言就只是一句临时断言，并未根本上改变原先对象的类型。其次则是，在 rust 这里 A {}的主语是A，而ts那里则是{}。ts的类型本身对结构体负责，换言之，空的{}结构可以对应到任何一个对象，而不是标签的语义意涵。
