English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
Generics are an indispensable mechanism in a programming language.
C++ Generics are implemented using "templates" in the language, while C language does not have the mechanism of generics, which also makes it difficult to build complex type projects in C language.
The generic mechanism is a programming language used to express type abstraction, generally used for classes with determined functions and undetermined data types, such as linked lists, hash tables, etc.
This is a method for selecting sorting of integer numbers:
fn max(array: &[i32]) -> i32 {}} let mut max_index = 0; let mut i = 1; while i < array.len() { if array[i] > array[max_index] { max_index = i; } i += 1; } array[max_index] } fn main() { let a = [2, 4, 6, 3, 1]; println!("max = {}", max(&a)); }
Running Result:
max = 6
This is a simple program to find the maximum value, which can be used to process i32 Numeric type data, but cannot be used for f64 Type data. By using generics, we can make this function applicable to various types. However, not all data types can be compared, so the next piece of code is not for execution but to describe the syntax format of the function generic:
fn max<T>(array: &[T]) -> T { let mut max_index = 0; let mut i = 1; while i < array.len() { if array[i] > array[max_index] { max_index = i; } i += 1; } array[max_index] }
In the Option and Result enum classes we learned before are generic.
Structures and enum classes in Rust can implement the generic mechanism.
struct Point<T> { x: T, y: T }
This is a point coordinate structure, T represents the numeric type describing the point coordinates. We can use it like this:
let p1 = Point { x: 1, y: 2}; let p2 = Point { x: 1.0, y: 2.0};
When used, the type is not declared, and here the automatic type mechanism is used, but it is not allowed to appear type mismatch situations such as:
let p = Point { x: 1, y: 2.0};
x and 1 when bound, T has already been set to i32, so it is not allowed to appear f64 type. If we want x and y to be represented by different data types, we can use two generic identifiers:
struct Point<T1, T2> { x: T1, y: T2 }
Methods representing generics in enum classes such as Option and Result:
enum Option<T> { Some(T), None, } enum Result<T, E> { Ok(T), Err(E), }
Both structures and enum classes can define methods, so methods should also implement the generic mechanism, otherwise the generic class will not be effectively operated by methods.
struct Point<T> { x: T, y: T, } impl<T> Point<T> { fn x(&self) -> &T { &self.x } } fn main() { let p = Point { x: 1, y: 2 }; println!("p.x = {}", p.x()); }
Running Result:
p.x = 1
Note that there must be <T> after the impl keyword because T after it is modeled after it. But we can also add a method to one of the generics:
impl Point<f64> { fn x(&self) -> f64 {}} self.x } }
The generic type of the block itself does not hinder the generic capability of its internal methods:
impl<T, U> Point<T, U> { fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> { Point { x: self.x, y: other.y, } } }
The method mixup merges the x of a Point<T, U> point with the y of a Point<V, W> point into a new point of type Point<T, W>.
The concept of a feature (trait) is similar to that of an interface (Interface) in Java, but they are not completely the same. Like interfaces, features are a kind of behavior specification that can be used to identify which classes have which methods.
In Rust, features are represented by the trait keyword:
trait Descriptive { fn describe(&self) -> String; }
Descriptive specifies that the implementor must have a describe(&self) -> String method.
We use it to implement a struct:
struct Person { name: String, age: u8 } impl Descriptive for Person { fn describe(&self) - format!("{} {} } }
The format is:
impl <feature name> for <type name implemented>
In Rust, the same class can implement multiple features, and each impl block can only implement one.
This is the difference between features and interfaces: interfaces can only specify methods but cannot define methods, while features can define methods as default methods. Since they are "default", objects can either redefine methods or use the default methods without redefining them:
trait Descriptive { fn describe(&self) - String::from("[Object]") } } struct Person { name: String, age: u8 } impl Descriptive for Person { fn describe(&self) - format!("{} {} } } fn main() { let cali = Person { name: String::from("Cali"), age: 24 }; println!("{}", cali.describe()); }
Running Result:
Cali 24
If we remove the content of the impl Descriptive for Person block, the running result will be:
[Object]
In many cases, we need to pass a function as a parameter, such as callback functions, setting button events, etc. In Java, functions must be passed in as an instance of a class that implements an interface, while in Rust, this can be achieved by passing feature parameters:
fn output(object: impl Descriptive) { println!("{}", object.describe()); }
Any object that has implemented the Descriptive feature can be used as a parameter for this function. The function does not need to know whether the object passed in has other properties or methods; it only needs to know that the object has the method specifications of the Descriptive feature. Of course, other properties and methods cannot be used within this function either.
Trait parameters can also be implemented using this equivalent syntax:}}
fn output<T: Descriptive>(object: T) { println!("{}", object.describe()); }
This is a grammar sugar with a generic style, which is very practical when there are multiple parameter types that are traits:
fn output_two<T: Descriptive>(arg1: T, arg2: T) { println!("{}", arg1.describe()); println!("{}", arg2.describe()); }
When a trait is used as a type representation and involves multiple traits, you can use + Symbol representation, such as:
fn notify(item: impl Summary + Display) fn notify<T: Summary + Display>(item: T)
Note:When used only to represent types, it does not mean that it can be used in the 'impl' block.
Complex implementation relationships can be simplified using the 'where' keyword, for example:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U)
This can be simplified to:
fn some_function<T, U>(t: T, u: U) -> i32 where T: Display + Clone, U: Clone + Debug
After understanding this grammar, the 'Find Maximum' case in the generic chapter can be truly implemented:
trait Comparable { fn compare(&self, object: &Self) -> i8; } fn max<T: Comparable>(array: &[T]) -> &T { let mut max_index = 0; let mut i = 1; while i < array.len() { if array[i].compare(&array[max_index]) > 0 { max_index = i; } i += 1; } &array[max_index] } impl Comparable for f64 {}} fn compare(&self, object: &f64) -> i8 {}} if &self > &object { 1 } else if &self == &object { 0 } else { -1 } } } fn main() { let arr = [1.0, 3.0, 5.0, 4.0, 2.0]; println!("maximum of arr is {}", max(&arr)); }
Running Result:
maximum of arr is 5
Tip: Since the second parameter of the compare function must be the same as the type that implements the trait, the Self (note the case) keyword represents the current type itself (not the example).
The format of the trait return value is as follows:
fn person() -> impl Descriptive { Person { name: String::from("Cali"), age: 24 } }
However, there is a point, the trait return value only accepts objects that have implemented the trait as return values, and all possible return value types in the same function must be completely the same. For example, the structures A and B both implement the Trait, and the following function is incorrect:
fn some_function(bool bl) -> impl Descriptive { if bl { return A {}; } else { return B {}; } }
The impl feature is very powerful, we can use it to implement methods of classes. But for generic classes, sometimes we need to distinguish between the methods implemented by the generic type to decide which method to implement next:
struct A<T> {} impl<T: B + C> A<T> { fn d(&self) {} }
This code declares that the A<T> type must be implemented effectively under the premise that T has already implemented B and C traits.