面向对象我们都很熟悉,可以说它是一种软件开发最重要的编程范式之一,它将程序中的数据和操作数据的方法组织成对象。面向对象有几个重要特性: 封装、继承和多态,基于这些特性带来了在可重用性、可维护性、扩展性、可靠性的优点。 Java提供了一套完整的实现,那么在Rust中如何面向对象编程呢?
别着急,在回答这个问题之前,我们先画个5分钟,学习几个基本概念,答案就呼之欲出了。
结构体定义
在Rust中,通过使用 struct
关键字来定义结构体。结构体定义了一个包含多个字段的数据结构,并可以为结构体实现方法。每个字段都有自己的类型和名称。
// 定义一个Person结构体
struct Person {
name: String,
age: u32,
}
结构体创建
可以使用结构体的名称和字段的值来创建结构体。有两种常见的创建方式:
// 直接初始化结构体
let person = Person {
name: String::from("Alice"),
age: 30,
};
impl Person {
// 定义一个新的Person实例的构造函数
fn new(name: String, age: u32) -> Person {
Person { name, age }
}
}
// 使用构造函数创建结构体实例
let person = Person::new(String::from("Alice"), 30);
访问结构体字段:
可以使用点操作符( .
)来访问结构体中的字段。通过结构体实例的名称后跟字段名称,即可访问该字段的值。
// 访问结构体字段
println!("Name: {}", person.name);
println!("Age: {}", person.age);
修改结构体字段:
在Rust中,结构体的字段默认是不可变的(immutable)。如果想要修改结构体的字段,可以使用 mut
关键字将结构体实例声明为可变的(mutable),然后通过点操作符来修改字段的值。
// 修改结构体字段
let mut mutable_person = Person::new(String::from("Bob"), 25);
mutable_person.age = 26;
println!("Modified Age: {}", mutable_person.age);
在上述代码中,我们首先定义了一个名为Person的结构体,它有两个字段:name和age。然后,我们为Person结构体实现了一个构造函数 new
,用于创建新的Person实例。
在 main
函数中,我们通过直接初始化的方式实例化了一个Person结构体,将姓名设置为"Alice",年龄设置为30。然后,我们使用点操作符来访问结构体字段,并打印出姓名和年龄。
接下来,我们使用构造函数 Person::new
创建了一个可变的Person结构体实例 mutable_person
,将姓名设置为"Bob",年龄设置为25。然后,通过将实例声明为可变的,并使用点操作符来修改年龄字段的值为26,并打印出修改后的年龄。
ok,我们掌握了rust的对象定义方法,但是rust除了上述方式,还提供了一些struct的增强,学习了他们可以帮助更简洁的使用struct。
Rust中的元组结构体是一种特殊类型的结构体,它类似于元组,但具有命名的字段。元组结构体允许我们在结构体中组合不同类型的值,并为每个字段指定一个名称。直接看示例:
// 定义一个元组结构体
struct Person(String, u32);
fn main() {
// 创建一个Person实例
let person = Person("Alice".to_string(), 25);
// 访问元组结构体的字段
let name = person.0;
let age = person.1;
println!("Name: {}", name);
println!("Age: {}", age);
}
在上述代码中,我们定义了一个名为Person的元组结构体。它有两个字段:一个是String类型的name字段,另一个是u32类型的age字段。
在 main
函数中,我们创建了一个Person实例 person
,并为name字段赋值为"Alice",age字段赋值为25。
然后,我们通过使用索引来访问元组结构体的字段。元组结构体的字段可以使用 .
加上索引的方式进行访问,索引从0开始。在这个例子中,我们通过 person.0
访问了name字段,通过 person.1
访问了age字段。
最后,我们打印出name和age字段的值。
通过元组结构体,我们可以将不同类型的值组合在一起,并为每个字段指定一个名称,提高了代码的可读性和可维护性。
单元结构体是一种特殊类型的结构体,它不包含任何字段。它类似于C语言中的空结构体或者其他语言中的 unit
类型。单元结构体在Rust中常用于表示没有实际数据的情况,通常用作占位符或者标记类型。下面是一个示例代码来介绍单元结构体的使用:
// 定义一个单元结构体
struct EmptyStruct;
fn main() {
// 创建一个单元结构体实例
let empty = EmptyStruct;
// 访问单元结构体(无操作)
// 打印单元结构体
println!("{:?}", empty);
}
在上述代码中,我们定义了一个名为EmptyStruct的单元结构体。它没有任何字段。
在 main
函数中,我们创建了一个EmptyStruct实例 empty
,并没有对它进行任何操作。
最后,我们通过使用 println!
宏来打印出单元结构体 empty
。
单元结构体在Rust中用于表示没有实际数据的情况,通常用作占位符或者标记类型。例如,在某些情况下,我们可能只关心某个类型是否存在,而不关心它的具体值。这时,可以使用单元结构体来表示这种情况。
细心的朋友肯定注意到了,上面的例子里有一个新知识点:impl
,这个是实现?我们下面就学习它
impl
是Rust中的关键字,用于为类型实现方法。通过 impl
关键字,我们可以在特定类型上定义和实现方法。下面给出一个示例代码来介绍 impl
的使用:
// 定义一个名为Rectangle的结构体
struct Rectangle {
width: u32,
height: u32,
}
// 为Rectangle结构体实现一个名为area的方法
impl Rectangle {
// 定义area方法,用于计算矩形的面积
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
// 创建一个Rectangle实例
let rect = Rectangle {
width: 10,
height: 20,
};
// 调用Rectangle实例的area方法
let area = rect.area();
println!("Area: {}", area);
}
在上述代码中,我们首先定义了一个名为Rectangle的结构体,它有两个字段:width和height。然后,使用 impl
关键字为Rectangle结构体实现了一个名为area的方法。
在 impl Rectangle
块中,我们定义了一个area方法,用于计算矩形的面积。该方法接受一个 &self
参数,表示对Rectangle实例的借用。在方法体内,我们使用 self.width
和 self.height
来访问结构体的字段,并返回它们的乘积作为矩形的面积。
在 main
函数中,我们创建了一个Rectangle实例 rect
,并调用了它的area方法来计算矩形的面积。最后,我们打印出计算得到的面积。
通过 impl
关键字,我们可以在特定类型上定义和实现方法,使得代码更加模块化和可读性更高。
Rust中的枚举(Enum)是一种自定义数据类型,它允许我们将相关的值组合在一起,并为这些值定义一个公共的类型。枚举在Rust中非常常见,并且被广泛用于表示多个可能的取值。
下面使用颜色作为一个例子来说明枚举的使用。假设我们需要表示一组不同的颜色,可以使用枚举来定义这些颜色的类型。
enum Color {
Red,
Green,
Blue,
}
fn main() {
let favorite_color = Color::Blue;
match favorite_color {
Color::Red => println!("I like red!"),
Color::Green => println!("I like green!"),
Color::Blue => println!("I like blue!"),
}
}
在上述代码中,我们定义了一个名为Color的枚举类型,它有三个可能的取值:Red、Green和Blue。然后,在main函数中,我们创建了一个favorite_color变量,并将其设置为Color::Blue。接下来,我们使用match表达式来匹配favorite_color的取值,并根据不同的情况打印不同的消息。
通过枚举,我们可以将相关的值组合在一起,并为这些值定义一个公共的类型。这使得代码更加清晰和可读,同时也提供了更好的类型安全性。在实际开发中,枚举在表示状态、选项、错误类型等方面都非常有用。
Rust中的trait可以类比java的implement,它是一种用于定义共享行为的机制。Trait可以看作是一组方法的抽象,它定义了一系列的方法签名,但没有提供默认的实现。通过实现trait,类型可以拥有这些方法,并共享相同的行为。
定义trait:
使用 trait
关键字可以定义一个trait,后面跟着trait的名称和方法签名。例如:
trait Printable {
fn print(&self);
}
实现trait:
使用 impl
关键字可以为类型实现一个trait。在实现trait时,需要提供trait中定义的所有方法的具体实现。例如:
struct Person {
name: String,
}
impl Printable for Person {
fn print(&self) {
println!("Name: {}", self.name);
}
}
默认实现:
可以为trait中的方法提供默认的实现。这样,在实现trait时,如果不重写该方法,将会使用默认的实现。例如:
trait Printable {
fn print(&self) {
println!("Printing...");
}
}
trait约束:
可以在函数或方法中使用trait约束,以指定参数必须实现某个trait。这样可以在函数内部使用trait中定义的方法。例如:
fn print_person<T: Printable>(person: T) {
person.print();
}
多个trait约束:
可以使用 +
运算符将多个trait约束组合在一起。这样,参数必须同时实现这些trait。例如:
fn process<T: Printable + Clone>(data: T) {
data.print();
let cloned_data = data.clone();
// ...
}
trait继承:
trait可以继承其他trait,从而扩展或组合行为。使用 :
符号可以指定一个trait继承另一个trait。例如:
trait Printable {
fn print(&self);
}
trait Debuggable: Printable {
fn debug(&self);
}
Trait是Rust中非常强大的特性,它提供了一种灵活的方式来实现共享行为,并在编译时进行静态检查。通过trait,可以实现代码的重用性和可扩展性。
Rust是一种支持泛型编程的静态类型编程语言。泛型是一种编程技术,允许在编写代码时使用参数化类型,从而增加代码的灵活性和重用性。以下是Rust中泛型的一些特点和用法,并提供具体的例子:
来定义泛型类型。例如, Vec
表示一个可以存储任意类型元素的动态数组。let numbers: Vec<i32> = vec![1, 2, 3, 4, 5];
let names: Vec<String> = vec!["Alice".to_string(), "Bob".to_string()];
fn foo(x: T) -> T
表示一个接受任意类型参数并返回相同类型的函数。fn print_value<T>(value: T) {
println!("Value: {}", value);
}
print_value(42);
print_value("Hello");
struct Point { x: T, y: T }
表示一个具有泛型字段的结构体。struct Point<T> {
x: T,
y: T,
}
let int_point: Point<i32> = Point { x: 10, y: 20 };
let float_point: Point<f64> = Point { x: 1.5, y: 2.5 };
impl Point { fn get_x(&self) -> &T }
表示一个具有泛型方法的结构体。impl<T> Point<T> {
fn get_x(&self) -> &T {
&self.x
}
}
let int_point: Point<i32> = Point { x: 10, y: 20 };
println!("X coordinate: {}", int_point.get_x());
trait Iterator- { fn next(&mut self) -> Option
}
表示一个具有泛型关联类型和方法的trait。trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
struct Counter {
current: u32,
max: u32,
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.current < self.max {
let value = self.current;
self.current += 1;
Some(value)
} else {
None
}
}
}
let mut counter = Counter { current: 0, max: 5 };
while let Some(value) = counter.next() {
println!("Counter: {}", value);
}
fn foo(x: T) { println!("{}", x) }
表示一个接受实现了 Display
trait的泛型参数的函数。use std::fmt::Display;
fn print_value<T: Display>(value: T) {
println!("Value: {}", value);
}
print_value(42);
print_value("Hello");
通过泛型,Rust允许我们编写通用的代码,适用于多种类型,并在编译时进行静态检查,从而提高代码的灵活性和重用性。
OK,fine!到此我们学完了rust关于结构体的基础知识,那么怎么面向对象呢?
Rust是一门多范式的编程语言,它支持面向对象编程(OOP)的概念,包括封装、继承和多态。下面我将用一个例子来说明如何在Rust中实现面向对象的代码。
假设我们要创建一个图形库,其中包含不同类型的图形对象,比如矩形(Rectangle)和圆形(Circle)。我们可以使用结构体(struct)和特征(trait)来实现封装、继承和多态。
首先,我们定义一个 Shape
特征,用于表示图形对象的共同行为:
trait Shape {
fn area(&self) -> f64;
fn display(&self);
}
在这个特征中,我们定义了两个方法: area
用于计算图形对象的面积, display
用于显示图形对象的信息。
接下来,我们实现 Rectangle
结构体,并为其实现 Shape
特征:
struct Rectangle {
width: f64,
height: f64,
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
fn display(&self) {
println!("Rectangle: width={}, height={}", self.width, self.height);
}
}
在这个实现中,我们为 Rectangle
结构体实现了 Shape
特征的方法。通过实现 area
方法,我们可以计算矩形的面积;通过实现 display
方法,我们可以打印矩形的信息。
类似地,我们可以实现 Circle
结构体,并为其实现 Shape
特征:
struct Circle {
radius: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
fn display(&self) {
println!("Circle: radius={}", self.radius);
}
}
在这个实现中,我们为 Circle
结构体实现了 Shape
特征的方法。通过实现 area
方法,我们可以计算圆形的面积;通过实现 display
方法,我们可以打印圆形的信息。
最后,我们可以在主函数中使用这些图形对象,并调用它们的方法:
fn main() {
let rectangle = Rectangle { width: 5.0, height: 3.0 };
let circle = Circle { radius: 2.0 };
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(rectangle),
Box::new(circle),
];
for shape in shapes {
shape.display();
println!("Area: {}", shape.area());
}
}
在这个例子中,我们创建了一个包含矩形和圆形对象的 shapes
向量,并使用 Box
来存储不同类型的图形对象。然后,我们遍历 shapes
向量,调用每个图形对象的 display
和 area
方法。
通过这个例子,我们可以看到,在Rust中通过结构体和特征的组合,我们可以实现封装(将数据和行为封装在结构体中)、继承(通过实现特征来共享行为)和多态(通过使用 Box
来存储不同类型的对象)等面向对象的概念。是不是很简单,你学废了吗?
同样的留一个课后作业:用面向对象的方式完成题目:551. 学生出勤记录 I
,任何疑问评论区交流