戏说rust二_细节篇

作者: 引线小白-本文永久链接:httpss://www.limoncc.com/post/54839b6bfa303599/
知识共享许可协议: 本博客采用署名-非商业-禁止演绎4.0国际许可证

上篇说道rust的核心机制

1、内存所有制
2、借用规则
3、生命周期规则

本篇就来填充一些关键细节,本文的目的就是呈现这些细节,让这些规则变的显然。需要唠叨的是,静态语言的核心是类型系统(Type System),尤其是rust这种强静态类型,类型系统(Type System)更是核心的核心。rust为了实现编程世界的目标,围绕类型做了很多工作。

一、rust的类型

类型是静态语言的需要了解的核心。但通常学习起来都比较繁琐。rust的类型和其他语言有所不同。考虑到rust的内存所有权交易问题。类型的划分既考虑了类型本身的特点又考虑了所有权的交易特性

1.1、标量类型(Scalar Types )

Rust 有四种主要的基本类型:整数、浮点数、布尔值和字符。rust又叫它们标量类型。其他所有类型都是基于这个四大基本类型。

整数又分为有符号整数 (i8, i16, i32, i64, isize)、 无符号整数 (u8, u16, u32, u64, usize)。它们的能力范围如下:

长度 有符号类型=[$-2^{n - 1}$ , $2^{n - 1} - 1$] 无符号类型=[$0$, $2^{n}-1$]
8 位 i8=[-128,127] u8=[0,255]
16 位 i16=[-32768,32767] u16=[0,65535]
32 位 i32 =[-2147483648,2147483647] u32=[0,4294967295]
64 位 i64=[$-9.2\times 10^{18}$,$9.2\times 10^{18}$] u64=[0,$18.4\times 10^{18}$]
128 位 i128=[$-1.7\times 10^{38}$,$1.7\times 10^{38}$] u128=[0,$3.4\times 10^{38}$]

浮点数根据 IEEE-754 标准表示。 f32 类型是单精度浮点数, f64 类型是双精度。这里就不做过多解释了,提供两个玩耍链接:

1、从十进制浮点到 32 位和 64 位十六进制表示形式
2、从 32 位十六进制表示形式到十进制浮点数

然后提供几个案例

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let a = (0.1_f64 + 0.2 - 0.3).abs();
println!("{a}");
let b = (0.1_f32 + 0.2 - 0.3).abs();
println!("{b}");
let c = (0.123_f32 as f64 - 0.123_f64).abs();
println!("{c}");
}
// 0.00000000000000005551115123125783
// 0
// 0.0000000033974647539736225

布尔值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
let a = false;
let b = true;
let c = a & b;
println!("与a & b==>{c}");
let c = a | b;
println!("或a | b==>{c}");
let c = !a;
println!("非!a==>{c}");
let c = a ^ b;
println!("异或a ^ b==>{c}");
}
// 与a & b==>false
// 或a | b==>true
// 非!a==>true
// 异或a ^ b==>true

字符类型,注意是单引号

1
2
3
4
5
6
7
fn main() {
let a:char = '我';
let b:char = '😍';
let c:char = '你';
println!("{a}{b}{c}")
}
// 我😍你

最后最重要的一点,标量类型所有权交易都是克隆(Clone),因为它们都是使用的栈内存。如果还不熟悉这一点请看第一篇《戏说rust一_入门篇》

1.2、复合类型(Compound Types)

Rust 有两种原始复合类型:元组(tuple)和数组(array)

1.2.1、元组(tuple)

元组(tuple)是由多种类型组合到一起形成的,因此它是复合类型,元组的长度是固定的,元组中元素的顺序也是固定的。

1
2
3
4
5
6
7
fn main() {
let a:(i32,f64,u8) = (520,3.14,1);
let b = a.2;
println!("元组{a:?}的第三位数是{b}")

}
// 元组(520, 3.14, 1)的第三位数是1

如果下面我们来考察一下元组的所有权交易

1
2
3
4
5
6
7
8
9
fn main() {
let a: (i32, f64, u8) = (500, 6.4, 1);
let b = a;
println!("a的内存地址{:p}", &b);
println!("b的内存地址{:p}", &a);

}
// a的内存地址0x16d6e6920
// b的内存地址0x16d6e6910

当元组的元素都是基本类型(四大标量类型)的时候,所有权交易(非借用赋值)都是使用的克隆(Clone)。因为基本类型的所有权交易就是克隆(Clone),下面来点迷之操作,如果元组的元素类型既是标量类型又是非标量类型会怎么样?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
let a: (i32, f64, String) = (500, 6.4, "我爱你".to_string());
println!("a==>{:p}", &a);
println!("a.0==>{:p}", &a.0);
println!("a.1==>{:p}", &a.1);
println!("a.2==>{:p}", a.2.as_ptr());
let b = a;
println!("a==>{:p}", &b);
println!("b.0==>{:p}", &b.0);
println!("b.1==>{:p}", &b.1);
println!("b.2==>{:p}", b.2.as_ptr());
}
// a==>0x16cf926d0
// a.0==>0x16cf926d8
// a.1==>0x16cf926d0
// a.2==>0x600000fec060
// a==>0x16cf92830
// b.0==>0x16cf92838
// b.1==>0x16cf92830
// b.2==>0x600000fec060

发现rust依然追寻了规则,标量类型的所有权交易是克隆(Clone)其他类型的所有权交易是转移(move)。所以a前2个元素是标量类型就克隆,第三元素是字符串类型就是转移。

1.2.2、堆内存交易转移(move)和栈内存交易克隆(clone)的本质

那么这背后的作用机制到底是什么?我们来看一个报错信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
fn main() {
let a: (i32, f64, String) = (500, 6.4, "我爱你".to_string());
println!("a.2==>{:p}", a.2.as_ptr());
let b = a;
println!("b.2==>{:p}", b.2.as_ptr());
println!("{a:?}")
}
error[E0382]: borrow of moved value: `a`
--> src/main.rs:10:15
|
6 | let a: (i32, f64, String) = (500, 6.4, "我爱你".to_string());
| - move occurs because `a` has type `(i32, f64, String)`, which does not implement the
`Copy` trait
7 | println!("a.2==>{:p}", a.2.as_ptr());
8 | let b = a;
| - value moved here
9 | println!("b.2==>{:p}", b.2.as_ptr());
10 | println!("{a:?}")
| ^^^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of
the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
8 | let b = a.clone();
| ++++++++

其中有一段信息:

move occurs because a has type (i32, f64, String), which does not implement the Copy trait
转移发生是因为a的类型(i32, f64, String)没有实现复制特征(Copy trait)

这里出现了一个新概念trait,我们暂且不表。顾名思义即可。我们来把这句话说的人性话点。就是说类型没有复制的特征(或者功能,或者方法),所有权交易就会使用转移move。如果实现了,显然基本类型(四大标量类型)是实现了Copy特征的。于是所有权交易是克隆(Clone)。

回忆一下这个原则:堆内存交易转移(move)栈内存交易克隆(clone)。其背后就是使用栈内存的类型rust都实现了一种叫Copy的trait,自然就是使用克隆(clone);而使用堆内存的类型没有这个特征于是就会触发转移(move)。

1.2.3、数组(array)

拥有多个值集合的另一种方法是使用数组(array)。与元组不同,数组的每个元素都必须具有相同的类型。与其他一些语言中的数组不同,Rust 中的数组具有固定的长度。

1
2
3
4
5
6
7
8
9
fn main() {
let a:[i32;4] =[1,2,3,4];
let b:[i32;4] = a;
println!("a的内存地址{:p}",&a);
println!("b的内存地址{:p}",&b);

}
// a的内存地址0x16b8a6910
// b的内存地址0x16b8a6920

我来看看元素是非基本类型的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let a: [String; 2] = std::array::from_fn(|index| format!("第{index}爱你中国"));
println!("a的内存地址{:p}", &a);
println!("a0的内存地址{:p}", a[0].as_ptr());
println!("a1的内存地址{:p}", a[1].as_ptr());
let b = a;
println!("b的内存地址{:p}", &b);
println!("{b:?}");
println!("b0的内存地址{:p}", b[0].as_ptr());
println!("b1的内存地址{:p}", b[1].as_ptr());
}
// a的内存地址0x16ce8a708
// a0的内存地址0x6000037891c0
// a1的内存地址0x6000037891e0
// b的内存地址0x16ce8a810
// ["第0爱你中国", "第1爱你中国"]
// b0的内存地址0x6000037891c0
// b1的内存地址0x6000037891e0

还是那句老话标量类型的所有权交易是克隆(Clone)其他类型的所有权交易是转移(move);或者说堆内存交易转移(move)栈内存交易克隆(clone);或者说实现了Copy的trait的类型内存交易使用克隆(clone),否则转移(move)

注意非标量类型或者说是非基本类型的转移(move)操作是指的内部变量转移(move)操作。它本身的内存地址是改变了的。

1.3、自定义类型:结构体(struct)和枚举(enum)
1.3.1、结构体(struct)

一个结构体(struct)是这样的:通过关键字 struct 定义名称,内部有字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
struct User {
name: String,
age: u8,
email: String,
address: String,
}
let a = User {
name: "李四".to_string(),
age: 18,
email: "xxx@mail.com".to_string(),
address: "广州天河区".to_string(),
};
println!("{}|{}|{}|{}",a.name,a.age,a.email,a.address)
}
// 李四|18|xxx@mail.com|广州天河区

这个时候你可能疑惑了?为啥不直接println!。可以的,恭喜你又引入了新概念。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
error[E0277]: `User` doesn't implement `Debug`
--> src/main.rs:18:15
|
18 | println!("{a:?}")
| ^^^^^ `User` cannot be formatted using `{:?}`
|
= help: the trait `Debug` is not implemented for `User`
= note: add `#[derive(Debug)]` to `User` or manually `impl Debug for User`
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider annotating `User` with `#[derive(Debug)]`
|
6 + #[derive(Debug)]
7 | struct User {
|

有出现了trait(特征),我们这里暂时不解释。提示说要格式化打印需要有Debug的trait。我才学struct。什么Debug的trait奇奇怪怪的。接着编译器它说可以再你的结构体User上面加#[derive(Debug)]。又懵圈了。derive是派生的意思,那么

#[derive(Debug)]==>翻译是派生Debug这个trait

那#是什么意思。联想一下css里面似乎表示属性。嗯,那么就是

#[derive(Debug)]==>翻译是属性:派生Debug这个trait

没错rust管这个操作叫做Derived Traits(派生特征),通过属性#这个操作添加。至于本质我们暂且不表。形式化理解先。这里再说一个rust属性

#[allow(dead_code)]==>翻译是属性:允许未使用(死亡代码)

也就说,rust中没有使用的变量,rust称为dead_code。形式化理解先。我们继续。有时候,抑制好奇心是我们能够前进的动力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn main() {
#[allow(dead_code)]
#[derive(Debug)]
struct User {
name: String,
age: u8,
email: String,
address: String,
}
let a = User {
name: "李四".to_string(),
age: 18,
email: "xxx@mail.com".to_string(),
address: "广州天河区".to_string(),
};
println!("{a:#?}")
}
// User {
// name: "李四",
// age: 18,
// email: "xxx@mail.com",
// address: "广州天河区",
// }

我们来看看结构体的所有权交易情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
fn main() {
#[allow(dead_code)]
#[derive(Debug)]
struct User {
name: String,
age: u8,
email: String,
address: String,
}
let a = User {
name: "李四".to_string(),
age: 18,
email: "xxx@mail.com".to_string(),
address: "广州天河区".to_string(),
};
println!("a==>{:p}", &a);
println!("a.name==>{:p}", a.name.as_ptr());
println!("a.age==>{:p}", &(a.age));
println!("a.email==>{:p}", a.email.as_ptr());
println!("a.address==>{:p}", a.address.as_ptr());
let b = a;
println!("b==>{:p}", &b);
println!("b.name==>{:p}", b.name.as_ptr());
println!("b.age==>{:p}", &(b.age));
println!("b.email==>{:p}", b.email.as_ptr());
println!("b.address==>{:p}", b.address.as_ptr());
}
// a==>0x16b8fe580
// a.name==>0x600003264060
// a.age==>0x16b8fe5c8
// a.email==>0x600003264070
// a.address==>0x600003264080
// b==>0x16b8fe780
// b.name==>0x600003264060
// b.age==>0x16b8fe7c8
// b.email==>0x600003264070
// b.address==>0x600003264080

依然还是那句老话标量类型的所有权交易是克隆(Clone)其他类型的所有权交易是转移(move);或者说堆内存交易转移(move)栈内存交易克隆(clone);或者说实现了Copy的trait的类型内存交易使用克隆(clone),否则转移(move)

不过我们需要注意的是,非标量类型或者说是非基本类型的转移(move)操作是指的内部变量转移(move)操作。它本身的内存地址是改变了的。

1.3.2、枚举(enum)

枚举(enum)为您提供了一种表示值是一组可能的值之一的方法。和大多数语言定义的差不多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {
#[allow(dead_code)]
#[derive(Debug)]
enum Vehicle {
Car,
Bus,
Truck,
}
let a = Vehicle::Car;
let b = Vehicle::Bus;
println!("{a:?}");
println!("{b:?}");
}
// Car
// Bus

这里主要关注的一个特殊的枚举Option,当然你如果熟悉scala对这个概念应该不陌生。结合模式匹配,用在None值处理上是比较优雅的。如下

1
2
3
4
5
6
7
8
9
fn main() {
let a = Option::from(1);
let b = match a {
Some(x)=>format!("我是{x}哦。"),
None=>format!("我是None哦。")
};
println!("{b}")
}
// 我是1哦。

也就说枚举Option就两个元素Some(x)和None。

1.4、集合类型(collections)
1.4.1、向量(Vector)

向量(Vector)允许您在单个数据结构中存储多个值,它的长度是可变的。该结构将所有值彼此相邻地放在内存中。向量只能存储相同类型的值。向量(Vector)应该被作为一个整体对待,这与前面的元组、数组、结构体、枚举是不一样的。因为向量(Vector)只能被分配到堆内存上,为何?皆因长度可变。而前述类型并不一定,要看里面的具体元素情况。

下面举几个例子,自行体会:

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let a = std::array::from_fn::<String, 2, _>(|index| format!("第{index}爱你中国")).to_vec();
println!("a==>{:p}", a.as_ptr());
println!("a0==>{:p}", a[0].as_ptr());
let b = a;
println!("b==>{:p}", b.as_ptr());
println!("b0==>{:p}", b[0].as_ptr());
}
// a==>0x600000044270
// a0==>0x600000c40060
// b==>0x600000044270
// b0==>0x600000c40060
1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let a = vec![1,2,3,4,5,6,7,8];
println!("a==>{:p}", a.as_ptr());
println!("a0==>{:p}", &(a[0]));
let b = a;
println!("b==>{:p}", b.as_ptr());
println!("b0==>{:p}", &(b[0]));
}
// a==>0x60000165d1c0
// a0==>0x60000165d1c0
// b==>0x60000165d1c0
// b0==>0x60000165d1c0
1.4.2、字符串(String)

字符串到底是什么,直接上源码。StringVec\<u8> 上的包装器。源码又很多通过rust属性的派生操作,暂且忽略。

1
2
3
4
5
6
#[derive(PartialEq, PartialOrd, Eq, Ord)]
#[stable(feature = "rust1", since = "1.0.0")]
#[cfg_attr(not(test), lang = "String")]
pub struct String {
vec: Vec<u8>,
}

对于字符串,关于 UTF-8 的一点是从 Rust 的角度来看,实际上有三种相关的方式来看待字符串:字节(bytes)、标量值(scalar values)和字素簇(grapheme clusters)。

1
2
3
4
5
6
7
8
9
10
11
12
use hex;
fn main() {
let a = "我爱你中国".to_string();
let b = &a.as_bytes();
println!("b长度是{}内容是{b:?}",b.len());
let c = &a.chars();
println!("{c:?}");
println!("{:?}",hex::encode(a.as_bytes()));
}
// b长度是15内容是[230, 136, 145, 231, 136, 177, 228, 189, 160, 228, 184, 173, 229, 155, 189]
// Chars(['我', '爱', '你', '中', '国'])
// "e68891e788b1e4bda0e4b8ade59bbd"

内存所有权就不演示了。其他操作去查API

1.4.3、哈希映射(HashMap)

最后一个常见的集合是哈希映射。类型HashMap使用哈希函数存储类型 K 的键到类型 V 的值的映射,该函数确定如何将这些键和值放入内存。许多编程语言都支持这种数据结构,但它们通常使用不同的名称,例如哈希、映射、对象、哈希表、字典或关联数组,仅举几例演示如何创建哈希映射。注意_表示任意,这一点和scala里面的意思差不多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
use std::collections::HashMap;
let mut my_car = HashMap::new();
my_car.insert("宝马车", 1);
my_car.insert("比亚迪车", 2);
my_car.insert("老爷车", 1);
println!("{:?}", my_car);

let teams_list = vec![
("小白".to_string(), 18),
("小黑".to_string(), 22),
("小美".to_string(), 19),
];

let teams_map: HashMap<_, _> = teams_list.into_iter().collect();
println!("{:?}", teams_map)
}

哈希映射的所有权和复合类型一致,看元素。

二、rust的抽象类型:泛型(generics)和特征(traits)

每种编程语言都有有效处理概念重复的工具。静态语言除了函数,还有泛型(generics)和特征(traits)、类(class)、接口(interface)。由于继承在代码实践中被很多人吐槽。现在一般都用特征(traits)、或者接口(interface)来解决。

2.1、泛型(generics)

我们来看看没有泛型(generics)情况,强类型语言无法解决的问题。异质类型无法一起计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
fn num_add(a: i32, b: i32) -> i32 {
a + b
}
let c = num_add(1.2,2);
println!("{c}");
}
error[E0308]: mismatched types
--> src/main.rs:10:21
|
10 | let c = num_add(1.2,2);
| ------- ^^^ expected `i32`, found floating-point number
| |
| arguments to this function are incorrect
|
note: function defined here
--> src/main.rs:7:8
|
7 | fn num_add(a: i32, b: i32) -> i32 {
| ^^^^^^^ ------

你要解决这个问题,那么你只能写多个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
fn num_add_i32(a: i32, b: i32) -> i32 {
a + b
}
fn num_add_i8(a: i8, b: i8) -> i8 {
a + b
}
fn num_add_f32(a: f32, b: i32) -> f32 {
a + b as f32
}
let c = num_add_i32(1, 2);
println!("{c}");
let c = num_add_i8(1, 2);
println!("{c}");
let c = num_add_f32(1.2, 2);
println!("{c}");
}
// 3
// 3
// 3.2

这样实现是不是太繁琐。我们需要能够代表所有i32、i8、f32等一个东东。rust管这个叫泛型(generics),语法如下:

1
2
3
4
5
6
7
8
9
fn main() {
fn num_add<T>(a: T, b: T) -> T {
a+b
}
let c = num_add(1.2, 3.3);
println!("{c}")

}
// 4.5

那么如果是f32+i32呢?那就多加一个泛型和几个约束:

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
use std::*;
fn num_add<'t,'l,T: ops::Add<Output=T> + From<U>, U>(a:T, b: U) -> T {
a + b.into()
}
let c:f64 = num_add(1.1, 3);
println!("{c}");
let c:i32 = num_add(520, 3);
println!("{c}");
}
4.1
523

又多了点奇怪的语法。我们来分析一下这个函数签名(就是理解为函数的写法就行)

1
fn num_add\<T: ops::Add\<Output=T\> + From\<U\>, U\>(a: T, b: U) -> T

1、简单理解fn是关键字
2、num_add是函数名
3、<>称为尖括号。里面当然是写尖括号参数,目前我们知道有生命周期、泛型两个。其中生命周期我们使用‘t: 单引号和小写字母。泛型我们使用T大写字母。
4、尖括号的第一个参数

1
T: ops::Add<Output=T> + From<U>

冒号表示对泛型约束。多个约束使用 + 相连。于是泛型又两个约束:1、要有add的操作;2、要能使得泛型U能转化为泛型T。
4、尖括号的第二个参数是泛型U
5、()称为圆括号。里面当然是写圆括号参数,也就是我们输入参数,第一个参数a有泛型T的约束,第二参数b有泛型U的约束
6、 -> T箭头表示输出类型,就是泛型T。

是不是有点复杂,木有办法要实现自由内存的自由联合确实要实现高水平的治理水平。上例没有生命周期,那么我们加个生命周期,来点复杂的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
fn longest<'t, 's, T: PartialOrd + ?Sized,U:?Sized>(x: &'t T, y: &'t U) -> &'s T
where 't: 's,&'t T: From<&'t U>{

if x > y.into() {
x
} else { y.into() }
}

let x = "作用域原则".to_string();
let y = "生命周期的灵活性";
let a = &x[..];
let b = y;
let c = longest(a, b);
println!("{c}");
let c = longest(&8, &6);
println!("{c}");
}
// 生命周期的灵活性
// 8

这里解释一下?Sized:

Types with a constant size known at compile time.All type parameters have an implicit bound of Sized. The special syntax ?Sized can be used to remove this bound if it’s not appropriate.
类型,其大小在编译时已知。 所有类型参数都有一个size的隐式边界。如果这个边界不合适,可以使用特殊的语法?Sized来删除它。

解释一下PartialOrd:

Trait for types that form a partial order .The lt, le, gt, and ge methods of this trait can be called using the <, <=, >, and >= operators, respectively.
构成偏序类型的特征。 这个trait的lt、le、gt和ge方法可以分别使用<、<=、>和>=操作符调用。

2.2、特征(traits)
2.2.1、方法语法(Method Syntax)

好了终于讲到特征了,在讲特征之前。我们需要了解一下类型的方法。关键词是impl impl仅适用于结构体、枚举、union和trait对象(impl only structs, enums, unions and trait objects)

我们来实现一个二维坐标象限显示的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
fn main() {
#[derive(Debug)]
struct Coordinate {
x: f64,
y: f64,
}
impl Coordinate {
fn show(&self) -> String {
format!("===========\n\
本点在第{}象限\nx是{:.4}\ny是{:.4}\
\n===========", self.quadrant(), self.x, self.y)
}
fn quadrant(&self) -> i8 {
let res = match (self.x > 0.0, self.y > 0.0) {
(true, true) => 1,
(false, true) => 2,
(false, false) => 3,
(true, false) => 4,
};
return res;
}
}

let a = Coordinate { x: 1.2, y: 2.0 };
let b = a.show();
println!("{b}");
println!("{a:?}")
}
// ===========
// 本点在第1象限
// x是1.2000
// y是2.0000
// ===========
// Coordinate { x: 1.2, y: 2.0 }

对于方法,其它语言中所有定义都在 class 中,但是 Rust 的对象定义和方法定义是分离的,这种数据和使用分离的方式,会给予使用者极高的灵活度。

类型既然可以用泛型抽象,那么方法可不可以抽象。当然可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
fn main() {
pub trait Show {
fn show(&self) -> String;
fn quadrant(&self) -> i8;
}

#[derive(Debug)]
struct Coordinate2d {
x: f64,
y: f64,
}
#[derive(Debug)]
struct Coordinate3d {
x: f64,
y: f64,
z: f64,
}

impl Show for Coordinate2d {
fn show(&self) -> String {
format!("====\n本点在第{}象限\nx是{:.4}\ny是{:.4}"
, self.quadrant(), self.x, self.y)
}
fn quadrant(&self) -> i8 {
let res = match (self.x > 0.0, self.y > 0.0) {
(true, true) => 1,
(false, true) => 2,
(false, false) => 3,
(true, false) => 4,
};
return res;
}
}

impl Show for Coordinate3d {
fn show(&self) -> String {
format!("====\n本点在第{}象限\nx是{:.4}\ny是{:.4}\nz是{:.4}"
, self.quadrant(), self.x, self.y, self.z)
}
fn quadrant(&self) -> i8 {
let res = match (self.x > 0.0, self.y > 0.0, self.z > 0.0) {
(true, true, true) => 1,
(false, true, true) => 2,
(false, false, true) => 3,
(true, false, true) => 4,
(true, true, false) => 5,
(false, true, false) => 6,
(false, false, false) => 7,
(true, false, false) => 8,
};
return res;
}
}

fn notify(item: &impl Show) {
println!("提示如下{}", item.show());
}
let a = Coordinate2d{x:1.2,y:2.4};
let b = Coordinate3d{x:1.2,y:-3.2,z:3.2};
notify(&a);
notify(&b);
}
// 提示如下====
// 本点在第1象限
// x是1.2000
// y是2.4000
// 提示如下====
// 本点在第4象限
// x是1.2000
// y是-3.2000
// z是3.2000

自行体会一下。

三、评述

这里在啰嗦一下特征(traits)和类(class)的区别。数据聚集在一起可以抽象。方法聚集在一起也可以抽象。那么到底哪个是世界的本质。这就涉及集合论和范畴论哪个才是数学的基础的问题。

一开始数学的基础是集合论,但随着数学的发展将事物关系抽象出来,进而作为一切的基础能大大简化分析,而这就是范畴论。我们不在追寻事物本身是什么。我们更关心事物的关系是什么,关系才是本质的东西。而哲学上也基本是这样的一个套路。唯物主义认为人的本质是一切社会关系的总和。也就说我们并不关心人本来是什么,有没眼睛,会不会做30个引体向上,有没有漂亮脸蛋… 这些我们都不关心。我们只关心这个人在社会中的各种关系。也就说无论是阿猫阿狗,还是人工智能,如果它也有这样的关系总和,我们就称它为人。

这反应到编程上,就是大概你应该听说过的鸭子类型:当你看到一只鸟走起来像鸭子,游泳起来鸭子,叫起来也像鸭子,那么这只鸟就被称为鸭子类型。是不是觉得编程语言实际蕴含着人工智能觉醒的基础哈。

所以class将数据和方法集聚,然后使用继承来解决方法抽象的问题是不自然的。它们应该分开。而这就是泛型(generics)和特征(traits)。


版权声明
引线小白创作并维护的柠檬CC博客采用署名-非商业-禁止演绎4.0国际许可证。
本文首发于柠檬CC [ https://www.limoncc.com ] , 版权所有、侵权必究。
本文永久链接httpss://www.limoncc.com/post/54839b6bfa303599/
如果您需要引用本文,请参考:
引线小白. (Sep. 15, 2022). 《戏说rust二_细节篇》[Blog post]. Retrieved from https://www.limoncc.com/post/54839b6bfa303599
@online{limoncc-54839b6bfa303599,
title={戏说rust二_细节篇},
author={引线小白},
year={2022},
month={Sep},
date={15},
url={\url{https://www.limoncc.com/post/54839b6bfa303599}},
}

'