如何在Rust中使用通用植入块(附代码)

84 阅读6分钟

Rust泛型实现块使实现泛型的方法变得更加容易。

在这篇文章中,我们将通过了解没有泛型的世界的繁琐性,以及使用泛型时的结果,来说明泛型实现块的价值。

在这篇文章中,要先跳过:

Rust的结构和impl

与C++、JavaScript和C#等语言不同,在Rust中,你没有对象(属性和方法组)或类来定义对象的类型。

相反,Rust和Go一样,选择了只是属性组的结构。在Rust中,方法是通过使用实现添加到结构中的,而impl 块是用来定义方法的。请参考下面的例子:

// Struct Definition
struct Person {
    name: String
}

// Impl block defining method
impl Person {
  fn say_name(&self){
    println!("{}", self.name);
  }
}


fn main() {
    // Instantiate a person
    let alex = Person {name: "Alex Merced".to_string()};

    // Call methods
    alex.say_name();
}

在上面的代码中,我们创建了一个Person 结构,有一个名为nameString 属性。然后,我们使用一个impl 块来定义和实现一个say_name 方法。在主函数中,我们创建了一个Person 结构并调用了say_name() 方法,该方法将打印Alex Merced

通用类型

有时,我们的结构可能有一些属性,其类型的定义是不同的。为了指定这一点,我们可以将未知的类型列为变量,如<T><T, U> ,这取决于我们需要指定多少个变量类型。

然后,我们需要为每个我们想要支持的类型实现任何方法。如果我们要为许多类型实现一个方法,这可能是非常繁琐的:

// Generic Struct Definition, T is a variable for unknown type
struct Stats<T> {
    age: T,
    height:T
}

// Impl blocks defining methods for different types
impl Stats<i32> {
  fn print_stats(&self){
    println!("Age is {} years and Height is {} inches", self.age, self.height);
  }
}

impl Stats<f32> {
  fn print_stats(&self){
    println!("Age is {} years and Height is {} feet", self.age, self.height);
  }
}


fn main() {
    // Instantiate using i32 stats
    let alex = Stats {age: 37, height: 70};
    // Instantiate using f32 stats
    let alex2 = Stats {age: 37.0, height: 5.83};
    // Call methods
    alex.print_stats();
    alex2.print_stats();
}

在上面的示例代码中,我们定义了一个Stats 结构,它有两个相同类型的属性,用T 来表示。因为我们在编写<T> 时将其定义为通用类型,所以这个类型可以是任何东西,但它对年龄和身高必须是相同的。

我们希望任何使用两个32位数的Stats 对象,无论是整数还是浮点数,都有一个print_stats 方法。在这种情况下,我们必须为每种可能性创建两个实现块。

然后我们实例化两个Stats 结构来使用i32 值和f32 值。我们为结构调用print_stats 方法,得到以下输出:

Age is 37 years and Height is 70 inches
Age is 37 years and Height is 5.83 feet

注意不同的输出,因为我们从正确的实现块中调用了实现。

通用的实现块

为每种类型编写单独的实现块可能会很繁琐,尤其是在有多种可能性的情况下。

如果几种类型的实现都是相同的,我们可以使用通用实现块,它和通用类型一样,允许我们定义代表该类型的变量:

use std::fmt::Display;

// Generic Struct Definition, T is a variable for unknown type
struct Stats<T> {
    age: T,
    height:T
}

// Impl blocks defining methods for different types
impl<T:Display> Stats<T> {
  fn print_stats(&self){
    println!("Age is {} years and Height is {}", self.age, self.height);
  }
}


fn main() {
    // Instantiate using i32 stats
    let alex = Stats {age: 37, height: 70};
    // Instantiate using f32 stats
    let alex2 = Stats {age: 37.0, height: 5.83};
    // Instantiate using String stats
    let alex3 = Stats {age: "37".to_string(), height: "5'10ft".to_string()};
    // Call methods
    alex.print_stats();
    alex2.print_stats();
    alex3.print_stats();
}

在上面的代码中,我们从标准库中导入了Display 特质,因为我们以后需要引用它。

我们将Stats 结构定义为一个具有两个相同泛型的属性的结构,分别是年龄和身高。

我们只用了一个实现块来定义<T:Display> 的通用类型,这意味着T 是通用类型的变量。:Display 表示它必须是实现Display 特质的类型。这是必须的,这样我们才能在这个实现中使用println! 这个宏。

然后我们定义了三个结构:一个使用i32 ,另一个使用f32 ,最后一个使用字符串作为其属性值。(我们使用了三种不同的类型,只需要一个impl 块。这多酷啊!)

当它运行时,你应该得到下面的输出:

Age is 37 years and Height is 70
Age is 37 years and Height is 5.83
Age is 37 years and Height is 5'10ft

用通用的impl 块来实现火车

当我们需要在许多类型上实现一个特性时,通用的impl 块也会有帮助。在这种情况下,我们可以使用一个通用的实现来节省时间,然后根据需要定义具体的实现。你可以在下面的代码中看到这一点:

use std::fmt::Display;

// Generic Struct Definition, T is a variable for unknown type
struct Stats<T> {
    age: T,
    height:T
}

// Generic Tuple Struct that holds one value
struct Number<T> (T);

// trait with default implementation of print_stats method
trait ViewStats {
  fn print_stats(&self){
    println!("The type of age and height doesn't implement the display trait")
  }
}

// Impl blocks defining methods for different types
impl<T:Display> ViewStats for Stats<T> {
  fn print_stats(&self){
    println!("Age is {} years and Height is {}", self.age, self.height);
  }
}

//Impl block to ViewStats trait to number but use default implementation
impl<T> ViewStats for Stats<Number<T>> {}


fn main() {
    // Instantiate using i32 stats
    let alex = Stats {age: 37, height: 70};
    // Instantiate using f32 stats
    let alex2 = Stats {age: 37.0, height: 5.83};
    // Instantiate using String stats
    let alex3 = Stats {age: "37".to_string(), height: "5'10ft".to_string()};
    // Instantiate using String stats
    let alex4 = Stats {age: Number(37), height: Number(70)};
    // Call methods
    alex.print_stats();
    alex2.print_stats();
    alex3.print_stats();
    alex4.print_stats();
}

在上面的代码中,我们从标准库中导入了Display 特质。我们将Stats 结构定义为具有任何类型的ageheight 的属性的结构,除了匹配类型。

然后,我们定义了一个Number 结构,它是一个有一个值的元组。这只是为了让我们演示当我们用一个没有实现显示的类型创建统计时,会发生什么。

接下来,我们定义了一个ViewStats 特质,其中的print_stats 方法被赋予了一个默认的实现。如果ageheight 的值是有效的类型,但没有自己的实现,就会调用这个实现。

然后,我们为Stats<T> 定义了一个ViewStats 的实现,其中T 是任何实现了Display 特质的类型,比如i32,f32, 和String

然后,我们为Number 实现了ViewStats 。因为我们没有定义print_stats 的新版本,它将使用特质声明块中的默认版本。

然后我们创建了四个结构。前三个与之前相同,第四个是Stats 结构,其中年龄和身高由Number 结构表示。

当我们运行该脚本时,我们得到了以下输出:

Age is 37 years and Height is 70
Age is 37 years and Height is 5.83
Age is 37 years and Height is 5'10ft
The type of age and height doesn't implement the display trait

请注意,最后一行显示的是我们在火车块中默认实现的结果,因为Number<T> 没有实现显示,所以它不能使用与前三个实例相同的实现Stats<T>

结论

泛型可以简化编写应该与许多类型协同工作的代码,无论是使用泛型来定义属性类型,还是使用泛型impl 块来定义方法,都不需要反复进行繁琐的操作。