Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

basic/trait/trait-object #685

Mar 31, 2022 · 202 comments · 146 replies
Discussion options

You must be logged in to vote

Replies: 202 comments 146 replies

Comment options

看到这一脸懵逼

You must be logged in to vote
8 replies
Comment options

目前为止最抽象的吧

Comment options

反复入门几次就不懵逼了...

Comment options

昨天一遍,今天一遍,不懵逼了😉

Comment options

+1,一脸懵逼

Comment options

其实就是多态,如果不用dyn就是静态多态,在编译期参数的类型编译成不同的函数,一个类型一个函数。如果用了dyn就是运行时多态,通过查输入对象的vtable获取对应的方法

Comment options

特征对象要划重点,得多温习

You must be logged in to vote
4 replies
Comment options

重点太多啦 :)

Comment options

这个dyn到底是什么啊......

Comment options

这个dyn到底是什么啊......

dynamic, 动态的

Comment options

谢谢🙏

Comment options

尚未完全理解

You must be logged in to vote
0 replies
Comment options

各位大佬们,怎么理解 Box动态分发,就像例子中:
let x = 1.1f64;
// do_something(&x);
let y = 8u8;

draw1(Box::new(x));
draw1(Box::new(y));

这里x, y不是已知类型么,为什么不能像静态分发一样,为u8和f64分别生成一份代码

You must be logged in to vote
5 replies
Comment options

fn draw3(x: &impl Draw) {
 x.draw();
}
fn main() {
 draw3(&1.1f64);
 draw3(&8u8);
}

这里是可以静态分发的,我的理解是,在一个流程里类型推导有歧义的话,才一定要用动态分发,就像 returns_summarizable 那样

Comment options

在你说的这个例子里 自然是可以用泛型来做静态分发的
请回头考虑文章中开始提出动态分发时的举例是用作返回值的场景

Comment options

如果你的Box里面装的是一个trait object呢?这就是动态分发了。

Comment options

你的例子用静态分发没问题.
这里主要要看 Screen::components 的类型是怎么定义的.

一开始的需求就是:

  1. 对象集合类型/个数 并不能事先明确地知道
  2. 这个组件可能是三方实现的. (自定义组件)
  3. 需要循环 draw

然后, 即便如此, 这种场景就不能做到静态分发吗? 是的,因为在编译器是不可能知道 componets 是怎么收集起来的. 那类型就不可能猜的出来了

Comment options

泛型+特征约束是静态分发的,特征对象是动态分发的。两个应用的场景不同。

Comment options

fn add<T: Add<T, Output = T>>(a: T, b: T) -> T

这个 Output = T 没看懂啊

You must be logged in to vote
9 replies
Comment options

可以把代码转换成使用Where约束:
fn add(a : T, b : T ) -> T
Where T: Add<T,Output=T>
这样比较好理解

Comment options

放弃了1年之后再回来看了一遍 看懂了

Comment options

我的理解是<T: Add<T, Output=T>>的意思是泛型T实现Add特征,并且T和T进行add操作返回值也是T。<T, Output=T>相当于对T: Add进行进一步说明Add的参数和返回值是什么。

Comment options

尝试自己写了一个trait,感觉这里可能是这样的, 不过我也没求证过 Add 内的具体实现,希望有懂的大佬👻

// @note: 类似的Add特征
trait PAdd {
 type OutputPAdd;
 fn add(self, rhs: Self) -> Self::OutputPAdd;
}
#[derive(Debug, Clone)]
struct Point3d<T: std::ops::Add> {
 x: T,
 y: T,
 z: T,
}
// @note: 应该解读为为 泛型 Point3d<T> 实现 PAdd 特征,
// 而 T 这个类型要满足 std::ops::Add 特征
impl<T> PAdd for Point3d<T>
where
 T: std::ops::Add<T, Output = T>, 
 // 这里的Output就是Add这个特征里面定义的一个type
 // 类似PAdd的type
{
 type OutputPAdd = Point3d<T>;
 fn add(self, rhs: Point3d<T>) -> Self::OutputPAdd {
 Point3d {
 x: self.x + rhs.x,
 y: self.y + rhs.y,
 z: self.z + rhs.z,
 }
 }
}
fn main()
{
 let p1 = Point3d {
 x: 1.1f32,
 y: 1.1,
 z: 1.1,
 };
 let p2 = p1.clone();
 // 由于 + 本身是 std::opts::Add 特征的 add 函数实现,
 // 而我们实现的是 PAdd 特征,所以这里只能显式调用
 let p3 = p1.add(p2);
 dbg!(p3);
}
Comment options

看一下Add的官方文档就懂了,trait Add实现其实很简单:

pub trait Add<Rhs = Self> {
 type Output;
 // Required method
 fn add(self, rhs: Rhs) -> Self::Output;
}

trait Add唯一的作用就是加法运算符 + 。下面是使用泛型实现 Add 特征的相同 Point 结构的示例:

use std::ops::Add;
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point<T> {
 x: T,
 y: T,
}
// Notice that the implementation uses the associated type `Output`.
// 注意,实现使用了关联类型`Output`。
impl<T: Add<Output = T>> Add for Point<T> {
 type Output = Self;
 fn add(self, other: Self) -> Self::Output {
 Self {
 x: self.x + other.x,
 y: self.y + other.y,
 }
 }
}
assert_eq!(Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
 Point { x: 3, y: 3 });

说白了就是需要规定 + 运算之后返回的类型到底是什么,Output是trait Add 中必须明确指定的一项。

Comment options

最后好像没有直接给出一开始提出问题的解决方法, 是在练习里吗?
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
Post {
// ...
}
} else {
Weibo {
// ...
}
}
}

You must be logged in to vote
4 replies
Comment options

这样子就好了
fn returns_summarizable(switch: bool) -> Box{
if b {
Box::new(Post{...})
}else{
Box::new(Weibo{...})
}

}

Comment options

这样子就好了
fn returns_summarizable(switch: bool) -> Box{
if b {
Box::new(Post{...})
}else{
Box::new(Weibo{...})
}

}

Comment options

fn returns_summarizable(switch: bool) ->Box<dyn Summary >{
if b {
Box::new(Post{...})
}else{
Box::new(Weibo{...})
}
}
Comment options

Add trait 里面定义了一个关联类型的泛型,比较特殊,

pub trait Add<Rhs = Self> {
 type Output;
}

所以在声明的时候,也需要声明关联类型的类型。

Comment options

觉得 Box 智能指针能在本节做一个介绍,可能会更方便大家理解本节内容

You must be logged in to vote
1 reply
Comment options

好的,建议收到

Comment options

特征对象,有点像java的接口类型,例如Map,List。例如:

List<String> list = new ArrayList<String>();

List是一个接口,ArrayList类型实现了List接口。

You must be logged in to vote
1 reply
Comment options

还是有很大区别的,特征trait 就是一种共享行为的抽象。 可以为任何类型,实现! 比java接口灵活多了!,但是,由于孤儿规则。 特化还没实现,也有很多限制! 哎,感觉抽象(继承)就不是很好实现。要不就要写特征宏,把抽象的的一种繁琐,转化为另外一种繁琐!

Comment options

眼睛看懂了,手还没懂。练习题都不够撸。

You must be logged in to vote
0 replies
Comment options

"m1 max太厉害了,电脑再也不会卡"

我好像知道了什么,留下了贫穷的泪水

You must be logged in to vote
2 replies
Comment options

m1 pro 也不卡,可以冲

Comment options

哈哈 什么 mi max! 还是我的3090Ti 无敌!!

Comment options

看到这里,表示这是我学习 Rust 的第一个卡点。。

You must be logged in to vote
3 replies
Comment options

卡点? m1 max吗? 哈哈,多练练就有感觉了

Comment options

同....真第一个卡点。

Comment options

好痛苦

Comment options

特征对象可以模拟继承

You must be logged in to vote
5 replies
Comment options

类似虚函数?
声明的类型是父类,实际绑定的是子类
用(表面)父类调用非 final 方法时,会动态派发到子类相同方法执行

Comment options

+多态

Comment options

+1,感觉和C++的多态在效果上面很相像

Comment options

子类可以继承父类的成员变量,trait不能定义成员变量。

Comment options

不是严格的继承, 顶多算是广度实现,他只是一个维度, 比说说我这个trait 会返回一堆实现struct 、 enum 什么的,我可以用dyn 动态获取其引用,封装起来, 但是获取的实例只能操作方法,不与原生struct、enum有关

Comment options

其实就是"多态",在不同的语言里面有不同的名称、语法和实现方式,比如c语言就是用不安全的指针粗暴地实现,c++用虚函数来实现,java通过抽象类与接口来实现,像go,swift这种很新的语言都倾向于通过更灵活的鸭式辨型的抽象来实现多态和动态派发,go里面叫interface,swift里面叫protocol,而rust里面就叫trait

You must be logged in to vote
1 reply
Comment options

Java那个接口 实现也是虚函数表, 封装了一下隐藏了细节
这一章写的烂完了..... 新手别说看蒙了 我老鸟都看蒙了...... 后面反应过来 就这
写的是什么勾八

Comment options

为自定义类型实现 + 操作

给struct Point加 :Add<T,Output =T > 和 不加 有什么区别呢?
我的理解是
加不加都无法限制 不符合Add<T,Output =T >的 Point 实例 被创建呀!

请大佬讲解下

You must be logged in to vote
1 reply
Comment options

加了这个能确保你在实现Point的Add特征时,其内部的T是实现了Add特征的。

Comment options

修复上一节中的 largest 函数

用下标处理,就不需要 Copy 了。

fn largest<T: PartialOrd>(list: &[T]) -> &T {
 let mut largest_idx = 0_usize;
 
 for i in 0..list.len() {
 if list[i] > list[largest_idx] {
 largest_idx = i;
 }
 }
 &list[largest_idx]
}
You must be logged in to vote
7 replies
Comment options

more graceful way to implement this method

pub fn largest<T>(list: &[T]) -> &T
 where T: Ord {
 list.iter()
 .map(|x| x)
 .max()
 .expect("find largest item failed")
}
Comment options

你这个太实战了,不适合用来讲解 :D

Comment options

map 多余了

pub fn largest<T>(list: &[T]) -> &T
 where T: Ord {
 list.iter().max().unwrap()
}
Comment options

没看明白这里为什么把&item修改为item,能解释一下么?

Comment options

list.iter()返回的是&T类型,&item时实际是&item与&T匹配,item的类型是T,改成item后item的类型就是&T了,可以复制到vscode中,观察修改前后自动推导的item的类型

Comment options

真抽象啊!!!

You must be logged in to vote
0 replies
Comment options

前面的都能理解,其实在类似于 java 中的多态,只是因为 rust 中没有继承,导致需要特征对象作为载体(在 Java 中则是父类 or 接口),仅就动态分发这一 part而言,肯定是比 java 啰嗦一些。
另外不太理解的是特征对象的限制:

当一个特征的所有方法都有如下属性时,它的对象才是安全的

假如特征 Draw 有 2 个方法 draw1 和 draw2,而我只需要用到其中的 draw1,难道也要要求 draw2 去符合对特征要求的限制吗?

You must be logged in to vote
2 replies
Comment options

再结合习题看本章,有几个疑问

代码 1

trait MyTrait {
 fn f(&self) -> Self;
}
impl MyTrait for u32 {
 fn f(&self) -> u32 { 42 }
}
impl MyTrait for String {
 fn f(&self) -> String { self.clone() }
}
fn my_function(x: impl MyTrait) -> impl MyTrait {
 x.f()
}
fn main() {
 my_function(13_u32);
 my_function(String::from("abc"));
}

Q1:以上代码中 trait MyTrait 为什么可以返回值可以是Self?这不就违背了特征对象的限制么?
A1:猜测原因是在此例中,MyTrait 仅为特征,而特征对象,特征对象在使用上应当以 Box<dyn MyTrait>dyn MyTrait 形式,本例中未涉及,因此不需要受特征对象的限制。

代码 2

pub trait Summary {
 fn summarize(&self) -> String;
}
pub struct Post {
 pub title: String, // 标题
 pub author: String, // 作者
 pub content: String, // 内容
}
impl Summary for Post {
 fn summarize(&self) -> String {
 format!("文章{}, 作者是{}", self.title, self.author)
 }
}
pub struct Weibo {
 pub username: String,
 pub content: String
}
impl Summary for Weibo {
 fn summarize(&self) -> String {
 format!("{}发表了微博{}", self.username, self.content)
 }
}
fn returns_summarizable(switch: bool) -> impl Summary {
 if switch {
 Post {
 title: String::from(
 "Penguins win the Stanley Cup Championship!",
 ),
 author: String::from("Iceburgh"),
 content: String::from(
 "The Pittsburgh Penguins once again are the best \
 hockey team in the NHL.",
 ),
 }
 } else {
 Weibo {
 username: String::from("horse_ebooks"),
 content: String::from(
 "of course, as you probably already know, people",
 ),
 }
 }
}
fn main() {
 let post = Post{title: "Rust语言简介".to_string(),author: "Sunface".to_string(), content: "Rust棒极了!".to_string()};
 let weibo = Weibo{username: "sunface".to_string(),content: "好像微博没Tweet好用".to_string()};
 println!("{}",post.summarize());
 println!("{}",weibo.summarize());
	returns_summarizable(true);
	returns_summarizable(false);
}

Q2:同样代码 2 的 fn returns_summarizable(switch: bool) -> impl Summary; 函数返回值使用了特征约束,代码而会有问题,而代码 1 的 fn my_function(x: impl MyTrait) -> impl MyTrait; 却没问题,这是为什么?
A2: 有人能帮忙解答一下吗?

Comment options

针对Q1:你回答的没毛病,MyTrait 仅为特征不受特征对象约束
针对Q2:代码2中fn returns_summarizable(switch: bool) -> impl Summary; 函数返回值使用了特征约束,并非特征对象,这其实就是泛型的一个语法糖形式,该签名完整写法如下:
fn returns_summarizable<T:Summary>(switch: bool) -> T;
函数返回值就只能为一种确定类型,所以该函数编译报错

Comment options

有一些描述的语言,翻译味道比较重,不太像中国话,知识倒是还好理解。

You must be logged in to vote
0 replies
Comment options

PHP终于有用一下了,泪目

You must be logged in to vote
0 replies
Comment options

c++中的 concept + abstract class

You must be logged in to vote
0 replies
Comment options

特征对象的动态分发:

  1. 什么是特征对象?当将某类型实例赋值给一个 Box<dyn Draw>、&dyn Draw (这样的特征对象类型,这里是类型)变量(以这两个类型进行约束的变量),或者将其传递给一个接受特征对象的函数参数。一旦这个行为发生,那个新的变量才是一个特征对象。

  2. 特征对象保存了作为特征对象的数据指针和行为指针(指向 vtable),这里的数据指针指向的是:在转变为特征对象之前的类型实例数据在内存中的位置;这里的行为指针指的是:特征对象包含指向虚函数表(vtable)的指针。这个 vtable 是在编译时为每个实现了 Draw 特征的具体类型(例如 Button,初始的类型实例)生成的,包含:初始类型的大小和对齐信息、实现 Draw 特征中所有方法的实际地址(这里是 draw 方法的地址)。

  3. vtable 概念是针对特征对象的,被视为 dyn Draw 的特征对象所关联的 vtable 只包含 Draw 特征中定义的方法。以及该具体类型的大小和对齐信息。这个 vtable 是专门为 Draw 特征构建的。

  4. vtable 的作用就是提供一个统一的接口,让你能够通过特征对象调用任何实现了该特征的类型上的方法。它只包含那些这个特定特征"关心"的方法。

  5. 特征对象只能调用实现于特征的方法( 例如 Draw 的 draw 方法),而不能调用类型(例如 Button )本身实现的方法和类型 (例如 Button )实现于其他特征的方法。再具体一点来说: 特征对象提供的是统一的、基于 Trait 的接口。当你拥有一个 &dyn Draw 类型的值时,你唯一能确定的就是它实现了 Draw Trait。因此,你只能调用 Draw Trait 中定义的方法。你无法通过这个特征对象访问其底层具体类型(Button)独有的方法,也无法访问 Button 可能实现的其他 Trait 中的方法。

You must be logged in to vote
1 reply
Comment options

特征对象的限制:
对于,方法没有任何泛型参数(不能有哈?),做如下理解:如果 Trait 中的方法本身是泛型的(例如 fn transform(&self, val: U)),将意味着该方法可能有无限种可能的实例化(每种 U 都是一个不同的方法)。vtable 需要一个固定数量和结构的槽位,无法容纳无限多的方法。

再者,如果方法签名中包含泛型参数 S: Into 这样的绑定,其中 Self 是一个类型参数,编译器就无法确定其具体类型,也就无法在 vtable 中为这个方法提供一个固定的函数指针。

另外需要补充:

  1. 如果一个 Trait 包含了不带 self 参数的关联函数,那么这个 Trait 就不能被用来创建特征对象。
    为什么?: 特征对象的核心机制是通过 vtable 来实现运行时方法分发。vtable 是一张函数指针表,这些函数指针都是实例方法(即需要一个 self 参数)。关联函数(静态方法)不操作特定的实例,它们不需要 self 参数。因此,vtable 无法为不带 self 的关联函数提供一个入口,也没有实例可以"调度"。

完整来看:
一个 Trait 如果包含以下任何一种情况,就会被认为不安全,从而不能被用来创建特征对象 (dyn Trait):
方法(Method)返回 Self: fn clone_self(&self) -> Self;

无法知道返回的具体类型和大小。
方法(Method)有 Self 类型参数(除了接收者 self): fn from_self(&self, other: Self);

无法在 vtable 中为这种不确定类型的参数生成调用签名。
方法(Method)是泛型的: fn transform(&self, value: T) -> T;

vtable 需要固定数量的槽位,无法为无限种泛型方法变体提供入口。
(例外:泛型参数被约束为 Sized 或 Copy 时有时是对象安全的,但这个复杂。)
关联函数(Associated Function,即不带 self 参数的方法):

通常,Trait 中的关联函数本身就不能被通过特征对象来调用。 因此,如果 Trait 只有关联函数而没有方法,它就不能被用作特征对象。
尤其当关联函数返回 Self 或接受 Self 参数时:fn new() -> Self; 或 fn default() -> Self;。这更明确地违反了对象安全的原则,因为在编译时无法知道要创建或操作的 Self 具体是什么。

Comment options

函数 returns_summarizable 可以返回 summarizable 对象的方式 Rust Playground :

fn returns_summarizable(switch: bool) -> Box<dyn Summary> {
 if switch {
 let summarizable = Post {
 title: String::from(
 "Penguins win the Stanley Cup Championship!",
 ),
 author: String::from("Iceburgh"),
 content: String::from(
 "The Pittsburgh Penguins once again are the best \
 hockey team in the NHL.",
 ),
 };
 Box::new(summarizable)
 } else {
 let summarizable = Weibo {
 username: String::from("horse_ebooks"),
 content: String::from(
 "of course, as you probably already know, people",
 ),
 };
 Box::new(summarizable)
 }
}
You must be logged in to vote
0 replies
Comment options

这玩意在swift里叫协议,我说的对吗

You must be logged in to vote
1 reply
Comment options

用起来差不多一样。从原理上看Rust的trait和所有权是有关的。

Comment options

特征对象还有一个限制:特征内的函数不能为关联函数,即第一个参数必须是self、&self、&mut self

You must be logged in to vote
0 replies
Comment options

如果你想要为类型 A 实现特征 T,那么 A 或者 T 至少有一个是在当前作用域中定义的

准确来说是在当前crate中定义的

You must be logged in to vote
0 replies
Comment options

这一节写的太难懂了 ,用到的后面的知识有点多 所以我先跳过了 回来再看

You must be logged in to vote
0 replies
Comment options

这一章到底是哪个傻卵写的 废话多 例子狗屎

You must be logged in to vote
0 replies
Comment options

拥有java和js经验, 对我来说不难理解,但是太多不同的地方要去记忆..先粗过一遍,后面边写边记

You must be logged in to vote
0 replies
Comment options

长期的学习过程,非常陡峭

You must be logged in to vote
0 replies
Comment options

pub trait Summary {
 fn summarize(&self) -> String;
}
pub struct Post {
 pub title: String, // 标题
 pub author: String, // 作者
 pub content: String, // 内容
}
impl Summary for Post {
 fn summarize(&self) -> String {
 format!("文章{}, 作者是{}", self.title, self.author)
 }
}
pub struct Weibo {
 pub username: String,
 pub content: String,
}
impl Summary for Weibo {
 fn summarize(&self) -> String {
 format!("{}发表了微博{}", self.username, self.content)
 }
}
fn returns_summarizable(switch: bool) -> Box<dyn Summary> {
 if switch {
 Box::new(Post {
 title: String::from("Penguins win the Stanley Cup Championship!"),
 author: String::from("Iceburgh"),
 content: String::from(
 "The Pittsburgh Penguins once again are the best \
 hockey team in the NHL."
 ),
 })
 } else {
 Box::new(Weibo {
 username: String::from("horse_ebooks"),
 content: String::from("of course, as you probably already know, people"),
 })
 }
}
fn main() {
 returns_summarizable(true);
}
You must be logged in to vote
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

AltStyle によって変換されたページ (->オリジナル) /