从零开始的 Rust 学习笔记(6)

  1. 为什么使用泛型
  2. 同时可以有多个泛型
  3. Enum 也可以泛型
  4. 方法定义时也可以泛型,亦可以特化其中一个/多个泛型
  5. Trait 很像 Protocol
  6. Trait 作为参数

1. 为什么使用泛型

为什么使用泛型呢~?跟 C++ 或者其他很多支持泛型的(强类型,虽然 C++ 实际上还是可以比较随意的 cast 的)语言一样——复用代码

比如说,C++ 里的 STL 容器,像是 std::vector<T>, std::map<K, V> 等等,也就是我们写的一些算法,本身来说是可以用在 很多 / 任意 类型上的,如果不支持泛型的话,我们就不得不自己为每一种类型都写一次( ;´Д`)

当然可以写一段代码让程序自动生成好,然后我们再复制粘贴到自己的模块中,不过粗浅来说,这也就是支持泛型的语言的编译器代替我们做的事情

更何况,如果我们的代码是要给别人用的,那我们更是无从知道别人会扔给我们什么类型,因此,我们有相对充足的理由让一门语言支持泛型,但这也不意味着泛型是一门语言所必须的 feature(比如有在语言层面上不支持泛型的,Google 的 golang,但是 golang 目前也有别的方法去解决泛型的问题)

那话说回来,最简单的例子就是:给一个数组,然后找到该数组中最大的那个元素

fn largest_element(array: Vec<i32>) -> Option<i32> {
    // check len()
    if array.len() == 0 {
        ()
    }
    
    // compute the largest
    let mut ret = array[0];
    for v in &array {
        if *v > ret {
            ret = *v;
        }
    }
    Some(ret)
}

fn main() {
    let p = vec![1,2,3,4,5];
    if let Some(largest) = largest_element(p) {
        println!("{}", largest);
    }
}

那这个时候,光是基本数据类型就有 i8, i16, i32, i64, i128, u8, u16, u32, u64, u128, isize, usize, f32, f64. 要是每个都单独写的话,就这么一个简单的功能都可以写上个 100 多行了

那么泛型的话,则是我们使用一个符号来替换具体的类型(就像是参数的名字一样),一般我们选择 T, 因为是 template 的缩写嘛~我们写的就是一个模版,编译器会用具体的类型去换掉这个模版

于是说了这么多,要改的其实只有一点点,就是用 T 去替换那个具体的类型嘛~(或者别的你喜欢的字符也可以)当然了,紧跟在函数名的后面,需要用 <> 将这个 T 写出来,也就是告诉编译器我们这里要用模版了

fn largest_element<T>(array: Vec<T>) -> Option<T> {
    // check len()
    if array.len() == 0 {
        ()
    }
    
    // compute the largest
    let mut ret = array[0];
    for v in &array {
        if *v > ret {
            ret = *v;
        }
    }
    Some(ret)
}

fn main() {
    let p = vec![1,2,3,4,5];
    if let Some(largest) = largest_element(p) {
        println!("{}", largest);
    }
}

2. 同时可以有多个泛型

那看了上面的代码的话,马上随之而来的问题就是,要是有两个数组,然后一个要找最大,一个要找最小,然后需求是将这个结果以结构体返回

其实就只需要将多个泛型的模版名字用 , 隔开就可以啦~

#[derive(Debug)]
struct MinMax<A, B> {
    a: A,
    b: B,
}

fn min_max<T, U>(array1: Vec<T>, array2: Vec<U>) -> Option<MinMax<T, U>> {
    // check len()
    if array1.len() == 0 || array2.len() == 0 {
        ()
    }
    
    // compute the largest
    let mut max = array1[0];
    for v in &array1 {
        if *v > max {
            max = *v;
        }
    }
    
    // compute the smallest
    let mut min = array2[0];
    for v in &array2 {
        if *v > min {
            min = *v;
        }
    }
    Some(MinMax {
        a: max,
        b: min
    })
}

fn main() {
    let p = vec![1,2,3,4,5];
    let q = vec![1.0,2.0,3.0,4.0,5.0];
    if let Some(minmax) = min_max(p, q) {
        println!("{:?}", minmax);
    }
}

3. Enum 也可以泛型

写到这里的时候,大概看到上面的

Option<MinMax<T, U>>

然后再回想起 Option<T> 的定义的话~

enum Option<T> {
    Some(T),
    None,
}

是的~ Enum 也是支持泛型的~用法跟之前我们定义带数据的 Enum 一样,只不过也是用模版替换了具体的类型

另一个常见的模版类型的 Enum 则是 Result<T, E> 啦(⁎⁍̴̛ᴗ⁍̴̛⁎) 是的~ Enum 也可以有多个不同模版类型参数

enum Result<T, E> {
    Ok(T),
    Err(E),
}

4. 方法定义时也可以泛型,亦可以特化其中一个/多个泛型

于是我们可以为一个带模版参数的结构体去写所有类型都适用的那些方法,也可以为其中某一些类型增加一些独立于别的的方法,比如

struct Data<T> {
    d: T
}

// functions specialized to Data<i32>
impl Data<i32> {
    fn debug(&self) {
        println!("debug on i32 type: {}", self.d);
    }
}

// all Data<T> have these functions implemented
impl<T> Data<T> {
    fn some_func(&self) {
        // ...
    }
}

fn main() {
    let p = Data::<i32> { d: 233 };
    p.debug();
    p.some_func();
}

在上面的代码中,我们为所有的 Data<T> 都定义了 fn some_func(&self),然后我们特别为 Data<i32> 定义了一个 fn debug(&self)

5. Trait 很像 Protocol

那么写到这里,你可能会发现,前面好多代码都没有贴上运行结果,或者在你尝试的时候,就已经发现,除了上面这个之后,前面的都并不能通过编译,rustc 会报错~

比如我们那个 largest_element 的例子,rustc 会告诉我们

error[E0369]: binary operation `>` cannot be applied to type `T`
   --> main.rs:10:15
    |
 10 |         if *v > ret {
    |            -- ^ --- T
    |            |
    |            T
    |
    = note: `T` might need a bound for `std::cmp::PartialOrd`
 

 error: aborting due to previous error

这个其实想想也是必然的嘛,怎么可能随便哪个类型丢过来都可以用个 > 符合比较两个实例的大小呢?甚至可能那种类型的同类之间做比较都是没有任何意义的!

于是 Rust 告诉我们 T 至少是要实现 std::cmp::PartialOrd

元素之间连个偏序关系都没有的话,还怎么比较呢

那么「满足有偏序关系」则成为了 T 的一个 Trait~

可我们怎么去声明一个 Trait 呢?举个简单的例子,我们做一个叫做 Summary 的 Trait,它要求满足它的类都要实现一个 summarize 的函数

pub trait Summary {
    fn summarize(&self) -> String;
}

这个写出来之后,我的第一感觉就是真的超像 Objective-C 里的 protocol,对比一下~

@protocol Summary
@required
- (NSString *) summarize;
@end

那么为一个类型实现一个 Trait,就像在 ObjC 中为一个类实现一个 protocol

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

对比 Objective-C 的话~

@protocol Summary
- (NSString *) summarize;
@end

@interface Tweet : NSObject <Summary>
@property (nonatomic) NSString * username;
@property (nonatomic) NSString * content;
@property (nonatomic) bool reply;
@property (nonatomic) bool retweet;
@end

@implementation Tweet
- (NSString *) summarize {
    return [NSString stringWithFormat:@"%@: %@", self.username, self.content];
}
@end

我个人是真的觉得特别相似

6. Trait 作为类型

当 Trait 作为类型时,其实就像 ObjC 的 delegate 一样,不关心具体是哪个类,关心的是是否实现了某个 protocol

pub trait Summary {
    fn summarize(&self) -> String;
}

pub fn notify(item: impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

而在 Objective-C 里面的话,就会写成是

@protocol Summary
- (NSString *) summarize;
@end

void notify(id<Summary> item) {
    NSLog(@"%@", [item summarize]);
}

当然,Rust 的写法比较灵活,上面还可以写成

pub trait Summary {
    fn summarize(&self) -> String;
}

pub fn notify<T: Summary>(item: T) {
    println!("Breaking news! {}", item.summarize());
}

这样的话,给我的感觉就是和 Swift 的写法几乎相同

protocol Summary {
    func summarize() -> String
}

func notify<T: Summary>(item: T) {
    print("Breaking News:", item.summarize())
}

如果要求 impl 了(或者说满足)多个 Trait 呢?与多个模版类型不同,多个 Trait 的话,Rust 里是用 + 来写~

pub fn notify(item: impl Summary + Briefly) {
    println!("Briefly: {}", item.briefly());
    println!("Summary: {}", item.summarize());
}

或者写在前面也是可以的~

pub fn notify<T: Summary + Briefly>(item: T) {
    println!("Briefly: {}", item.briefly());
    println!("Summary: {}", item.summarize());
}

Objective-C 的话,多个 protocol 之间还是用 , 分隔,不过写法就一种:

@protocol Summary
@required
- (NSString *) summarize;
@end

@protocol Briefly
@required
- (NSString *) briefly;
@end

void notify(id<Summary, Briefly> item) {
    NSLog(@"%@", [item briefly]);
    NSLog(@"%@", [item summarize]);
}

而 Swift 的话,则是是用 & 连接多个 protocol

protocol Summary {
    func summarize() -> String
}

protocol Briefly {
    func briefly() -> String
}

func notify<T: Summary & Briefly>(item: T) {
    print("Breaking News:", item.summarize(), item.briefly())
}

不过在极端情况下的话,满足的 trait / protocl 很多时,还是会让人眼花,像下面这样

fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32

因此 Rust 还提供了另一种使用 where 的写法

fn some_function<T, U>(t: T, u: U) -> i32
        where T: Display + Clone,
              U: Clone + Debug
{
    // ...
}

Swift 也支持这样的 where 写法~

protocol Summary {
    func summarize() -> String
}

protocol Briefly {
    func briefly() -> String
}

func notify<T>(item: T) 
    where T: Summary & Briefly
{
    print("Breaking News:", item.summarize(), item.briefly())
}

对于返回值来说,当然也可以是只关心是否 impl / 满足 了某个 Trait / protocol

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

fn returns_summarizable() -> impl Summary {
    Tweet {
        username: String::from("BlueCocoa"),
        content: String::from("learning rust..."),
        reply: false,
        retweet: false,
    }
}

fn main() {
    let tweet = returns_summarizable();
    println!("{}", tweet.summarize());
}

再次对比 Objective-C 的代码

@protocol Summary
@required
- (NSString *) summarize;
@end

@interface Tweet : NSObject <Summary>
@property (nonatomic) NSString * username;
@property (nonatomic) NSString * content;
@property (nonatomic) bool reply;
@property (nonatomic) bool retweet;
@end

@implementation Tweet
- (NSString *) summarize {
    return [NSString stringWithFormat:@"%@: %@", self.username, self.content];
}
@end

id<Summary> returns_summarizable {
    Tweet * tweet = [[Tweet alloc] init];
    [tweet setUsername:@"BlueCocoa"];
    [tweet setContent:@"from ObjC"];
    [tweet setReply:true];
    [tweet setRetweet:false];
    return tweet;
}

int main() {
    id<Summary> tweet = returns_summarizable();
    NSLog(@"%@", [tweet summarize]);
}

不过~与 Objective-C 不同的是,Rust 虽然可以只关心是否 impl 了某个 Trait,但是一个函数实际上的返回类型只能有一种,不像 Objective-C 那样可以自由返回不同 class 的指针

也就是说,如下的代码编译是不行的~

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

pub struct Another {
    pub username: String,
    pub content: String,
}

impl Summary for Another {
    fn summarize(&self) -> String {
        format!("From Another {}: {}", self.username, self.content)
    }
}

fn returns_summarizable(toggle: bool) -> impl Summary {
    if toggle {
        Tweet {
            username: String::from("BlueCocoa"),
            content: String::from("learning rust..."),
            reply: false,
            retweet: false,
        }
    } else {
        Another {
            username: String::from("BlueCocoa"),
            content: String::from("learning rust..."),
        }
    }
}

fn main() {
    let id = returns_summarizable(true);
    println!("{}", id.summarize());
}

而 Objective-C 的话,就是随心所欲了23333

@protocol Summary
@required
- (NSString *) summarize;
@end

@interface Tweet : NSObject <Summary>
@property (nonatomic) NSString * username;
@property (nonatomic) NSString * content;
@property (nonatomic) bool reply;
@property (nonatomic) bool retweet;
@end

@implementation Tweet
- (NSString *) summarize {
    return [NSString stringWithFormat:@"%@: %@", self.username, self.content];
}
@end

@interface Another : NSObject <Summary>
@property (nonatomic) NSString * username;
@property (nonatomic) NSString * content;
@end

@implementation Another
- (NSString *) summarize {
    return [NSString stringWithFormat:@"From Another %@: %@", self.username, self.content];
}
@end

id<Summary> returns_summarizable(BOOL toggle) {
    if (toggle) {
        Tweet * tweet = [[Tweet alloc] init];
        tweet.username = @"BlueCocoa";
        tweet.content = @"from ObjC";
        tweet.reply = true;
        tweet.retweet = false;
        return tweet;
    } else {
        Another * another = [[Another alloc] init];
        another.username = @"BlueCocoa";
        another.content = @"from ObjC";
        return another;
    }
}

int main() {
    id<Summary> tweet = returns_summarizable(true);
    NSLog(@"%@", tweet.summarize);
    
    id<Summary> another = returns_summarizable(false);
    NSLog(@"%@", another.summarize);
}

ObjC 的输出则是~

2019-10-01 01:17:20.700 main[92398:3530622] BlueCocoa: from ObjC
2019-10-01 01:17:20.701 main[92398:3530622] From Another BlueCocoa: from ObjC

声明: 本文为0xBBC原创, 转载注明出处喵~

Leave a Reply

Your email address will not be published. Required fields are marked *