当前位置: 首页 > news >正文

Rust:Trait 抽象接口 特征约束

Rust:Trait 抽象接口 & 特征约束

    • 抽象接口
      • 定义与实现
      • 默认实现
      • 泛型 Trait
      • 关联类型
      • 关联常量
      • Trait 继承
      • 完全限定语法
      • 孤儿规则
      • NewType 模式
    • 特征约束
      • where 子句
    • Nominal Typing


TraitRust 最强大的特性之一,既是类型系统的支柱,也是泛型约束的核心机制。它大致有四大功能:抽象接口、泛型约束、抽象类型、标签。本博客聚焦于前两点。

Trait的含义是特征,它是类型所具有特征。你可以理解为,它是某些类型所具有的能力。比如说两个i32类型之间具有做加法的能力,实际上是具有Add这个Trait。但是两个char不能做加法,就说明这个类型不具有加法的能力,实际上是char没有实现Add这个Trait

现在也许你有一些初印象了,Trait表达的是一个类型具有的某些能力。


抽象接口

抽象接口是Trait的最基础的用法,它的特点如下:

  1. 接口中可以定义方法,并支持默认实现
  2. 接口中不能实现另一个接口,但接口之间可以继承
  3. 同一个接口可以被多个类型实现,但不能被一个类型实现多次
  4. 使用impl关键字为类型实现接口方法
  5. 使用trait关键字定义接口

接下来我们会一点一点讲解以上内容,构建起这个Trait的知识体系。

定义与实现

Trait定义了一组可以被共享的行为,只要实现了Trait,该类型就能使用这组行为

  • 定义Trait
trait t_name {fn func_1(args...) -> ret;fn func_2(&self, args..) -> ret;
}

使用trait关键字,可以定义一个Trait。在它的名字后面,使用{}定义这个Trait所具有的方法。在定义时,往往只写出函数的签名,并且允许使用Self作为第一个参数,函数体暂时用;代替,表示还未实现。

  • 实现Trait
impl t_name for type {fn func_1(args...) -> ret {// ...}fn func_2(&self, args..) -> ret {// ...}
}

使用impl关键字,可以为指定类型实现指定Trait,例如impl Add for i32就是给i32类型实现Add。在{}中,需要给每一个之前定义的方法做出实现,必须实现所有未提供默认实现的方法

假设现有两个结构体:

struct Person {name: String,age: u8,can_swim: bool,
}struct Duck {color: String,
}

它们分别表示一个人,以及一只鸭子。Person::can_swim表示这个人是否学会游泳。

随后定义一个Trait,表示一个类型是否有游泳的能力:

trait Swim {fn swim(&self);
}

分别为DuckPerson实现这个Swim的特征:

impl Swim for Duck {fn swim(&self) {println!("I was born to swim.");}
}impl Swim for Person {fn swim(&self) {if self.can_swim {println!("I've learned to swim.");} else {println!("I can't swim.");}}
}

对鸭子来说,天生就会游泳,而人类需要通过后天学习。

let p = Person {name: "zhangsan".to_string(),age: 28,can_swim: true,
};let d = Duck {color: "yellow".to_string(),
};p.swim();
d.swim();

最后,不论是Person还是Duck类型,都可以去调用swim这个方法,并且最后执行了不同的函数逻辑。


默认实现

Trait 可以提供默认实现,这样实现者可以直接使用,也可以选择覆盖。

trait Swim {fn swim(&self) {println!("I was born to swim.");}
}

在定义Swim这个Trait的时候,可以直接为其默认实现,后续实现该Trait的类型,可以选择不实现这个方法,从而使用默认实现。

比如此时Duck类型就可以直接使用默认实现:

impl Swim for Duck { }impl Swim for Person {fn swim(&self) {if self.can_swim {println!("I've learned to swim.");} else {println!("I can't swim.");}}
}

impl Swim for Duck的时候,没有实现任何方法,使用了默认实现。


泛型 Trait

Trait中,支持使用泛型,语法如下:

trait t_name<T, ...> {
}impl<T, ...> t_name<T, ...> for type {
}

只需要在名称后面添加泛型声明列表,后续在实现中就可以使用。

例如实现一个MySwap

trait MySwap<RHS> {fn my_swap(self, other: RHS) -> (RHS, Self);
}impl<RHS> MySwap<RHS> for i32 {fn my_swap(self, other: RHS) -> (RHS, Self) {(other, self)}
}

此处的RHS是一个泛型,表示右操作数Right Hand Side。与基本的泛型语法相同,在impl时需要先impl<>声明泛型,后续才能使用。

要注意的是:当模板参数不同,MySwap是不同的Trait,例如MySwap<i32>MySwap<i64>是两个不同的Trait实现,这是通过单态化实现的


关联类型

假设现在我们要实现一个MyAdd,它表示加法操作。

trait MyAdd<RHS, Output> {fn my_add(self, rhs: RHS) -> Output;
}

它涉及两个泛型参数,RHS表示右操作数,Output表示最终返回值。分开两个泛型参数是因为不同情况下所需的返回值可能不同,比如说 i64 + i32i32 + i64最好返回i64,才能防止溢出。而前者Self = i32RHS = i64,后者相反。你很难把这个返回值固定下来,以适配所有情况,所以把返回值单独做成了一个泛型。

实现这个Trait

// i32 + i32
impl MyAdd<i32, i32> for i32 {fn my_add(self, other: i32) -> i32 {self + other}
}// i32 + i64
impl MyAdd<i64, i64> for i32 {fn my_add(self, other: i64) -> i64 {self as i64 + other}
}// i64 + i32
impl MyAdd<i32, i64> for i64 {fn my_add(self, other: i32) -> i64 {self + other as i64}
}

现在就可以正常调用了:

 let i3: i32 = 10;let i6: i64 = 10;let r1 = i3.my_add(i3);let r2 = i3.my_add(i6);let r3 = i6.my_add(i3);

现在看来一切正常,这个方案确实可以实现我们的目的,在不同情况下根据两个操作数决定加法返回类型。

但有一个问题是,两个类型相加,那么返回类型也应该是固定的,但这个实现方案,允许已知的两个操作数返回不定的类型。

例如:

// i32 + i32
impl MyAdd<i32, i32> for i32 {fn my_add(self, other: i32) -> i32 {self + other}
}// i32 + i32
impl MyAdd<i32, i64> for i32 {fn my_add(self, other: i32) -> i64 {(self + other) as i64}
}

以上代码,把i32 + i32依据返回值不同,实现了两个返回版本,这就进一步导致两个确定类型的加法,允许返回一个不确定类型。

你可以利用返回值自动推导泛型参数,来调用不同版本:

let i3: i32 = 10;
let r4: i32 = i3.my_add(i3);
let r5: i64 = i3.my_add(i3);

以上代码中,r4调用的是返回i32的版本,而r5调用的是返回i64的版本。因为泛型是调用层来决定调用哪一个版本的。

我们希望的是,当SelfRHS已知,那么Output就是固定的,这才符合Rust对类型系统的安全性要求。也就是说我们希望SelfRHS是可变的,这两个固定则Output固定,但目前SelfRHSOutput都是可变的。

此时就需要引入关联类型

关联类型允许为不同类型实现Trait时,分别指定一个类型进行关联。

  • 在定义时,通过type关键字定义一个关联类型:
trait MyAdd<RHS> {type Output;fn my_add(self, rhs: RHS) -> Self::Output;
}

此处的Output就是一个关联类型,在定义时,关联类型相当于一个泛型,它的类型是不确定的。

  • 在实现时,需要给关联类型指定一个具体的类型:
// i32 + i32
impl MyAdd<i32> for i32 {type Output = i32;fn my_add(self, other: i32) -> Self::Output {self + other}
}// i32 + i64
impl MyAdd<i64> for i32 {type Output = i64;fn my_add(self, other: i64) -> Self::Output {self as i64 + other}
}// i64 + i32
impl MyAdd<i32> for i64 {type Output = i64;fn my_add(self, other: i32) -> Self::Output {self + other as i64}
}

Self = i32RHS = i32时,则Output = i32,后两个同理。

这就是关联类型的作用:Trait确定,实现该Trait的类型也确定,则关联类型是确定的

要注意的是,前两个实现中,都是i32实现MyAdd,但是它们实现的不是同一个Trait,前者是MyAdd<i32>,后者是MyAdd<i64>,所以两者的Output允许不同。

可以通过下图来理解此处的逻辑:

在这里插入图片描述

左侧是Output作为泛型的版本,右侧是Output作为关联类型的版本。整个Trait分为三个步骤,定义,实现,调用。

在泛型中,泛型参数的取值是在调用时决定的(左侧绿色部分),用户可以自由指定RHSOutput的类型,如果有匹配的实现,那么就调用成功。用户传入确定的SelfRHS,依然可以进一步自行决定Output

而在关联类型中,关联类型的取值是在实现时决定的(右侧绿色部分),那么用户传入确定的SelfRHS后,一定得到的是确定的Output

两者的核心区别在于,确定这个Output类型的时机不同(绿色部分),一个在用户调用时确定,另一个在实现时就已经确定了

在实际应用中,如果你确实希望用户可以自行决定这个类型,那么将它作为泛型。如果你希望当SelfTrait已经确定时,某个类型也得到一个确定值,那就将它作为关联类型。

最后,在定义Trait时,可以设置关联类型的默认值:

trait MyAdd<RHS> {type Output = Self;fn my_add(self, rhs: RHS) -> Self::Output;
}

这个特性需要较nightly版本的Rust支持,截止1.90.0依然不稳定。


关联常量

除了关联类型,Trait 还可以定义关联常量,此处的关联常量和之前在impl时讲的关联常量是一样的。

trait Limit {const MAX: u32;
}struct Counter;
impl Limit for Counter {const MAX: u32 = 100;
}

与关联类型同样的是,关联常量在定义Trait阶段定义,在实现阶段确定值。

trait Limit {const MAX: u32 = 100;
}

关联常量可以给一个默认值,并且已经是一个稳定特性了,可以直接使用。


Trait 继承

Rust不支持面向对象中的类型继承,但是在Trait之间允许继承。

语法:

trait t_name: t1 + t2 ... {
}

定义Trait时,在名称后面使用:指明要继承的其它Trait,多个Trait之间使用+分隔。

此处继承的含义为:如果某个类型要实现该Trait,必须实现它继承的所有Trait

示例:

trait Introduce: Display {fn introduce(&self) {println!("我是:{}", self);}
}

此处的 Introduce 用于自我介绍,它要求类型必须实现Display这个类型,才能保证print的时候不报错。

struct Cat {name: String,
}impl Display for Cat {fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {write!(f, "猫咪 {}", self.name)}
}impl Introduce for Cat {}let my_cat = Cat { name: "咪咪".to_string() };
my_cat.introduce();

在这个Cat类中,首先实现Display,然后才能实现Introduce,并且使用了默认实现。最后my_cat这个实例就可以调用introduce方法。

其实此处的Trait继承与面向对象中的继承区别还是很大的,它更多的是表示一种接口之间的依赖关系,比如introduce依赖Display


完全限定语法

一个类型的多个Trait是可以分别自由实现方法的,那么也就允许多个Trait中出现同名方法。而当多个 Trait 或者类型本身impl的方法名发生冲突时,编译器就会陷入困境。此时就需要 完全限定语法 (Fully Qualified Syntax)来避免歧义。

Rust 的方法调用有一个推导的过程:

  • 编译器会先在当前类型的固有方法里查找,即impl的内容
  • 如果没找到,再去该类型实现的所有 Trait 中查找
  • 如果存在多个候选,就会报错,提示你需要显式指定

完全限定语法如下:

<Type as Trait>::method(args...)

Type 表示具体的类型,Trait 表示方法来源的Traitmethod 是要调用的方法名,args... 是额外的参数。

假设我们有两个 Trait,它们都定义了一个同名方法:

trait Pilot {fn fly(&self);
}trait Wizard {fn fly(&self);
}struct Human;impl Pilot for Human {fn fly(&self) {println!("Pilot flying the plane!");}
}impl Wizard for Human {fn fly(&self) {println!("Wizard flying with magic!");}
}impl Human {fn fly(&self) {println!("Human flapping arms... not very effective.");}
}

此时 Human 类型同时具备三种 fly 方法:

  • 自身固有方法 Human::fly
  • 来自 Pilotfly
  • 来自 Wizardfly

如果直接调用:

let h = Human;
h.fly();

编译器会优先选择固有方法,因此输出:

Human flapping arms... not very effective.

但如果我们想调用 PilotWizard 的版本,就必须使用完全限定语法:

Pilot::fly(&h);            // 等价于 <Human as Pilot>::fly(&h)
Wizard::fly(&h);           // 等价于 <Human as Wizard>::fly(&h)
<Human as Pilot>::fly(&h); // 更显式的写法

此处由于第一个参数是self,所以要传入&h作为参数。通过完全限定语法就能明确告诉编译器要调用的是哪个 Trait 的实现。


孤儿规则

基于impltrait两个特性,你可以很轻易的给一个类型添加各种方法,这就有可能导致一些不太安全的操作。

比如你的某位同事,已经为一个类型封装好了它的各类接口和Trait。但是你使用这个类型前,又对它的这个Trait进行了实现,导致篡改了某些该类型原本的行为,这就是一种破坏性的改写,可能导致难以预料的Bug,孤儿规则可以避免类似的情况。

如果要实现某个Trait,那么该Trait和要实现这个Trait的类型,至少有一个要在当前Crate中定义

此处的Crate可以理解为一个库,比如std标准库算一个Crate,这个内容会在后续深入讲解。

  • 尝试给Vec<i32>实现std::fmt::Display

Vec<i32>是标准库中的一个动态数组类型,而std::fmt::Display是一个标准库的Trait,实现后可以被print输出。

impl std::fmt::Display for Vec<i32> {fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {write!(f, "My custom Vec: {:?}", self)}
}

以上代码尝试给Vec<i32>实现std::fmt::Display,但是这个代码会报错,因为Vec<i32>这个类型不属于本地,而std::fmt::Display也不属于本地,这违背了孤儿规则,编译不通过。

  • 给标准库类型实现本地Trait
trait MyTrait {fn my_method(&self);
}impl MyTrait for Vec<i32> {fn my_method(&self) {println!("Vec length: {}", self.len());}
}

以上代码给Vec<i32>实现MyTrait,这个代码是合法的,因为MyTrait是本地的,符合孤儿规则。

  • 给本地类型实现标准库Trait
struct MyStruct;impl std::fmt::Display for MyStruct {fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {write!(f, "This is MyStruct")}
}let my_instance = MyStruct;
println!("{}", my_instance);

此处的 MyStruct 是自己定义的类型,std::fmt::Display是之前提到的标准库Trait。实现这个Trait后,直接就可以通过print输出结构体。以上过程也是正确的,因为类型是本地的,符合孤儿规则。


NewType 模式

在介绍孤儿规则时我们提到:你不能为外部类型实现外部 trait。这条规则保证了编译器在全局范围内的一致性,但在工程实践中也经常让人卡壳。比如:

  • 你想为 String 实现某个第三方库的 trait
  • 或者你想为 Vec<T> 增加一个外部 trait 的实现

那么该怎么办?Rust 社区的惯用解法就是 Newtype 模式。

所谓 Newtype,就是用一个新的元组结构体把原有类型“包裹”起来:

struct MyString(String);

这样一来,MyString 是你自己定义的本地类型,本地类型自然可以实现任何外部 trait

假设我们想为 String 实现一个外部库的 Display

use std::fmt::{self, Display, Formatter};struct MyString(String);impl Display for MyString {fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {write!(f, "MyString says: {}", self.0)}
}fn main() {let s = MyString("hello".to_string());println!("{}", s);
}

输出:

MyString says: hello

这里的关键点在于:String 是外部类型,而Display 是外部 trait。直接 impl Display for String 会违反孤儿规则,但 MyString 是我们自己定义的本地类型,所以 impl Display for MyString 完全合法。


特征约束

在之前讲解过的所有泛型,都是不受约束的泛型,它们可以是任意类型。但这种情况其实反而是少数,在Rust中,大部分泛型都是收到约束的泛型。对泛型的约束,叫做泛型约束,而特征约束(Trait Bounds)属于泛型约束的一种

特征约束语法如下:

T: Trait_1 + Trait2 + ...

在进行泛型声明时,可以在对应的泛型后面使用:对这个泛型进行约束,只有实现某些Trait的泛型,才能传入。泛型声明主要出现在函数、方法、复合类型、泛型Trait中,接下来一个一个尝试。

  • 函数中的特征约束
fn get_max<T: std::cmp::PartialOrd>(a: T, b: T) -> T {if a > b {a} else {b}
}

函数 get_max 用于返回最大值,它接受一个泛型T。但不是所有类型都可以使用>进行比大小的,只有实现了 std::cmp::PartialOrd 这个Trait的类型才能直接比大小,因此对T进行泛型约束。

  • 方法中的特征约束
struct Point<T> {x: T,y: T,
}impl<T: std::fmt::Display> Point<T> {fn show(&self) {println!("x = {}, y = {}", self.x, self.y);}
}impl<T: std::ops::Add<Output = T> + Copy> Point<T> {fn add(&self, other: &Point<T>) -> Point<T> {Point {x: self.x + other.x,y: self.y + other.y,}}
}

以上代码,定义了一个Point类,它表示一个二维坐标,并分别实现了addshow方法。

对于show来说,在impl时通过特征约束,约束了T必须实现Display,这样xy才能正常print输出。

对于add来说,它要求T必须实现AddCopyAdd<Output = T>要求T必须可以进行加法,而且加法返回值是T,此处<Output = T>是对关联类型的限制,也就是说特征约束还可以限制关联类型。而Copy要求T可以进行拷贝操作,多个约束之间使用+连接。Copy涉及到所有权,会在后面深入了解,但是现在也可以简单讲一讲。

Add这个Trait中,内部的add方法决定了是否可以用+这个操作符,这个方法的第一个参数是self,而不是一个借用。因此 self.x + other.x 的时候,必须对这两个值进行一次拷贝,那就需要实现Copy这个Trait了。

当使用Point的时候,会根据不同的类型,决定它可以调用哪些方法。

比如i32满足以上所有约束,就可以调用addshow方法:

let mut p1 = Point { x: 5, y: 10 };
let mut p2 = Point { x: 50, y: 100 };
p1.show();
p2.add(&p1);
p2.show();

但是对于String来说,它只实现了Display,没有实现Add<Output = String>Copy,就只能调用show而不能调用add

let p_str= Point { x: String::from("hello"), y: String::from("world") };
p_str.show(); // 可以正常调用 show

在之前的博客提到过,一个类型可以有多个impl,多个impl结合特征约束,可以实现不同的类型拥有不同的方法。

  • 类型中的特征约束
struct Holder<T: std::fmt::Display> {value: T,
}impl<T: std::fmt::Display> Holder<T> {pub fn print_value(&self) {println!("我持有的值是: {}", self.value);}
}

以上代码中,将特征约束放在了定义类型时,此时只有实现了Display的类型才能作为Holder::value

但是在定义类型时进行了约束,不代表impl的时候可以省略这个约束,在impl时还是需要写出T的特征约束。这个特性用的比较少,一般不会在类型使用特征约束,而是在impl层面使用。

  • 泛型Trait中的特征约束

Trait 本身也可以是泛型 Trait,也就是说,一个 Trait 的定义可以引入泛型参数。这时如果你希望限制这些泛型参数的范围,就需要为它们添加特征约束。

trait Summary<T: Display> {fn summarize(&self, item: T) -> String;
}struct News;impl Summary<String> for News {fn summarize(&self, item: String) -> String {format!("新闻摘要:{}", item)}
}

以上代码中,Summary是一个泛型Trait,它要求T实现了Display。后续别的类型实现这个Trait的时候,保证参数item是可以直接被输出的。

  • 关联类型中的特征约束

除去以上使用泛型的位置,关联类型也是可以被特征约束的

trait Container {type Item: Display;
}

以上代码中,Container内部有一个关联类型Item,并通过特征约束要求其实现Display,后续impl的时候,具体的Item类型就必须是实现了Display的类型。

最后,可以回想一下Trait继承语法,它也是通过:Trait进行限定,使用+隔开多个Trait。这两个语法很相似,实际上Trait继承本质上也是一个特征约束,只是有部分人把它当做一种继承,这种说法也被社区接受了


where 子句

当特征约束变得复杂时,使用 : 语法会让代码可读性变差,特别是当泛型参数较多或多个特征约束组合时。

比如说刚才的:impl<T: std::ops::Add<Output = T> + Copy> Point<T>,这里仅仅涉及到两个Trait,就已经十分难以辨别了。

为此,Rust 提供了 where 子句来更清晰地组织约束。

语法如下:

// 定义类型时
struct MyType<T, U>
whereT: Trait_1 + Trait_2 ... ,U: Trait_3 + Trait_4 ... ,
{}// 定义方法时
impl<T, U> MyType<T, U>
whereT: Trait_1 + Trait_2 ... ,U: Trait_3 + Trait_4 ... ,
{}// 定义函数时
fn func<T>() -> T 
whereT: Trait_1 + Trait_2 ... ,
{}// Trait 继承时
trait Mytrait
whereSelf: Trait_1 + Trait_2 ... ,
{type Item where Self::Item: Trait_1; // 关联类型
}

当需要使用特征约束时,可以用where子句语法代替原本的:语法。以上示例展示了四种情况,分别是定义类型、定义方法、定义函数、Trait 继承。但其实它们不用分开记,它们有统一的特点:where子句直接写在{}前面

使用where定义一个子句,子句内部可以对所有之前声明过的泛型进行特征约束,多个泛型之间用,逗号隔开。对于每个泛型使用:语法表示特征约束。

另外的,关联类型特征约束时也是可以使用where子句的。当Trait继承时,受到约束的类型是Self。而在关联类型中使用where时,必须使用Self::做前缀。

实际上对于关联类型和Trait继承,不使用where子句反而更简洁,这个语法更多的用于有多个泛型的情况下,分别把每个泛型的特征约束列举出来。


Nominal Typing

学完Trait限定,不知道你有没有感觉它有点像鸭子类型。如果你没听过 Duck Typing,简而言之就是一句话:

“如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。”*

在编程语言里,这意味着:只要一个对象拥有某些方法,就可以被当作某个类型来使用,而不需要显式声明它属于这个类型。

比如在 Python 里,你写一个函数 make_it_quack(x),只要传入的对象有 quack() 方法,就能正常运行,根本不管它是什么类型。这就是典型的 Duck Typing,特点是灵活,在运行时对方法进行检查。

而在类似于C++这样的语言,一个函数写出来后,每个参数的类型都必须是写死的。它不以这个类型的功能为依据,而是以类型本身作为依据。

打个比方:某个公司招聘软件工程师。

鸭子类型的逻辑:公司要求受聘者可以进行软件开发。不管你曾经是什么专业,你可以学数学,可以学汉语言文学,甚至哪怕你不是一个人,你是一个会敲代码的猴子。只要你会软件开发,你都可以来面试。但是面试前谁知道你会不会敲代码?因此只能面试过程中对你提问,这就有可能导致面试过程中才发现这个人根本不会敲代码,浪费了时间。

非鸭子类型的逻辑:公司要求受聘者是软件工程专业毕业生。公司不以你是否可以敲代码为依据,而是要求你必须就是该专业的人。这样招募进来的人一定是会敲代码的,但是也会错失某些优秀人才。

因此鸭子类型和非鸭子类型的区别就体现出来了。鸭子类型下更加灵活,但是在运行过程中才能检查出问题,导致安全性和效率降低。而非鸭子类型,在起初就限制好了类型,只要你能通过类型检查,那就保证一定可以完成函数内部的操作,提高了安全性,但也降低了灵活性。

Rust 的特征约束看起来是不是有点像 Duck Typing

当我们写下:

fn make_it_swim<T: Swim>(x: T) {x.swim();
}

这段代码的语义就是:只要能游泳的类型都能传进来。是不是很像Duck Typing

但关键的不同在于:Rust 会在编译期就检查 T 是否真的实现了 Swim。换句话说,Rust 提供了 Duck Typing 的表达力,却把它变成了静态 Duck Typing

这背后体现的是 Rust 的设计思想:

  • 它不是动态语言的 Duck Typing,它不会等到运行时才发现对象不会叫。
  • 采用的是 Nominal Typing(名义类型):只有当你显式 impl Trait for Type 时,编译器才承认这个类型具备某个能力。

名义类型的逻辑:公司招募要求用户必须完成一场笔试,笔试通过才有面试机会,而且任何专业的人都可以笔试。这样既可以招募到各式各样的人才,又保证了参与面试的人一定具有开发能力。

而这个笔试的过程,就是Rust中的impl for

这种方案既保证了安全性,而且在编译期完成检查,对运行时没有任何效率影响,这也实现了Rust最注重的 效率 + 安全。


http://www.dtcms.com/a/596660.html

相关文章:

  • 【Windows】tauri+rust运行打包工具链安装
  • 网站被人抄袭怎么办哪家做网站的公司
  • 在 Linux 上实现 Spring Boot 程序的自动启动与守护运行
  • 得物TiDB升级实践
  • uni-app微信小程序相机组件二次拍照白屏问题的排查与解决
  • 邯郸大名网站建设网站服务器类型查询
  • 远程在线诊疗|在线诊疗|基于java和小程序的在线诊疗系统小程序设计与实现(源码+数据库+文档)
  • Linux进程间通信(IPC)常用方法精要
  • 展望无人机的未来发展,技术趋势和应用前景
  • 黄页 网站模板什么是展示型网站
  • gov域名网站有哪些如何建一个免费试用网站
  • Vue中 class 和 style 属性的区别对比
  • 视频融合平台EasyCVR:构建智慧化城市/乡镇污水处理厂综合安防与运营监管方案
  • 【ZeroRange WebRTC】KVS WebRTC C SDK 崩溃分析报告
  • 库卡机器人编程语言 | 深入了解库卡机器人的编程方法与应用
  • 移动+协作+视觉=?复合型机器人重新定义智能产线
  • 【macOS 版】Android studio jdk 1.8 gradle 一键打包成 release 包的脚本
  • 网站关键词优化原理亳州做企业网站
  • 数据库知识整理——SQL数据定义
  • AAAI 2026|港科大等提出ReconVLA:利用视觉重构引导,刷新机器人操作精度!(含代码)
  • Java 进阶:IO 流与 Lambda 表达式深度解析
  • 集团公司网站推广方案怎么做十年网站建设
  • 张祥前统一场论视角下的托卡马克Z箍缩不稳定性解读
  • 【每天一个AI小知识】:什么是MCP协议?
  • 在 kubernetes 上使用 SMB 协议做存储的「即插即用」方案
  • 软件测试大赛Web测试赛道工程化ai提示词大全
  • 智慧团建官方网站登录入口优秀的网站建设价格
  • 静海做网站公司十一月新闻大事件摘抄
  • GJOI 11.6 题解
  • Java Excel 导出:EasyExcel 使用详解