Rust generics and characteristics
Generics are an indispensable mechanism for programming languages.
Templates are used to implement generics in C++ language, but there is no mechanism of generics in C language, which makes it difficult for C languageto build complex projects.
Generic mechanism is a mechanism used by programming languages to express type abstraction, which is generally used for classes with functions determined and data types to be determined, such as linked lists, mapping lists, and so on.
Define generics in a function
This is a method of selecting and sorting integer numbers:
Example
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 maximum program that can be used to process data of the i32digital type, but not for data of the f64 type. By using generics, we can make this function available to all types. But in fact, not all data types can be compared to size, so the next piece of code is not used to run, but to describe the syntax format of function generics:
Example
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]
}
Structures and generics in enumerated classes
The Option and Result enumeration classes we learned earlier are generic.
Both structures and enumerated classes in Rust can implement generic mechanisms.
struct Point<T> {
x: T,
y: T
}
This is a point coordinate structure, and T represents the numerical type that describes 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};
The type is not declared when it is used. The automatic type mechanism is used here, but type mismatches are not allowed as follows:
let p = Point {x: 1, y: 2.0};
When x is bound to 1, T is set to i32, so the type of f64 is no longer allowed. 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 that represent generics in enumerated classes such as Option and Result:
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
Both structures and enumerated classes can define methods, so methods should also implement generic mechanisms, otherwise generic classes will not be effectively manipulated by methods.
Example
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
Be careful, impl
keyword must be followed by <T>
because the T behind it follows its example. But we can also add methods to one of these generics:
impl Point<f64> {
fn x(&self) -> f64 {
self.x
}
}
The generics of the impl
block itself do not hinder the ability of its internal methods to be generic:
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,
}
}
}
Method mixup
put a Point<T, U>
the x and the x of the point Point<V, W>
. The y of the point is merged into a type of Point<T, W>
,it’s new.
Characteristics
The concept of trait is close to the Interface in Java, but the two are not exactly the same. Features are similar to interfaces in that they are behavioral specifications that can be used to identify which classes have which methods.
Features are used in Rust trait
indicates:
trait Descriptive {
fn describe(&self) -> String;
}
Descriptive
is stipulated that the implementer must have describe(&self) -> String
method.
We use it to implement a structure:
Example
struct Person {
name: String,
age: u8
}
impl Descriptive for Person {
fn describe(&self) -> String {
format!("{} {}", self.name, self.age)
}
}
The format is:
Impl<attribute name>for<implemented type name>
Rust can implement multiple features of the same class, each impl
only one block can be implemented.
Default Properties
This is the difference between a property and an interface: an interface canonly standardize a method, not define a method, but a property can define amethod as the default method, and because it is “default”, the object canredefine the method or not redefine the method using the default method:
Example
trait Descriptive {
fn describe(&self) -> String {
String::from("[Object]")
}
}
struct Person {
name: String,
age: u8
}
impl Descriptive for Person {
fn describe(&self) -> String {
format!("{} {}", self.name, self.age)
}
}
fn main() {
let cali = Person {
name: String::from("Cali"),
age: 24
};
println!("{}", cali.describe());
}
Running result:
Cali 24
If we put impl Descriptive for Person
, if the content in the block is removed, the running result is:
[Object]
Characteristics as parameters
In many cases, we need to pass a function as an argument, such as a callbackfunction, setting button events, and so on. In Java, the function must be passed as an instance of the class implemented by the interface, and it can be achieved by passing property parameters in Rust:
fn output(object: impl Descriptive) {
println!("{}", object.describe());
}
Anything that comes true Descriptive
, the object of the property can be used as an argument to this function. It is not necessary for this function to know whether there are any other properties or methods in the incoming object, just know that it must have Descriptive
. The method of feature specification is fine. Of course, other properties and methods cannot be used within this function.
Property parameters can also be implemented using this equivalent syntax:
fn output<T: Descriptive>(object: T) {
println!("{}", object.describe());
}
This is a syntactic sugar with a style similar to generics, which is useful when multiple parameter types are properties:
fn output_two<T: Descriptive>(arg1: T, arg2: T) {
println!("{}", arg1.describe());
println!("{}", arg2.describe());
}
If multiple properties are involved in the type representation of a property, it can be represented by a + symbol, for example:
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 used where
keyword simplification, for example:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U)
Can be simplified to:
fn some_function<T, U>(t: T, u: U) -> i32
where T: Display + Clone,
U: Clone + Debug
After understanding this syntax, the “take the maximum” case in the generic chapter can be really implemented:
Example
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: due to the need to declare compare
the second argument to the function must be the same as the type that implements the property, so Self
(note case) the keyword represents the current type (not the instance) itself.
Property as the return value
The format of the return value of the property is as follows:
Example
fn person() -> impl Descriptive {
Person {
name: String::from("Cali"),
age: 24
}
}
However, the return value of the property only accepts the object that implements the property as the return value, and all possible return values in the same function must be of exactly the same type. For example, both structure An and structure B implement features. Trait
the following function is wrong:
Example
fn some_function(bool bl) -> impl Descriptive {
if bl {
return A {};
} else {
return B {};
}
}
Conditional realization method
impl
is so powerful that we can use it to implement the methods of the class. But for a generic class, sometimes we need to distinguish betweenthe methods that the generic class to which it belongs has been implementedto determine which method it should implement next:
struct A<T> {}
impl<T: B + C> A<T> {
fn d(&self) {}
}
This code declares A<T>
type can effectively implement this only ifT has already implemented the B and C features with impl
block.