午夜视频在线网站,日韩视频精品在线,中文字幕精品一区二区三区在线,在线播放精品,1024你懂我懂的旧版人,欧美日韩一级黄色片,一区二区三区在线观看视频

分享

Rust學(xué)習(xí)筆記(4)-Ownership

 TestOps云層 2022-03-23

Ownership

Ownership是Rust語言所特有的,用于運行時內(nèi)存管理的一套規(guī)則。這是Rust語言的核心特點。

前置知識

要理解Ownership概念,首先需要理解堆內(nèi)存(Heap)和棧內(nèi)存(Stack)的特點,這個屬于基礎(chǔ)知識了,不懂的小伙伴自行補下課。

Ownership規(guī)則

先看一下這幾條規(guī)則:

  1. 每個值都需要有一個變量來承載,這個變量叫做Owner;

  2. 在同一時間內(nèi),一個值只能有一個owner;

  3. 當owner離開了自己的作用域(Scope),那么值就會被丟掉。

關(guān)于作用域

其實作用域很容易理解,跟c/c++,java等語言一樣,看例子:

1
2
3
4
5
{                      // s is not valid here, it’s not yet declared
let s = "hello"; // s is valid from this point forward

// do stuff with s
} // this scope is now over, and s is no longer valid

內(nèi)存與分配

跟Java其實很像,基本數(shù)據(jù)類型(整型,浮點型,布爾型,字符型,包括這些類型組成了tuple類型),因為固定長度,類型也明確,所以會直接被分配保存到棧(stack)內(nèi)存中,其余的類型,都會在堆內(nèi)存中分配空間保存值,而把分配到的堆內(nèi)存地址返回回來,保存在棧內(nèi)存中。

1
2
3
4
5
{                      
let mut s = String::from("hello"); // s is valid from this point forward

// do stuff with s
} // this scope is now over, and s is no longer valid

這個例子和前面的很像,只是把字符串常量換成String了,這里的差別,就在于字面常量的”hello”是不可變的,其內(nèi)容固定長度(5個字符類型),類型也確定,所以會保存在stack中,所以它不可變更。而實際應(yīng)用中,通常字符串長度都無法在編譯時確定,只有在運行時才能確定,所以這里使用了一個String類型。那么因為這個類型不屬于基礎(chǔ)類型,所以會將hello這五個字符值保存在heap中,并將heap中分配的地址、長度、容量保存到stack中。

那么其實就帶來一個細節(jié)問題了,上面那段代碼中的例子,當let s開始定義時,根據(jù)前面的說明,s有效了,在離開了作用域之后,s就會無效,此時遺留在heap中的5個字符”hello”怎么辦?heap內(nèi)存并不會主動去釋放這個字節(jié)的空間。

一些語言使用了GC的方式,比如java,使用GC的方式掃描heap中是否存在沒有引用的值,這些值所占的空間會被釋放。另一些語言則需要程序員主動去釋放,比如C/C++,在malloc/new內(nèi)存了之后,要有匹配的delete/free來進行內(nèi)存釋放,否則就可能會出現(xiàn)內(nèi)存泄露問題。

Rust選擇了一條比較困難的路,在判斷一個作用域到結(jié)尾的時候(通常就是”}”),會自動調(diào)用一個drop方法,去釋放heap中無用的值所占空間。這個邏輯雖然看起來簡單,但是會有很多細節(jié)的問題,導(dǎo)致了Rust的特殊性。來看個例子:

1
2
let x = 5;
let y = x;

這個在內(nèi)存中做了什么?首先在stack內(nèi)存中棧頂分配了一塊32個bit(4字節(jié))大小的空間,直接存放了5,然后繼續(xù)在棧頂分配了32bit的空間,依然存放了5,也就是說,兩塊緊挨著的內(nèi)存空間,分別代表著x和y,都存放著5,這個很容易理解。

再看下面的例子:

1
2
let s1 = String::from("hello");
let s2 = s1;

這會和前面的例子一樣嗎?對不起,完全不同。

首先s1的值hello存放在heap中,而stack中存放的,是值在heap內(nèi)存中的地址,以及大小和容量,如下圖:

然后是s2的賦值,s1賦值給s2的,是保存在stack中的heap內(nèi)存地址、大小和容量,而不是hello這個值本身。所以,其實就變成了這樣的情況:

那么這里就有一個問題了,我們前面說過,當變量離開自己的作用域時,Rust會調(diào)用一個drop方法,將值所占的heap空間釋放掉。而我們這里的例子,s1和s2顯然屬于同一個作用域,那么肯定會在離開作用域時,大家都會調(diào)用drop釋放heap中的值。但是注意了,s1和s2指向的heap空間是同一個,那就會出現(xiàn)重復(fù)釋放的問題,導(dǎo)致內(nèi)存訪問異常,這是典型的安全問題。

為了解決這個問題,Rust在s1賦值給s2時,會認為s1已經(jīng)無用了,將其直接標識為無效。所以后面的釋放就不用考慮s1了。那么下面這個錯誤也就可以理解了:

1
2
3
4
let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);

這將會出現(xiàn)編譯報錯:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:28
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{}, world!", s1);
| ^^ value borrowed here after move

For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` due to previous error

提示s1已經(jīng)被”move”了??梢娺@種賦值,造成的其實是”move”的操作,并不是”copy”的方式,賦值之后,原來的變量就失效了。如果要保留s1,真正做到”copy”,那可以用clone的方式:

1
2
3
4
let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);

這種”move”的情況,也同樣出現(xiàn)在函數(shù)調(diào)用傳值上:

1
2
3
4
5
6
7
8
9
fn main() {
let s = String::from("hello"); // s comes into scope

takes_ownership(s);
}
fn takes_ownership(some_string: String) { // some_string comes into scope
println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The backing
// memory is freed.

s在把值”move”給函數(shù)takes_ownership后,自己就失效了,如果在takes_ownership之后要調(diào)用s,就會出現(xiàn)編譯報錯!這點在Rust編程中一定要小心。

根據(jù)這個例子,也可以這么理解Rust的Ownership機制 —— 每一個在heap內(nèi)存中保存的值,只能有一個“擁有者”(Owner),也就是保存了這個內(nèi)存地址的變量,一旦換了其他變量來保存,也就是換了“擁有者”,原來的擁有者就失效了。

引用與借用

前面的那個例子中,s一旦傳給了函數(shù),本身就失效了,因為換了Owner。如果我們后面的代碼還想使用s,那就要換一種方式來給函數(shù)傳值:

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let s1 = String::from("hello");

let len = calculate_length(&s1);

println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
s.len()
}

這段代碼calculate_length函數(shù)的參數(shù)換成了&String,這個&符號表示引用,這里的s的類型就是String的引用類型,這個概念和C/C++一摸一樣。引用的作用,就是把傳入的參數(shù)的地址傳進去,但并不是值本身,這樣就沒有改變hello這個值的Owner,那么s1就不會失效。s和s1的關(guān)系,看下圖:

這種引用,在Rust中稱為“借用”(borrow),很有意思,直白的表達了只是“借”,不是擁有者,借完了之后還要“還”。另外,一個變量,一次只能“借”給一個變量,不能在同一作用域被借用兩次:

1
2
3
4
5
6
let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);

這段代碼編譯會直接報錯:

1
2
3
4
5
6
7
8
9
10
11
12
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src/main.rs:5:14
|
4 | let r1 = &mut s;
| ------ first mutable borrow occurs here
5 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
6 |
7 | println!("{}, {}", r1, r2);
| -- first borrow later used here

但是,如果“借用”是不可變借用,那可以被多次借用,這是Rust為了防止出現(xiàn)“數(shù)據(jù)爭用”(data race)做的規(guī)定:

1
2
3
4
5
6
7
let mut s = String::from("hello");

let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM

println!("{}, {}, and {}", r1, r2, r3);

上面這個例子還說明了一個規(guī)則,不可變借用和可變借用不可同時使用,因為不可變借用不希望借用所指向的數(shù)據(jù)被忽然變更。但是下面這種情況可以:

1
2
3
4
5
6
7
8
9
let mut s = String::from("hello");

let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, r2);
// variables r1 and r2 will not be used after this point

let r3 = &mut s; // no problem
println!("{}", r3);

只要在r3借用之后,不再出現(xiàn)使用r1、r2的語句,那就不會有編譯問題。

空懸引用

其實就是指無效引用,被引用的內(nèi)存空間已經(jīng)被釋放,那這個引用就無效了,Rust會直接在編譯時進行報錯提示,看下面這個例子:

1
2
3
4
5
6
7
8
9
10
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle returns a reference to a String

let s = String::from("hello"); // s is a new String

&s // we return a reference to the String, s
} // Here, s goes out of scope, and is dropped. Its memory goes away.
// Danger!

總體來說,Ownership這個概念中的“引用”其實跟C/C++挺像的,但是C/C++不會報這樣的編譯錯誤,并且不會有任何限制,而Rust為了內(nèi)存訪問安全的考慮,則做了很多限制,從這一點上看,Rust在內(nèi)存安全上花了很多功夫。

切片slice類型

切片類型也是一種引用,所以本身不會存儲值。切片的用法跟很多語言一樣,像python、golang??聪旅娴睦樱?/p>

1
2
3
4
5
6
7
8
9
10
let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];
let slice = &s[..];

在使用slice時,要注意如果被引用的對象本身被另外操作了,那就會出現(xiàn)訪問錯誤,比如下面這個例子:

1
2
3
4
5
6
7
8
9
10
fn main() {
let mut s = String::from("hello world");

let word = &s[..5];

s.push_str("!!"); // error!

println!("the first word is: {}", word);

}

這段代碼會導(dǎo)致編譯錯誤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:10:5
|
8 | let word = &s[..5];
| - immutable borrow occurs here
9 |
10 | s.push_str("!!"); // error!
| ^^^^^^^^^^^^^^^^ mutable borrow occurs here
11 |
12 | println!("the first word is: {}", word);
| ---- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.

這個錯誤,其實就跟前面說的,之前的slice,是做了不可變借用,而后面的push_str則發(fā)生了可變借用,那么在可變借用發(fā)生后,不可以再次使用前面的不可變借用。

再回到字符串字面常量:

1
let s = "hello world";

現(xiàn)在可以理解s了,它其實也是一個切片類型,是指向字符串字面常量的一個不可變借用。這就解釋了為何s不能變更了??聪旅娴睦觼砝斫馇衅茫?/p>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
let my_string = String::from("hello world");

// `first_word` works on slices of `String`s, whether partial or whole
let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);
// `first_word` also works on references to `String`s, which are equivalent
// to whole slices of `String`s
let word = first_word(&my_string);

let my_string_literal = "hello world";

// `first_word` works on slices of string literals, whether partial or whole
let word = first_word(&my_string_literal[0..6]);
let word = first_word(&my_string_literal[..]);

// Because string literals *are* string slices already,
// this works too, without the slice syntax!
let word = first_word(my_string_literal);
}

    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多