系列文章目錄
【跟小嘉學 Rust 編程】一、Rust 編程基礎
【跟小嘉學 Rust 編程】二、Rust 包管理工具使用
【跟小嘉學 Rust 編程】三、Rust 的基本程序概念
【跟小嘉學 Rust 編程】四、理解 Rust 的所有權概念
前言
本章節(jié)將講解 Rust 獨有的概念(所有權)。所有權是 Rust 最獨特的特性,它使得 Rust 能夠在不需要垃圾收集器的情況下保證內(nèi)存安全。因此理解所有權是如何工作很重要,本章節(jié)將講解所有權相關的特性:借用、切片以及 Rust 如何在內(nèi)存中布局數(shù)據(jù)。
主要教材參考 《The Rust Programming Language》
一、所有權(Ownership)
所有權是 Rust 最獨特的特性,它使得 Rust 能夠在不需要垃圾收集器的情況下保證內(nèi)存安全。
1.1.、所有權(Ownership)
在 Java 等編程語言中存在垃圾回收機制,在程序運行時定期查找不再使用的內(nèi)存,在C/C++中,程序員必須顯式地分配和釋放內(nèi)存。
在 Rust 之中使用了第三種方法:內(nèi)存通過一個所有權系統(tǒng)進行管理,該系統(tǒng)擁有一組編譯器檢查的規(guī)則,如果違反了任何規(guī)則,程序?qū)o法編譯。所有權的任何特性都不會再程序運行時減慢它的速度。
1.2、棧(Stack)和堆(Heap)
許多語言不需要經(jīng)??紤]堆和棧,但是在Rust 這樣的系統(tǒng)編程語言中,值存在棧還是堆上都會影響語言的行為,以及你要做出什么樣的處理。
堆棧都是都可以在運行時使用的內(nèi)粗部分,但是它們的結(jié)構(gòu)方式不同。棧按照獲取值的順序存儲值,并按照相反的順序刪除值。這被稱為后進先出。添加數(shù)據(jù)稱為壓棧(push),刪除數(shù)據(jù)稱為出棧(pop)。
存儲在棧上的所有數(shù)據(jù)必須具有已知的固定大小。在編譯時大小未知或大小可能改變的數(shù)據(jù)必須存儲在堆中。
堆的組織較差:當你數(shù)據(jù)放在堆上,您請求一定數(shù)量的空間。內(nèi)存分配器在堆中找到一個足夠大的空間,將其標記為正在使用,并返回一個指針,該指針是該位置的地址,這個過程叫做在堆上分配,由于指向堆堆指針是已知的固定大小,因此可以將指針存儲在棧上。
1.3、所有權規(guī)則(Ownership Rules)
所有權有如下三條規(guī)則
- Rust 中的每個值都有一個變量,稱為其所有者;
- 一次只能有一個所有者;
- 當所有者不再程序運行范圍時,該值將會被刪除;
這三條規(guī)則時所有權概念的基礎;
1.4、變量作用域(Variable Scope)
通過理解下面的代碼實例,可以理解變量作用范圍。
fn main() {
let s1 = "hello";
{ // s2 is not valid here, it’s not yet declared
let s2 = "hello"; // s2 is valid from this point forward
// do stuff with s2
} // this scope is now over, and s is no longer valid
}
1.5、字符串類型(String Type)
1.5.1、字符串切片引用(&str 類型)
使用字符串字面初始化的字符串類型是 &str 類型的字符串。此種類型是已知長度,存儲在可執(zhí)行程序的只讀內(nèi)存段中(rodata)。通過 &str 可以引用過 rodata 中的字符串。
let s:&str = "hello";
如果想直接使用 str 類型 是不可以的,只能通過 Box<str>
來使用。
1.5.2、String類型
1.5.2.1、 String 字符串介紹
Rust 在語言級別,只有一種字符串類型: str,它通常是以引用類型出現(xiàn) &str,也就是上文提到的字符串切片引用。雖然語言級別只有上述的 str 類型,但是在標準庫里,還有多種不同用途的字符串類型,其中使用最廣的即是 String 類型。
Rust 中的字符串是 Unicode 類型,因此每個字符占據(jù) 4 個字節(jié)內(nèi)存空間,但是字符串中不一樣,字符串是 UTF-8 編碼,也就是字符串中的字符所占的字節(jié)數(shù)是變化的。
字符串字面量值被硬編碼到程序中,它們是不可變的。但是實際上并不是所有的字符串值都是已知的。如果我們想要獲取用戶輸入并存儲他,Rust 提供了第二種字符串類型。這種類型管理在堆上分配的數(shù)據(jù)。
fn main() {
let s1:&str = "hello";
let s2:String = s1.to_string();
let s3:&String = &s2;
let s4:&str = &s2[0..3];
let s5:String = String::from(s1);
}
需要注意,rust 要求索引必須是 usize 類型。如果起始索引是0,可以簡稱 &s[..3]
,同樣可以終止索引 是 String 的最后一個字節(jié),那么可以簡寫為 &s[1..]
,如果要引用整個 String 可以簡寫為 &s[..]
。
字符串切片引用索引必須在字符之間的邊界未知,但是由于 rust 的字符串是 utf-8 編碼,因此必須小心。
1.5.2.2、 創(chuàng)建 String 字符串
let s = "Hello".to_string();
let s = String::from("world");
let s: String = "also this".into();
1.5.2.2、 追加字符串
fn main() {
let mut s = String::from("Hello ");
s.push('r');
println!("追加字符 push() -> {}", s);
s.push_str("ust!");
println!("追加字符串 push_str() -> {}", s);
}
1.5.2.3、 插入字符串
fn main() {
let mut s = String::from("Hello rust!");
s.insert(5, ',');
println!("插入字符 insert() -> {}", s);
s.insert_str(6, " I like");
println!("插入字符串 insert_str() -> {}", s);
}
1.5.2.4、字符串替換
1、replace 方法使用 兩種類型的 字符串;
fn main() {
let string_replace = String::from("I like rust. Learning rust is my favorite!");
let new_string_replace = string_replace.replace("rust", "RUST");
dbg!(new_string_replace); // 調(diào)試使用宏
let s = "12345";
let new_s = s.replace("3", "t");
dbg!(new_s);
}
2、replacen 方法使用 兩種類型的 字符串;
fn main() {
let string_replace = "I like rust. Learning rust is my favorite!";
let new_string_replacen = string_replace.replacen("rust", "RUST", 1);
dbg!(new_string_replacen);
}
3、replace_range 只使用 String 類型
fn main() {
let mut string_replace_range = String::from("I like rust!");
string_replace_range.replace_range(7..8, "R");
dbg!(string_replace_range);
}
1.5.2.4、字符串刪除
1、pop
fn main() {
let mut string_pop = String::from("rust pop 中文!");
let p1 = string_pop.pop();
let p2 = string_pop.pop();
dbg!(p1);
dbg!(p2);
dbg!(string_pop);
}
2、remove
fn main() {
let mut string_remove = String::from("測試remove方法");
println!(
"string_remove 占 {} 個字節(jié)",
std::mem::size_of_val(string_remove.as_str())
);
// 刪除第一個漢字
string_remove.remove(0);
// 下面代碼會發(fā)生錯誤
// string_remove.remove(1);
// 直接刪除第二個漢字
// string_remove.remove(3);
dbg!(string_remove);
}
3、truncate
fn main() {
let mut string_truncate = String::from("測試truncate");
string_truncate.truncate(3);
dbg!(string_truncate);
}
4、clear
fn main() {
let mut string_clear = String::from("string clear");
string_clear.clear(); // 相當于string_clear.truncate(0)
dbg!(string_clear);
}
1.5.2.5、字符串連接
字符串連接 使用 + 或 += 操作符,要求右邊的參數(shù)必須是字符串的切片引。使用 + 相當于使用 std::string 標準庫中的 add 方法
fn main() {
let string_append = String::from("hello ");
let string_rust = String::from("rust");
// // &string_rust會自動解引用為&str,這是因為deref coercing特性。這個特性能夠允許把傳進來的&String,在API執(zhí)行之前轉(zhuǎn)成&str。
let result = string_append + &string_rust;
let mut result = result + "!";
result += "!!!";
println!("連接字符串 + -> {}", result);
}
1.5.2.6、使用 format!連接字符串
這種方式適用于 String 和 &str,和C/C++提供的sprintf函數(shù)類似
fn main() {
let s1 = "hello";
let s2 = String::from("rust");
let s = format!("{} {}!", s1, s2);
println!("{}", s);
}
1.5.2.7、轉(zhuǎn)義的方式 \
fn main() {
// 通過 \ + 字符的十六進制表示,轉(zhuǎn)義輸出一個字符
let byte_escape = "I'm writing \x52\x75\x73\x74!";
println!("What are you doing\x3F (\\x3F means ?) {}", byte_escape);
// \u 可以輸出一個 unicode 字符
let unicode_codepoint = "\u{211D}";
let character_name = "\"DOUBLE-STRUCK CAPITAL R\"";
println!(
"Unicode character {} (U+211D) is called {}",
unicode_codepoint, character_name
);
}
1.5.2.7、字符串行連接
fn main() {
let long_string = "String literals
can span multiple lines.
The linebreak and indentation here \
can be escaped too!";
println!("{}", long_string);
}
1.5.2.7、原始字符串
使用 r 開頭的字符串不會被轉(zhuǎn)義
fn main() {
println!("{}", "hello \x52\x75\x73\x74"); // 輸出hello Rust
let raw_str = r"Escapes don't work here: \x3F \u{211D}"; // 原始字符串
println!("{}", raw_str); // 輸出Escapes don't work here: \x3F \u{211D}
}
1.5.2.8、字符串帶雙引號問題
rust 提供來 r# 方式來避免引號嵌套的問題。
fn main() {
// 如果字符串包含雙引號,可以在開頭和結(jié)尾加 #
let quotes = r#"And then I said: "There is no escape!""#;
println!("{}", quotes);
// 如果還是有歧義,可以繼續(xù)增加#,沒有限制
let longer_delimiter = r###"A string with "# in it. And even "##!"###;
println!("{}", longer_delimiter);
}
1.5.2.9、字符數(shù)組
由于 rust 的字符串是 utf-8 編碼的,而String 類型不允許以字符為單位進行索引。所以 String 提供了 chars() 遍歷字符和 bytes() 方法遍歷字節(jié)。
但是需要從 String 中獲取子串是比較困難的,標準庫中沒有提供相關的方法。
1.6、內(nèi)存(Memory)和分配(Allocation)
字符出字面量快速高效,是因為硬編碼到最終可執(zhí)行文件之中。這些字符出是不可變的。但是我們不能為每個在編譯時大小未知且運行程序時可能改變的文本放入二進制文件的內(nèi)存塊中。
String 類型為了支持可變、可增長的文本片段,我們需要在堆上分配一定數(shù)量的內(nèi)存來保存內(nèi)容,這就意味著內(nèi)存必須在運行時從內(nèi)存分配器中請求,我們需要一種方法,在使用完 String 后將這些內(nèi)存返回給分配器。
第一部分:當調(diào)用String::from時,它的實現(xiàn)請求它所需的內(nèi)存。
第二部分:在帶有垃圾回收器(GC)的語言, GC 會清理不在使用的內(nèi)存,我們不需要考慮他。在沒有GC的語言中我們有責任識別內(nèi)存不再使用,并且調(diào)用代碼顯式釋放它。
Rust 采用不同的方式:一旦擁有的內(nèi)存的變量超出作用域,內(nèi)存就會自動返回(Rust 會為我們調(diào)用一個特殊的函數(shù)叫做 drop)。
在c++中 這種項目生命周期結(jié)束時釋放資源的模式有時候被稱為 資源獲取即初始化(RALL)。
1.7、變量與數(shù)據(jù)交互的方式
1.7.1、移動(Move)
1、賦值
將一個變量賦值給另一個變量會將所有權轉(zhuǎn)移。
let s1 = String::from("hello");
let s2 = s1;
2、參數(shù)傳遞或函數(shù)返回
賦值并不是唯一涉及移動的操作,值在作為參數(shù)傳遞或從函數(shù)返回時也會被移動。
3、賦給結(jié)構(gòu)體或 enum
1.7.3、拷貝(copy)
在編譯是已知大小的整數(shù)類型完全存在棧中,因此可以快速直接復制實際值。
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
在 Rust 中有一個特殊的注解 叫做 Copy trait ,我們可以把它放在存儲在棧上的類型,就像整數(shù)一樣,如果一個類型實現(xiàn)了 Copy 特性,那么它的變量就不會移動。
如果類型或其任何部分實現(xiàn)了Drop trait,Rust將不允許我們用Copy注釋類型。
實現(xiàn)了 Copy trait的類型
-
所有的整型類型
-
bool 類型: true、false
-
浮點類型;f32、f64
-
字符類型:char
-
元組(只包含了實現(xiàn)了 Copy trait),例如 (i32,i32) 就是 copy,而( i32,String) 是move;
1.7.2、克隆(clone)
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
1.8、涉及函數(shù)的所有權機制
fn main() {
let s = String::from("hello"); // s comes into scope
takes_ownership(s); // s's value moves into the function...
// ... and so is no longer valid here
println!("s {}", s);
let x = 5; // x comes into scope
makes_copy(x); // x would move into the function,
// but i32 is Copy, so it's okay to still
// use x afterward
} // Here, x goes out of scope, then s. But because s's value was moved, nothing
// special happens.
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.
fn makes_copy(some_integer: i32) { // some_integer comes into scope
println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.
1.9、函數(shù)返回值的所有權機制
fn main() {
let s1 = gives_ownership(); // gives_ownership moves its return
// value into s1
let s2 = String::from("hello"); // s2 comes into scope
let s3 = takes_and_gives_back(s2); // s2 is moved into
// takes_and_gives_back, which also
// moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
// happens. s1 goes out of scope and is dropped.
fn gives_ownership() -> String { // gives_ownership will move its
// return value into the function
// that calls it
let some_string = String::from("yours"); // some_string comes into scope
some_string // some_string is returned and
// moves out to the calling
// function
}
// This function takes a String and returns one
fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
// scope
a_string // a_string is returned and moves out to the calling function
}
二、引用(Reference)和租借(Borrowing)
2.1、引用(Reference)
引用(Reference) 是 C++ 開發(fā)者較為熟悉的概念,如果你熟悉指針的概念,你可以把它當作一種指針。實質(zhì)上,引用是變量的間接訪問方式。
我們使用引用就可以避免所有權移動導致原先的變量不能使用的問題。
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()
}
因為 s 是 String的引用,它沒有所有權,當函數(shù)結(jié)束的時候,并不會drop。
2.3、租借(Borrowing)
2.3.1、租借
引用不會獲得值的所有權,引用只能租借(Borrow)值的所有權。引用本身也是一種類型并具有一個值,這個值記錄的是別的值所在的位置,但引用不具有所有值的所有權。
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
--> src/main.rs:8:3
|
8 | some_string.push_str(", world");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference
|
7 | fn change(some_string: &mut String) {
| ~~~~~~~~~~~
For more information about this error, try `rustc --explain E0596`.
error: could not compile `hello` (bin "hello") due to previous error
從上述錯誤提示可以知道 reference 是租借引用,只能讀不能進行寫操作,我們可以使用 &mut 來進行 可變引用。
2.3.2、Mutable References
使用 Mutable References 可以修改引用的內(nèi)容。
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
一個變量只能有一個 Mutable References。
對同一個值有不可變引用的時候,不能有可變引用。
2.4、垂懸引用(Dangling Reference)
垂懸引用 好像也叫做 野指針。
如果在有指針概念的編程語言,它指的是那種沒有實際指向一個真正能訪問的數(shù)據(jù)和指針(注意,不一定是空指針,還有可能是已經(jīng)釋放的資源),它們就像失去懸掛物體的繩子,所以叫做垂懸引用。
垂懸引用 在 Rust 語言里面不允許出現(xiàn),如果有,編譯器會發(fā)現(xiàn)它
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
很顯然,伴隨著 dangle 函數(shù)的結(jié)束,其局部變量的值本身沒有被當作返回值,被釋放了。但它的引用卻被返回,這個引用所指向的值已經(jīng)不能確定的存在,故不允許其出現(xiàn)。
error[E0106]: missing lifetime specifier
--> src/main.rs:5:16
|
5 | fn dangle() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
|
5 | fn dangle() -> &'static String {
| +++++++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `hello` (bin "hello") due to previous error
三、切片(Slice Type)
切片(Slice) 是對數(shù)據(jù)值的部分引用。
3.1、字符串切片(String Slice)
最簡單、最常用的數(shù)據(jù)切片類型是字符串切片(String Slice)。文章來源:http://www.zghlxwxcb.cn/news/detail-497106.html
3.2、數(shù)組切片
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);
總結(jié)
以上就是今天要講的內(nèi)容文章來源地址http://www.zghlxwxcb.cn/news/detail-497106.html
- 本文介紹 rust的所有權、切片、字符出類型、租借、可變引用,本章節(jié)的內(nèi)容比較難以理解;
到了這里,關于【跟小嘉學 Rust 編程】四、理解 Rust 的所有權概念的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!