2311rust特征

Rust无成本抽象

Rust中抽象基石是trait:
1,TraitRust中唯一的接口概念.多个类型可实现一个特征,事实上,可为现有类型提供新的特征实现.另一方面,想抽象未知类型时,找特征就行了.
2,与C++模板一样,可静态分发特征.
3,可动态分发特征.有时确实需要间接,所以不必运行时"擦除"抽象.想运行时分发时,可使用接口特征.

背景:Rust中的方法

Rust提供了方法自由函数,它们密切相关:

struct Point {
    x: f64,
    y: f64,
}
//把(借用的)点转换为串的`自由`函数
fn point_to_string(point: &Point) -> String { ... }
//"固有的`impl"`块,直接在`类型`上定义了可用的方法
impl Point {
    //此方法在`Point`上都可用,并自动借用`Point`值
    fn to_string(&self) -> String { ... }
}

上述to_string方法叫"固有"方法,因为它们:
1,(通过impl)绑定到单个具体的"self"类型.
2,在该类型值上自动可用.也即,与函数不同,内置方法总是"在域内".

方法第一个参数总是是显式的"self",根据期望的所有权级别,它是self,&mut self&self.使用.调用方法.
self参数,按方法中使用的self形式隐式借用:

let p = Point { x: 1.2, y: -3.7 };
let s1 = point_to_string(&p);  //显式借用,调用自由函数.
let s2 = p.to_string();        //按`&p`隐式借用,来调用方法

如下,流式生成进程的API:

let child = Command::new("/bin/cat")
    .arg("rusty-ideas.txt")
    .current_dir("/Users/aturon")
    .stdout(Stdio::piped())
    .spawn();

特征是接口

接口允许每个代码自由切换.对特征,规范主要围绕方法展开.
如,以下用来哈希的简单特征:

trait Hash {
    fn hash(&self) -> u64;
}

为了为给定类型实现此特征,必须提供匹配签名的哈希方法:

impl Hash for bool {
    fn hash(&self) -> u64 {
        if *self { 0 } else { 1 }
    }
}
impl Hash for i64 {
    fn hash(&self) -> u64 {
        *self as u64
    }
}

Java,C#Scala等语言中的接口不同,可为现有类型实现新特征(如上面的Hash).即可在事后创建抽象,并应用至现有库.

内置方法不同,仅当特征在域时,特征方法才在域中.但是假设Hash在域内,你可编写true.hash(),因此实现一个特征扩展了类型上可用的方法集.
定义和实现特征不过是抽象多个类型满足的通用接口.

静态分发

一般通过泛型消费特征:

fn print_hash<T: Hash>(t: &T) {
    println!("The hash is {}", t.hash())
}

在未知T类型上,print_hash函数是泛型函数,但要求T实现Hash特征.即可与booli64值一起,使用它:

print_hash(&true);      //实例化`T=bool`
print_hash(&12_i64);    //实例化`T=i64`

静态分发中编译掉泛型.也即,与C++模板一样,编译器生成print_hash方法的两个副本来处理上述代码,每个副本对应一个具体参数类型.

反之表明内部调用t.hash()(实际使用抽象点)的成本为零:按直接静态调用相关实现编译它:

//编译后的代码:直接调用特化`bool`版本
__print_hash_bool(&true);  //
__print_hash_i64(&12_i64);   
//直接调用特化`i64`版本

对像print_hash类函数,该编译模型不是很有用,但对更实际的哈希使用,却非常有用.假设还引入了一个相等比较特征:

trait Eq {
    fn eq(&self, other: &Self) -> bool;
}

这里按实现trait的类型解析Self的引用;在impl Eq for bool中,它引用bool.
然后,可定义一个在实现哈希EqT类型上是都通用的哈希映射:

struct HashMap<Key: Hash + Eq, Value> { ... }

泛型静态编译模型有几个好处:
1,对具体的KeyValue类型,每次使用HashMap都会产生不同的具体HashMap类型,即HashMap可在其存储桶内联(无间接)布局键和值.
来可节省空间和间接,并提高缓存局部性.

2,HashMap上的每个方法同样会生成特化代码.即,如上,调用哈希Eq,不会产生额外成本.表明优化器可用最具体(也即没有抽象)的代码.
特别是,静态分发允许在泛型用法间内联.
总之,与在C++模板一样,你可用泛型编写无成本的相当高级的抽象.
但是,与C++模板不同的是,会提前完全类型检查特征客户.也即,单独编译HashMap时,会根据抽象HashEq特征检查一次代码类型正确性,而不是在每当应用具体类型时的重复检查.

即库作者可更早,更清晰地出现编译错误,而客户类型检查成本更少(即编译速度更快).

动态分发

有时,抽象不仅是重用或模块化,有时在运行时不能去掉抽象.
如,GUI框架一般涉及响应事件(如点击鼠标)的回调:

trait ClickCallback {
    fn on_click(&self, x: i64, y: i64);
}

GUI元素,常见的是,允许为单个事件注册多个回调.对泛型,可想象这样写:

struct Button<T: ClickCallback> {
    listeners: Vec<T>,
    ...
}

但问题立即显现出来:即每个按钮都按ClickCallback一个实现特化,且按钮类型反映了该类型.这不是想要的!

相反,想要一个带一组每个都可能是不同具体类型,但都实现了ClickCallback异构监听器的Button类型.
难点是,如果是一组异构类型,则每个类型都有不同的大小,则如何才能布局内部向量?答案一般是:间接.在向量存储回调指针:

struct Button {
    listeners: Vec<Box<ClickCallback>>,
    ...
}

在此,就像它是一个类型一样,使用ClickCallback特征.在Rust中,特征类型,但它们是"无大小的",只允许出现在Box(指向堆)或&(可任意指向)等指针后面.

Rust中,像&ClickCallbackBox的类型叫"trait对象",它包括指向实现ClickCallbackT类型实例的指针,及一个虚表:一个指向T对trait每个方法实现的指针(这里,只是on_click).
可在运行时用该信息正确分发调用方法,并确保统一表示T.因此,只编译一次Button.

多用途

1,闭包.

类似ClickCallback特征,Rust中的闭包只是特定特征.深入

2,条件API.泛型可有条件地实现特征:

struct Pair<A, B> { first: A, second: B }
impl<A: Hash, B: Hash> Hash for Pair<A, B> {
    fn hash(&self) -> u64 {
        self.first.hash() ^ self.second.hash()
    }
}

在此,仅当组件实现Hash时,Pair类型才实现Hash,但允许在不同环境中使用单个Pair类型,这样最大化支持每个环境API的可用性.

这在Rust中很常见,因此内置了.

#[derive(Hash)]
struct Pair<A, B> { .. }

3,扩展方法.可用Traits使用新方法扩展(在其他地方定义的)现有类型,类似C#的扩展方法.只需在特征中定义新方法,为相关类型提供实现,就可用该方法.

4,标记.Rust有一些"标记类型":发送,同步,复制,调整(Send, Sync, Copy, Sized).这些标记只是带空体的特征,然后可在泛型和特征对象中使用.

可在库中定义标记,它们会自动提供#[derive]风格实现:如,如果所有子类型都是Send,则类型也是.如前,这些标记可能非常强大:发送(Send)标记是Rust保证线安的方式.

5,重载.Rust不支持用多个签名定义相同方法的传统重载.但是trait提供了重载的大部分好处:如果在trait泛型定义了方法,则实现该trait的类型都可调用它.

传统重载相比,有两个优点.首先,即重载不是临时的:一旦理解一个特征,就会立即理解使用它的API重载模式.
其次,它是可扩展的:通过提供新的特征实现,可有效地在方法下游提供新的重载.

6,符号.Rust允许在自己类型上重载+等符号.由相应标准库特征定义每个符号,实现该特征类型也会自动提供符号.

你可能感兴趣的:(rust,rust)