Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[第三章] 关于trait的Self类型参数不能被限定为Sized #137

Closed
ChyuWei opened this issue Feb 18, 2019 · 14 comments
Closed

[第三章] 关于trait的Self类型参数不能被限定为Sized #137

ChyuWei opened this issue Feb 18, 2019 · 14 comments
Labels
已修订 已经修订并提交给出版社 第三章 第三章 精选 表意问题 语言表达有问题,有歧义或难理解

Comments

@ChyuWei
Copy link

ChyuWei commented Feb 18, 2019

页码与行数

  • 第73页
  • 薛定谔的类型附近

对于

trait Foo: Sized{
    fn some(&self);
}

文中试图解释为什么不能对trait添加Self:Sized限定:

把trait当做对象使用时, 内部默认为Unsize类型, 将其置于编译期可确定大小的胖指针后, 以供动态调用.
...
如果给trait加上Self:Sized限定, 动态调用trait对象的过程中, 如果碰到Unsize 类型,调用方法将引发段错误.

对于trait Foo, 应该对所有实现Foo的类型都要求为Sized. 所以在后面应该不会有Unsize 的类型.

https://github.com/rust-lang/rfcs/blob/master/text/0255-object-safety.md
https://github.com/rust-lang/rfcs/blob/master/text/0546-Self-not-sized-by-default.md

根据这两个rfc, 貌似只是说为了保证trait object必须实现当前trait所以把Sized限定去掉了.

所以书中那部分描述可能不太完善.

@ZhangHanDong ZhangHanDong added 第三章 第三章 表意问题 语言表达有问题,有歧义或难理解 labels Feb 18, 2019
@ZhangHanDong
Copy link
Owner

@ChyuWei 感谢反馈。我在考虑修改描述,后续讨论。

@kvinwang
Copy link

kvinwang commented Feb 19, 2019

看了RFC,和之前群里讨论理解的差不多
把之前默认的Self: Sized放宽为Self: ?Sized是为了让dyn Trait能自动实现Trait。
而Sized的类型不能创建trait object是上述改动的结果。

那么,假如Rust放宽限制,究竟Sized类型会不会导致安全问题呢?
@ChyuWei给出的两个RFC可以看出之前是没有这个限制的,两个RFC也没有提到修复了任何安全漏洞,所以应当是不存在安全问题。

下面我假设Rust不限制来看一下两种情况的区别:

Self: Sized Self: ?Sized
能实现SomeTrait的类型 T: Sized (dyn SomeTrait不能实现SomeTrait) T:Sized
T:Unsized
能创建dyn SomeTrait的类型 T: Sized T: Sized

可以看出如果允许Sized类型创建trait object唯一的区别就是dyn SomeTrait是否实现了SomeTrait,对于能创建trait object的类型约束没有任何变化,并不会导致安全问题的区别。

正如rfc0255中说的,加以限制,是为了更好的编译错误提示、减少软件迭代产生的困扰(可能原设计更容易导致breaking change?)、更好的设计,相应的缺点就是限制了灵活性。

@ZhangHanDong
Copy link
Owner

相关 : #51

@ZhangHanDong
Copy link
Owner

今天有时间好好翻了下相关RFC,我发现我一直掉在一个坑里:「安全」。我一直在寻找「对象安全」在安全性上的 意义,但我发现,「对象安全」和安全性并没有关系,它关乎的是vtable的方法调度问题。

所以,一个trait,只有当编译器可以自动为其实现自身的时候,才是对象安全的。方法通过存储在trait对象的vtable中,将每个方法实现为动态的函数调用。如果没有对象安全规则,虽然可以编写满足trait对象的类型签名的函数,但是其内部无法实际使用trait对象。

trait Foo {
    fn method_a(&self) -> u8;

    fn method_b(&self, x: f32) -> String;
}

// automatically inserted by the compiler
impl<'a> Foo for Foo+'a {
    fn method_a(&self) -> u8 {
         // dynamic dispatch to `method_a` of erased type
         self.method_a()
    }
    fn method_b(&self, x: f32) -> String {
         // as above
         self.method_b(x)
    }
}

@ZhangHanDong
Copy link
Owner

ZhangHanDong commented May 8, 2019

补充

我写书的时候,看见「安全」这两个字,就自动脑补了一个解释:「对象安全肯定和内存安全相关」。导致我想当然了。

trait对象,在运行时已经擦除了类型信息,要通过虚表调用相应的方法。不像静态分发那样,trait对象不是为每个类型都实现trait的方法,而是只实现一个副本(自动为其实现自身),结合虚函数去调用。

现在想一个问题: 假如那个类型没有实现这个方法怎么办?实际上,会有很多种情况下,会出现这个问题。运行时确定的类型和方法应该合法的,保证trait对象在运行时可以安全地调用相关的方法。

比如trait里有泛型函数。这就搞的很复杂了,可能运行时无法确定该调用哪个函数。反正是各种情况吧。所以,为了避免出现这种问题,官方引入了对象安全的概念。实际上就是引入了一系列的规则,也就是书里列出的那些。编译器根据这些规则,在编译期判断一个你写的trait对象,是不是合法的。

比如:trait对象其实在内部维护两个表:safe_vtable和nonself_vtable,标记有where Self: Sized 的会被归类到nonself_vtable,也就是说,不会被trait对象调用。

如果是合法的,则代表了,这个trait对象在运行时调用方法应该是没问题的。不会出现没有实现,或者不知道该调用哪个的情况。这就是对象安全的概念。

它和内存安全并无直接关系。

这里有个讨论值得查看,里面探讨了trait对象设计的历史相关内容: https://internals.rust-lang.org/t/trait-objects-blocking-entire-traits-vs-blocking-members/8796/7

@ZhangHanDong
Copy link
Owner

合并: #213

@ZhangHanDong
Copy link
Owner

ZhangHanDong commented Jun 22, 2019

代码清单3-40修正:

// 对象不安全的trait 
trait Foo {
    fn bad<T>(&self, x: T); 
    fn new() -> Self; 
}
// 将对象不安全的泛型方法拆分为独立的trait
trait Bar {
    fn bad<T>(&self, x: T); 
} 
// Foo就成为了对象安全的trait 
trait Foo: Bar { 
    fn new() -> Self; 
}
// 对象安全的trait,使用where子句
trait Foo {
    fn new() -> Self where Self: Sized; 
}

@ZhangHanDong
Copy link
Owner

对象安全相关描述修改为:

当把trait当作对象使用时,其内部类型就默认为Unsize类型,也就是动态大小类型,只是将其置于编译期可确定大小的胖指针背后,以供运行时动态调用。对象安全的本质就是为了让trait对象可以安全地调用相应的方法。如果没有Sized的限定,那么就会很容易写出无用的类型。比如Box,它虽然会编译,但是不能用它做任何事情。对于更复杂的trait,往往就没有这么明显了,只有在做了大量繁重的工作之后可能会突然发现某个trait对象无法正常调用方法。

trait对象,在运行时已经擦除了具体类型信息,要通过虚表调用相应的方法。不像静态分发那样,trait对象不是为每个类型都实现trait的方法,所以,为trait增加Sized限定,然后编译器自动为该trait实现自身,就可以在编译期准确排除无效的trait对象。这就是对象安全。需要注意的是,对象安全和内存安全并无直接的关联,它只是保证trait对象在运行时可以安全准确地调用相关的方法。

trait对象在内部也维护两个表: safe_vtable和nonself_vtable,标记有where Self: Sized 的会被归类到nonself_vtable,也就是说,不会被trait对象调用。所以反过来,当不希望trait作为trait对象时,可以使用Self: Sized进行限定。

@ZhangHanDong
Copy link
Owner

@ChyuWei @kvinwang 我对描述做了修正,准备第四次印刷了,大家看看还有什么问题?

@ZhangHanDong ZhangHanDong added the 已修订 已经修订并提交给出版社 label Jun 22, 2019
@ZhangHanDong ZhangHanDong pinned this issue Jun 22, 2019
@lignyxg
Copy link

lignyxg commented Jul 22, 2019

@ZhangHanDong 关于代码清单3-40,修正后

// 将对象不安全的泛型方法拆分为独立的trait
trait Bar {
    fn bad<T>(&self, x: T); 
} 
// Foo就成为了对象安全的trait 
trait Foo: Bar { 
    fn new() -> Self; 
}

有两点没看明白

  1. 拆分后Foo的方法new依然将Self作为返回类型,对象安全不是要求Self只能出现在方法的第一个参数吗?
  2. Bar不是对象安全的,因为其中的方法有泛型参数,然后Foo继承了Bar,也就是说实现Foo的类型一定要先实现Bar,这样为什么说Foo就成为了对象安全的trait呢?

@wzgy
Copy link

wzgy commented Dec 11, 2019

@ZhangHanDong 关于代码清单3-40,修正后

// 将对象不安全的泛型方法拆分为独立的trait
trait Bar {
    fn bad<T>(&self, x: T); 
} 
// Foo就成为了对象安全的trait 
trait Foo: Bar { 
    fn new() -> Self; 
}

有两点没看明白

  1. 拆分后Foo的方法new依然将Self作为返回类型,对象安全不是要求Self只能出现在方法的第一个参数吗?
  2. Bar不是对象安全的,因为其中的方法有泛型参数,然后Foo继承了Bar,也就是说实现Foo的类型一定要先实现Bar,这样为什么说Foo就成为了对象安全的trait呢?

同样的困惑😳

@ZhangHanDong
Copy link
Owner

@lignyxg @wzgy
可能是描述的问题,之前那个注释忘记改了吧。。。

对象安全的trait是指下面被Sized限定的Foo

// 对象安全的trait,使用where子句
trait Foo {
    fn new() -> Self where Self: Sized; 
}

@liuweiccy
Copy link

liuweiccy commented Mar 13, 2020

@ZhangHanDong 关于代码清单3-40,修正后

// 将对象不安全的泛型方法拆分为独立的trait
trait Bar {
    fn bad<T>(&self, x: T); 
} 
// Foo就成为了对象安全的trait 
trait Foo: Bar { 
    fn new() -> Self; 
}

有两点没看明白

  1. 拆分后Foo的方法new依然将Self作为返回类型,对象安全不是要求Self只能出现在方法的第一个参数吗?
  2. Bar不是对象安全的,因为其中的方法有泛型参数,然后Foo继承了Bar,也就是说实现Foo的类型一定要先实现Bar,这样为什么说Foo就成为了对象安全的trait呢?

同样的困惑😳

struct Name {
    name: String,
}

trait Foo1 {
    fn bad<T>(&self, x: T);
}

trait Foo {
    fn new() -> Self where Self: Sized;
    fn say(&self);
}

impl Foo for Name {
    fn new() -> Self where Self: Sized {
        Name { name: "rust".to_string() }
    }
    
    fn say(&self) {
        println!("name: {:?}", self.name);
    }
}

impl Foo1 for Name {
    fn bad<T>(&self, x: T) {
        x;
    }
}

fn dyn_dispatch(d : &dyn Foo) {
    d.say();
}

#[test]
fn test_trait_safety() {
    let name: Name = Foo::new();
    dyn_dispatch(&name);
}

1:拆分后Foo的方法new依然将Self作为返回类型因为它满足方法受Self:Sized约束所以对象安全;因为方法受Self:Sized约束满足三点方法签名任一条件就是对象安全的方法
2:我的拆分没有使用trait继承
@wzgy @lignyxg

@simake2017
Copy link

dyn trait 既然是由数据指针和 vtable指针构成,为什么是unsized

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
已修订 已经修订并提交给出版社 第三章 第三章 精选 表意问题 语言表达有问题,有歧义或难理解
Projects
None yet
Development

No branches or pull requests

7 participants