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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
|
// 所有权(系统)是 Rust 最为与众不同的特性
// 它让 Rust 无需垃圾回收(garbage collector)即可保障内存安全。
// 因此,理解 Rust 中所有权如何工作是十分重要的。
//
// 所有运行的程序都必须管理其使用计算机内存的方式。
// 一些语言中具有垃圾回收机制,在程序运行时不断地寻找不再使用的内存
// 在另一些语言中,程序员必须亲自分配和释放内存。
// Rust 则选择了第三种方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。
// 在运行时,所有权系统的任何功能都不会减慢程序。
//
// 了解所有权之前还需要了解下栈和堆
// 栈空间的大小是固定的,操作系统会将一些特定的数据存储在栈中(入栈),而移出数据叫做出栈
// 栈以放入值的顺序存储值,并以相反顺序取出值。这也被称作 后进先出(last in, first out)。
//
// 栈中的所有数据都必须占用已知且固定的大小。在编译时大小未知或大小可能变化的数据,要改为存储在堆上。
// 堆是缺乏组织的:当向堆放入数据时,你要请求一定大小的空间。
// 操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 指针(pointer)。
// 这个过程称作 在堆上分配内存(allocating on the heap),有时简称为 “分配”(allocating)。
// 将数据推入栈中并不被认为是分配。因为指针的大小是已知并且固定的,你可以将指针存储在栈上,不过当需要实际数据时,必须访问指针。
//
// 入栈比在堆上分配内存要快,因为(入栈时)操作系统无需为存储新数据去搜索内存空间;其位置总是在栈顶。
// 相比之下,在堆上分配内存则需要更多的工作,这是因为操作系统必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备。
// 当你的代码调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。
//
// 总结一下
// 1.栈比堆快
// 2.栈是操作系统自动处理,我们不必关心(传递给函数的值,局部变量,指针等,都是存储在栈上)
// 3.堆用来存储编译时大小未知或大小可能发生变化的数据
// 4.堆空间需要释放(其他语言可能需要手动释放,或GC自动处理),不管怎么样,堆空间需要释放
// 5.访问堆空间数据需要指针
//
//
// 所有权规则
// 首先,让我们看一下所有权的规则。当我们通过举例说明时,请谨记这些规则:
// Rust 中的每一个值都有一个被称为其 所有者(owner)的变量。
// 值在任一时刻有且只有一个所有者。
// 当所有者(变量)离开作用域,这个值将被丢弃。
fn main() {
// 为了演示所有权,这里使用 String 作为例子
// 我们已经见过字符串字面值,即被硬编码进程序里的字符串值。
// 字符串字面值是很方便的,不过它们并不适合使用文本的每一种场景。原因之一就是它们是不可变的。
// 另一个原因是并非所有字符串的值都能在编写代码时就知道:例如,要是想获取用户输入并存储该怎么办呢?
// 为此,Rust 有第二个字符串类型,String。这个类型被分配到堆上,所以能够存储在编译时未知大小的文本。
// 可以使用 from 函数基于字符串字面值来创建 String,如下:
//let s = String::from("hello");
//
// 对于 String 类型,为了支持一个可变,可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容。
// 这意味着:
// 必须在运行时向操作系统请求内存。
// 需要一个当我们处理完 String 时将内存返回给操作系统的方法。
// 第一部分由我们完成:当调用 String::from 时,它的实现 (implementation) 请求其所需的内存。这在编程语言中是非常通用的。
//
// 然而,第二部分实现起来就各有区别了。
// 在有 垃圾回收(garbage collector,GC)的语言中, GC 记录并清除不再使用的内存,而我们并不需要关心它。
// 没有 GC 的话,识别出不再使用的内存并调用代码显式释放就是我们的责任了,跟请求内存的时候一样。
// 从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。
// 如果忘记回收了会浪费内存。如果过早回收了,将会出现无效变量。如果重复回收,这也是个 bug。
// 我们需要精确的为一个 allocate 配对一个 free。
//
// Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。
//
let s = String::from("hello"); // 从此处起,s 是有效的
println!("This value of s is: {}", s);
// 这是一个将 String 需要的内存返回给操作系统的很自然的位置:当 s 离开作用域的时候。
// 当变量离开作用域,Rust 为我们调用一个特殊的函数。
// 这个函数叫做 drop,在这里 String 的作者可以放置释放内存的代码。Rust 在结尾的 } 处自动调用 drop。
func_move();
func_clone();
func();
func_return();
} // 此作用域已结束,
// s 不再有效
fn func_move() {
// Rust如何保证不会出现二次释放的? 答:移动
// 看下面一个例子
let s1 = String::from("hello");
let s2 = s1;
// 有过其他语言基础的,应该都知道,像这种在堆中分配的数据赋值时,实际上都是复制的指针(浅拷贝)
// 那么根据上面知道rust在变量作用域结束时会调用drop函数释放内存,那么如果s2释放了,s1再释放岂不是会出现二次释放的bug?
// 这种场景下 Rust 认为 s1 不再有效,因此 Rust 不需要在 s1 离开作用域后清理任何东西。
// 看看在 s2 被创建之后尝试使用 s1 会发生什么;这段代码不能运行:
// println!("{}, world!", s1); // 你会得到一个错误,因为s1不再有效,Rust 禁止你使用无效的引用。
// 如果你在其他语言中听说过术语 浅拷贝(shallow copy)和 深拷贝(deep copy),
// 那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。
// 不过因为 Rust 同时使第一个变量无效了,这个操作被称为 移动(move),而不是浅拷贝。
// 上面的例子可以解读为 s1 被 移动 到了 s2 中。
println!("This value of s2 is: {}", s2)
// 另外,这里还隐含了一个设计选择:Rust 永远也不会自动创建数据的 “深拷贝”。
// 因此,任何 自动 的复制可以被认为对运行时性能影响较小。
}
fn func_clone() {
// 不过,如果我们 确实 需要深度复制 String 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone 的通用函数。
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
// 这段代码能正常运行,因为这里堆上的数据 确实 被复制了。
//
// 这里还有一个没有提到的小窍门。这些代码使用了整型并且是有效的,
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
// 但这段代码似乎与我们刚刚学到的内容相矛盾:没有调用 clone,不过 x 依然有效且没有被移动到 y 中。
// 原因是像整型这样的在编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速的。
// 这意味着没有理由在创建变量 y 后使 x 无效。
// Rust 有一个叫做 Copy trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型上
// 如果一个类型拥有 Copy trait,一个旧的变量在将其赋值给其他变量后仍然可用。
//
// 那么什么类型是 Copy 的呢?可以查看给定类型的文档来确认,
// 不过作为一个通用的规则,任何简单标量值的组合可以是 Copy 的,
// 不需要分配内存或某种形式资源的类型是 Copy 的。如下是一些 Copy 的类型:
// 所有整数类型,比如 u32。
// 布尔类型,bool,它的值是 true 和 false。
// 所有浮点数类型,比如 f64。
// 字符类型,char。
// 元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32) 是 Copy 的,但 (i32, String) 就不是。
}
// 所有权与函数
// 将值传递给函数在语义上与给变量赋值相似。向函数传递值可能会移动或者复制
fn func() {
let s = String::from("hello"); // s 进入作用域
takes_ownership(s); // s 的值移动到函数里 ...
//println!("This value of s is: {}", s); // 当尝试在调用 takes_ownership 后使用 s 时,Rust 会抛出一个编译时错误。这些静态检查使我们免于犯错。
// ... 所以到这里不再有效
let x = 5; // x 进入作用域
makes_copy(x); // x 应该移动函数里,
// 但 i32 是 Copy 的,所以在后面可继续使用 x
} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
// 所以不会有特殊操作
fn takes_ownership(some_string: String) {
// some_string 进入作用域
println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放
fn makes_copy(some_integer: i32) {
// some_integer 进入作用域
println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作
// 返回值与作用域
fn func_return() {
let s1 = gives_ownership(); // gives_ownership 将返回值
// 移给 s1
println!("This value of s1 is: {}", s1);
let s2 = String::from("hello"); // s2 进入作用域
println!("This value of s2 is: {}", s2);
let s3 = takes_and_gives_back(s2); // s2 被移动到
// takes_and_gives_back 中,
// 它也将返回值移给 s3
println!("This value of s3 is: {}", s3);
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,所以什么也不会发生。s1 移出作用域并被丢弃
fn gives_ownership() -> String {
// gives_ownership 将返回值移动给
// 调用它的函数
let some_string = String::from("hello"); // some_string 进入作用域.
some_string // 返回 some_string 并移出给调用的函数
}
// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String {
// a_string 进入作用域
a_string // 返回 a_string 并移出给调用的函数
}
|