高級特性 主要內容 不安全 Rust 高級 Trait 高級 類型 高級函數和閉包 巨集 一、不安全 Rust 匹配命名變數 隱藏著第二個語言,它沒有強制記憶體安全保證:Unsafe Rust(不安全的 Rust) 和普通的 Rust 一樣,但提供了額外的“超能力” Unsafe Rust 存在的原因: ...
高級特性
主要內容
- 不安全 Rust
- 高級 Trait
- 高級 類型
- 高級函數和閉包
- 巨集
一、不安全 Rust
匹配命名變數
- 隱藏著第二個語言,它沒有強制記憶體安全保證:Unsafe Rust(不安全的 Rust)
- 和普通的 Rust 一樣,但提供了額外的“超能力”
- Unsafe Rust 存在的原因:
- 靜態分析是保守的。
- 使用 Unsafe Rust:我知道自己在做什麼,並承擔相應風險
- 電腦硬體本身就是不安全的,Rust需要能夠進行底層系統編程
- 靜態分析是保守的。
Unsafe 超能力
- 使用 unsafe 關鍵字來切換到 unsafe Rust,開啟一個塊,裡面放著 Unsafe 代碼
- Unsafe Rust 里可執行的四個動作(unsafe 超能力):
- 解引用原始指針
- 調用 unsafe 函數或方法
- 方法或修改可變的靜態變數
- 實現 unsafe trait
- 註意:
- Unsafe 並沒有關閉借用檢查或停用其它安全檢查
- 任何記憶體安全相關的錯誤必須留在 unsafe 塊里
- 儘可能隔離 Unsafe 代碼,最好將其封裝在安全的抽象里,提供安全的API
解引用原始指針
- 原始指針
- 可變的:*mut T
- 不可變的:*const T。意味著指針在解引用後不能直接對其進行賦值
- 註意:這裡的 * 不是解引用符號,它是類型名的一部分。
- 與引用不同,原始指針:
- 允許通過同時具有不可變和可變指針或多個執行同一位置的可變指針來忽略借用規則
- 無法保證能指向合理的記憶體
- 允許為null
- 不實現任何自動清理
- 放棄保證的安全,換取更好的性能/與其它語言或硬體介面的能力
解引用原始指針
fn main() {
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
unsafe {
println!("r1: {}", *r1);
println!("r2: {}", *r2);
}
let address = 0x012345usize;
let r = address as *const i32;
unsafe {
println!("r: {}", *r); // 報錯 非法訪問
}
}
- 為什麼要用原始指針?
- 與 C 語言進行介面
- 構建借用檢查器無法理解的安全抽象
調用 unsafe 函數或方法
- unsafe 函數或方法:在定義前加上了 unsafe 關鍵字
- 調用前需手動滿足一些條件(主要靠看文檔),因為Rust無法對這些條件進行驗證
- 需要在 unsafe 塊里進行調用
unsafe fn dangerous() {}
fn main() {
unsafe {
dangerous();
}
}
創建 Unsafe 代碼的安全抽象
- 函數包含 unsafe 代碼並不意味著需要將整個函數標記為 unsafe
- 將 unsafe 代碼包裹在安全函數中是一個常見的抽象
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
assert!(mid <= len);
(&mut slice[..mid], &mut slice[mid..]) // 報錯 cannot borrow `*slice` as mutable more than once at a time
}
fn main() {
let mut v = vec![1, 2, 3, 4, 5, 6];
let r = &mut v[..];
let (a, b) = r.split_at_mut(3);
assert_eq!(a, &mut [1, 2, 3]);
assert_eq!(b, &mut [4, 5, 6]);
}
修改之後:
use std::slice;
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
let ptr = slice.as_mut_ptr()
assert!(mid <= len);
unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len = mid),
)
}
}
fn main() {
let address = 0x012345usize;
let r = address as *mut i32;
let slice: &[i32] = unsafe {
slice::from_raw_parts_mut(r, 10000)
};
}
使用 extern 函數調用外部代碼
- extern 關鍵字:簡化創建和使用外部函數介面(FFI)的過程。
- 外部函數介面(FFI,Foreign Function Interface):它允許一種編程語言定義函數,並讓其它編程語言能調用這些函數
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
- 應用二進位介面(ABI,Application Binary Interface):定義函數在彙編層的調用方式
- “C” ABI 是最常見的ABI,它遵循 C 語言的ABI
從其它語言調用 Rust 函數
- 可以使用 extern 創建介面,其它語言通過它們可以調用 Rust 的函數
- 在 fn 前添加 extern 關鍵字,並指定 ABI
- 還需添加
#[no_mangle]
註解:避免 Rust 在編譯時改變它的名稱
#[no_mangle]
pub extern "C" fn call_from_c() {
println!("Just called a Rust function from C!");
}
fn main() {}
訪問或修改一個可變靜態變數
- Rust 支持全局變數,但因為所有權機制可能產生某些問題,例如數據競爭
- 在 Rust 里,全局變數叫做靜態(static)變數
static HELLO_WORLD: &str = "Hello, world!";
fn main() {
println!("name is: {}", HELLO_WORLD);
}
靜態變數
- 靜態變數與常量類似
- 命名:SCREAMING_SNAKE_CASE
- 必須標註類型
- 靜態變數只能存儲 'static 生命周期的引用,無需顯示標註
- 訪問不可變靜態變數是安全的
常量和不可變靜態變數的區別
- 靜態變數:有固定的記憶體地址,使用它的值總會訪問同樣的數據
- 常量:允許使用它們的時候對數據進行複製
- 靜態變數:可以是可變的,訪問和修改靜態可變變數是不安全(unsafe)的
static mut COUNTER: u32 = 0;
fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
add_to_count(3);
unsafe {
println!("COUNTER: {}", COUNTER);
}
}
實現不安全(unsafe)trait
- 當某個 trait 中存在至少一個方法擁有編譯器無法校驗的不安全因素時,就稱這個 trait 是不安全的
- 聲明 unsafe trait:在定義前加 unsafe 關鍵字
- 該 trait 只能在 unsafe 代碼塊中實現
unsafe trait Foo {
// methods go here
}
unsafe impl Foo for i32 {
// method implementations go here
}
fn main() {}
何時使用 unsafe 代碼
- 編譯器無法保證記憶體安全,保證 unsafe 代碼正確並不簡單
- 有充足理由使用 unsafe 代碼時,就可以這樣做
- 通過顯示標記 unsafe,可以在出現問題時輕鬆的定位
二、高級 Trait
在 Trait 定義中使用關聯類型來指定占位類型
- 關聯類型(associated type)是 Trait中的類型占位符,它可以用於Trait的方法簽名中:
- 可以定義出包含某些類型的 Trait,而在實現前無需知道這些類型是什麼
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
fn main() {
println!("Hello, world!");
}
關聯類型與泛型的區別
泛型 | 關聯類型 |
---|---|
每次實現 Trait 時標註類型 | 無需標註類型 |
可以為一個類型多次實現某個 Trait(不同的泛型參數) | 無法為單個類型多次實現某個 Trait |
例子:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
pub trait Iterator2<T> {
fn next(&mut self) -> Option<T>;
}
struct Counter {}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
None
}
}
impl Iterator2<String> for Counter {
fn next(&mut self) -> Option<String> {
None
}
}
impl Iterator2<u32> for Counter {
fn next(&mut self) -> Option<u32> {
None
}
}
fn main() {
println!("Hello, world!");
}
預設泛型參數和運算符重載
- 可以在使用泛型參數時為泛型指定一個預設的具體類型。
- 語法:
<PlaceholderType=ConcreteType>
- 這種技術常用於運算符重載(operator overloading)
- Rust 不允許創建自己的運算符及重載任意的運算符
- 但可以通過實現 std::ops 中列出的那些 trait 來重載一部分相應的運算符
例子一:
use std::ops::Add;
#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
assert_eq!(Point {x: 1, y: 0} + Point {x: 2, y: 3},
Point {x: 3, y: 3}
);
}
例子二:
use std::ops::Add;
struct Millimeters(u32);
struct Meters(u32);
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
fn main() {
}
預設泛型參數的主要應用場景
- 擴展一個類型而不破壞現有代碼
- 允許在大部分用戶都不需要的特定場景下進行自定義
完全限定語法(Fully Qualified Syntax)如何調用同名方法
例子一:
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {
let persion = Human;
person.fly(); // Human 本身的 fly 方法
Pilot::fly(&person);
Wizard::fly(&person);
}
例子二:
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Dog::baby_name()); // Dog 本身的關聯方法
}
完全限定語法(Fully Qualified Syntax)如何調用同名方法
- 完全限定語法:
<Type as Trait>::function(receiver_if_method, netx_arg, ...);
- 可以在任何調用函數或方法的地方使用
- 允許忽略那些從其它上下文能推導出來的部分
- 當 Rust 無法區分你期望調用哪個具體實現的時候,才需使用這種語法
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Dog::baby_name()); // Dog 本身的關聯方法
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
使用 supertrait 來要求 trait 附帶其它 trait 的功能
- 需要在一個 trait 中使用其它 trait 的功能:
- 需要被依賴的 trait 也被實現
- 那個被間接依賴的 trait 就是當前 trait 的 supertrait
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {} *", output);
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
fn main() {}
使用 newtype 模式在外部類型上實現外部 trait
- 孤兒規則:只有當 trait 或類型定義在本地包時,才能為該類型實現這個 trait
- 可以通過 newtype 模式來繞過這一規則
- 利用 tuple struct (元組結構體)創建一個新的類型
use std::fmt;
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {}", w);
}
三、高級類型
使用 newtype 模式實現類型安全和抽象
- newtype 模式可以:
- 用來靜態的保證各種值之間不會混淆並表明值的單位
- 為類型的某些細節提供抽象能力
- 通過輕量級的封裝來隱藏內部實現細節
使用類型別名創建類型同義詞
- Rust 提供了類型別名的功能:
- 為現有類型生產另外的名稱(同義詞)
- 並不是一個獨立的類型
- 使用 type 關鍵字
- 主要用途:減少代碼字元重覆
例子一:
type Kilometers = i32;
fn main() {
let x: i32 = 5;
let y: Killometers = 5;
println!("x + y = {}", x + y);
}
例子二:
fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) {
// --snip--
}
fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {
Box::new(|| println!("hi"))
}
fn main() {
let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));
}
修改之後:
type Thunk = Box<dyn Fn() + Send + 'static>;
fn takes_long_type(f: Thunk) {
// --snip--
}
fn returns_long_type() -> Thunk {
Box::new(|| println!("hi"))
}
fn main() {
let f: Thunk = Box::new(|| println!("hi"));
}
例子三:
use std::io::Error;
use std::fmt;
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
fn flush(&mut self) -> Result<(), Error>;
fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>;
}
fn main() {
}
修改之後:
use std::fmt;
// type Result<T> = Result<T, std::io::Error>; // 聲明在 std::io 中
type Result<T> = std::io::Result<T>;
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize>;
fn flush(&mut self) -> Result<()>;
fn write_all(&mut self, buf: &[u8]) -> Result<()>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
}
fn main() {
}
Never 類型
- 有一個名為 ! 的特殊類型:
- 它沒有任何值,行話稱為空類型(empty type)
- 我們傾向於叫它 never 類型,因為它在不返回的函數中充當返回類型
- 不返回值的函數也被稱作發散函數(diverging function)
例子一:
fn bar() -> ! { // 報錯 返回單元類型 不匹配
}
fn main() {}
例子二:
fn main() {
let guess = "";
loop {
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue, // ! never 類型
};
}
}
註意:never 類型的表達式可以被強制的轉化為任意其它類型
例子三:
impl<T> Option<T> {
pub fn unwrap(self) -> T {
match self {
Some(val) => val,
None => panic!("called `Option::unwrap()` on a `None` value"), // !
}
}
}
例子四:
fn main() {
println!("forever");
loop {
println!("and ever");
}
}
動態大小和 Sized Trait
- Rust 需要在編譯時確定為一個特定類型的值分配多少空間。
- 動態大小的類型(Dynamically Sized Types,DST)的概念:
- 編寫代碼時使用只有在運行時才能確定大小的值
- str 是動態大小的類型(註意不是 &str):只有運行時才能確定字元串的長度
- 下列代碼無法正常工作:
let s1: str = "Hello there!";
let s2: str = "How's it going?";
- 使用 &str 來解決:
- str 的地址
- str 的長度
- 下列代碼無法正常工作:
Rust使用動態大小類型的通用方式
- 附帶一些額外的元數據來存儲動態信息的大小
- 使用動態大小類型時總會把它的值放在某種指針後邊
另外一種動態大小的類型:trait
- 每個 trait 都是一個動態大小的類型,可以通過名稱對其進行引用
- 為了將 trait 用作 trait 對象,必須將它放置在某種指針之後
- 例如 &dyn Trait 或 Box
(Rc ) 之後
- 例如 &dyn Trait 或 Box
Sized trait
- 為了處理動態大小的類型,Rust 提供了一個 Sized trait 來確定一個類型的大小在編譯時是否已知
- 編譯時可計算出大小的類型會自動實現這一 trait
- Rust 還會為每一個泛型函數隱式的添加 Sized 約束
fn generic<T>(t: T) {}
fn generic<T: Sized>(t: T) {} // 上面的generic 會隱式的轉化為這種
fn main() {}
- 預設情況下,泛型函數只能被用於編譯時已經知道大小的類型,可以通過特殊語法解除這一限制
?Sized trait 約束
fn generic<T>(t: T) {}
fn generic<T: Sized>(t: T) {}
fn generic<T: ?Sized>(t: &T) {} // ? 只能用在 sized上
fn main() {}
- T 可能是也可能不是 Sized
- 這個語法只能用在 Sized 上面,不能被用於其它 trait
四、高級函數和閉包
函數指針
- 可以將函數傳遞給其它函數
- 函數在傳遞過程中會被強制轉換成 fn 類型
- fn 類型就是 “函數指針(function pointer)”
fn add_one(x: i32) -> i32 {
x + 1
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let answer = do_twice(add_one, 5);
println!("The answer is: {}", answer);
}
函數指針與閉包的不同
- fn 是一個類型,不是一個 trait
- 可以直接指定 fn 為參數類型,不用聲明一個以 Fn trait 為約束的泛型參數
- 函數指針實現了全部3種閉包 trait(Fn、FnMut、FnOnce):
- 總是可以把函數指針用作參數傳遞給一個接收閉包的函數
- 所以,傾向於搭配閉包 trait 的泛型來編寫函數:可以同時接收閉包和普通函數
- 某些情景,只想接收 fn 而不接收閉包:
- 與外部不支持閉包的代碼交互:C 函數
例子一
fn main() {
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> = list_of_numbers
.iter().map(|i| i.to_string()).collect();
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> = list_of_numbers
.iter().map(ToString::to_string).collect();
}
例子二
fn main() {
enum Status {
Value(u32),
Stop,
}
let v = Status::Value(3);
let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
}
返回閉包
- 閉包使用 trait 進行表達,無法在函數中直接返回一個閉包,可以將一個實現了該 trait 的具體類型作為返回值。
fn returns_closure() -> Fn(i32) -> i32 { // 報錯 沒有一個已知的大小
|x| x + 1
}
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
fn main() {
}
五、巨集
巨集 macro
- 巨集在Rust里指的是一組相關特性的集合稱謂:
- 使用 macro_rules! 構建的聲明巨集(declarative macro)
- 3 種過程巨集
- 自定義 #[derive] 巨集,用於 struct 或 enum,可以為其指定隨 derive 屬性添加的代碼
- 類似屬性的巨集,在任何條目上添加自定義屬性
- 類似函數的巨集,看起來像函數調用,對其指定為參數的 token 進行操作
函數與巨集的差別
- 本質上,巨集是用來編寫可以生成其它代碼的代碼(元編程,metaprogramming)
- 函數在定義簽名時,必須聲明參數的個數和類型,巨集可處理可變的參數
- 編譯器會在解釋代碼前展開巨集
- 巨集的定義比函數複雜得多,難以閱讀、理解、維護
- 在某個文件調用巨集時,必須提前定義巨集或將巨集引入當前作用域:
- 函數可以在任何位置定義併在任何位置使用
macro_rules! 聲明巨集(棄用)
- Rust 中最常見的巨集形式:聲明巨集
- 類似 match 的模式匹配
- 需要使用 marco_rules!
// let v: Vec<u32> = vec![1, 2, 3];
#[macro_export]
macro_rules! vec {
($($x:expr),*) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
// let mut temp_vec = Vec::new();
// temp_vec.push(1);
// temp_vec.push(2);
// temp_vec.push(3);
// temp_vec
基於屬性來生成代碼的過程巨集
- 這種形式更像函數(某種形式的過程)一些
- 接收並操作輸入的 Rust 代碼
- 生成另外一些 Rust 代碼作為結果
- 三種過程巨集:
- 自定義派生
- 屬性巨集
- 函數巨集
- 創建過程巨集時:
- 巨集定義必須單獨放在它們自己的包中,並使用特殊的包類型
use proc_macro;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
自定義 derive 巨集
- 需求:
- 創建一個 hello_macro 包,定義一個擁有關聯函數 hello_macro 的 HelloMacro trait
- 我們提供一個能自動實現 trait 的過程巨集
- 在它們的類型上標註 #[derive(HelloMacro)],進而得到 hello_macro 的預設實現
➜ cd rust
~/rust
➜ cargo new hello_macro --lib
Created library `hello_macro` package
~/rust
➜ cd hello_macro
hello_macro on master [?] via