在 Rust 開發中,處理 ASCII 字串的需求相當常見。為了兼顧安全與效能,我們可以利用 newtype 模式包裝 Vec<u8>,並藉由 unsafe 程式碼實作零成本的 String 轉換。這個技巧能有效限制字串內容僅包含 ASCII 字元,同時避免額外的記憶體分配或複製。然而,使用 unsafe 程式碼必須格外謹慎,確保遵守相關契約以防止未定義行為。此外,瞭解原始指標的操作方式及自定義雜湊器的潛在風險,對於撰寫高效且安全的 Rust 程式碼至關重要。
高效的ASCII字串型別實作範例
在Rust程式語言中,建立一個確保內容始終為有效ASCII字元的字串型別是非常實用的。以下將展示如何定義一個名為Ascii的型別,並利用unsafe功能實作零成本轉換為String。
my_ascii模組實作
mod my_ascii {
use std::ascii::AsciiExt; // 匯入u8::is_ascii方法
/// 一個ASCII編碼的字串。
#[derive(Debug, Eq, PartialEq)]
pub struct Ascii(
// 這個欄位必須只包含有效的ASCII文字:
// 位元組範圍從0到0x7f。
Vec<u8>
);
impl Ascii {
/// 從ASCII文字`bytes`建立一個`Ascii`。
/// 如果`bytes`包含任何非ASCII字元,則傳回`NotAsciiError`錯誤。
pub fn from_bytes(bytes: Vec<u8>) -> Result<Ascii, NotAsciiError> {
if bytes.iter().any(|&byte| !byte.is_ascii()) {
return Err(NotAsciiError(bytes));
}
Ok(Ascii(bytes))
}
}
// 當轉換失敗時,傳回無法轉換的向量。
// 為簡潔起見,這裡省略了std::error::Error的實作。
#[derive(Debug, Eq, PartialEq)]
pub struct NotAsciiError(pub Vec<u8>);
// 安全、高效的轉換,使用unsafe程式碼實作。
impl From<Ascii> for String {
fn from(ascii: Ascii) -> String {
// 如果這個模組沒有錯誤,這是安全的,因為有效的ASCII文字也是有效的UTF-8。
unsafe { String::from_utf8_unchecked(ascii.0) }
}
}
}
內容解密:
Ascii結構體定義:使用一個Vec<u8>來儲存ASCII字元,但不公開該欄位,以確保內容的安全性。from_bytes方法:檢查輸入的位元組向量是否全部為ASCII字元,如果是,則建立一個Ascii例項,否則傳回錯誤。From<Ascii> for String實作:利用unsafe的String::from_utf8_unchecked方法,將Ascii轉換為String,因為有效的ASCII文字也是有效的UTF-8。
使用範例
use my_ascii::Ascii;
let bytes: Vec<u8> = b"ASCII and ye shall receive".to_vec();
// 這個呼叫需要掃描位元組,但不需要分配記憶體或複製文字。
let ascii: Ascii = Ascii::from_bytes(bytes)
.unwrap(); // 我們知道這些選擇的位元組是沒問題的。
// 這個呼叫是零成本的:不需要分配記憶體、複製或掃描。
let string = String::from(ascii);
assert_eq!(string, "ASCII and ye shall receive");
內容解密:
- 建立
Ascii:從位元組向量建立一個Ascii例項,需要檢查內容是否為有效的ASCII。 - 轉換為
String:利用From特性的實作,將Ascii轉換為String,這是一個零成本的操作。
newtype模式
在Rust中,像Ascii這樣的型別被稱為newtype,它包裝了一個現有的型別(在這裡是Vec<u8>),並增加了額外的限制或功能。這種模式在Rust中很常見,例如標準函式庫中的String型別也是這樣定義的:
pub struct String {
vec: Vec<u8>,
}
內容解密:
newtype模式:透過包裝現有的型別並新增額外的規則或方法,可以建立具有特定屬性的新型別。- 記憶體表示:在機器層級,
newtype和它包裝的型別具有相同的記憶體表示,因此建立一個newtype不需要任何機器指令。
不安全的函式
impl Ascii {
/// 在不檢查`bytes`是否包含有效ASCII的情況下,從`bytes`建立一個`Ascii`值。
///
/// # 安全性
///
/// 呼叫者必須確保`bytes`只包含ASCII字元:位元組值不大於0x7f。否則,效果是未定義的。
pub unsafe fn from_bytes_unchecked(bytes: Vec<u8>) -> Ascii {
Ascii(bytes)
}
}
內容解密:
from_bytes_unchecked方法:這是一個不安全的函式,因為它假設輸入的位元組向量是有效的ASCII,而不進行檢查。- 安全性契約:呼叫者有責任確保輸入是有效的ASCII,否則會導致未定義行為。
這種設計允許在確保安全性的前提下,提供高效的操作,同時也給予呼叫者足夠的靈活性。透過這種方式,Rust提供了一種機制,讓開發者可以在安全性和效能之間取得平衡。
使用Unsafe的風險與注意事項
在Rust程式設計中,unsafe關鍵字的使用代表著放棄了語言的部分安全檢查。這種做法雖然在某些情況下是必要的,但也帶來了未定義行為(undefined behavior)的風險。
未定義行為的來源
未定義行為可能源於違反unsafe函式或區塊的契約。例如,使用Ascii::from_bytes_unchecked函式時,如果傳入的位元組序列不是ASCII編碼,就會違反該函式的契約,進而導致未定義行為。
例項分析
let bytes = vec![0xf7, 0xbf, 0xbf, 0xbf];
let ascii = unsafe {
// 違反契約:bytes包含非ASCII位元組
Ascii::from_bytes_unchecked(bytes)
};
let bogus: String = ascii.into();
// bogus現在持有非法的UTF-8編碼
assert_eq!(bogus.chars().next().unwrap() as u32, 0x1fffff);
內容解密:
- 違反契約的風險:上述程式碼展示瞭如何透過違反
Ascii::from_bytes_unchecked的契約來建構一個包含非法UTF-8編碼的String。這是因為傳入的bytes向量包含了非ASCII位元組。 - 未定義行為的後果:這種錯誤可能在離
unsafe區塊很遠的地方才顯現出來,例如在解析bogus字串的第一個字元時。 - 安全檢查的重要性:Rust的型別檢查、借用檢查等靜態檢查工具試圖證明程式不會出現未定義行為。使用
unsafe關鍵字意味著開發者需要手動保證程式的安全性。
安全介面與契約
在設計函式時,應優先考慮建立安全的介面,避免使用契約。如果實作上需要使用unsafe功能,應該利用Rust的型別系統、生命週期和模組系統來滿足契約要求,而不是將責任推給呼叫者。
Unsafe區塊還是Unsafe函式?
在決定是否將函式標記為unsafe時,應根據該函式是否可能被誤用而導致未定義行為。如果是,則必須標記為unsafe;否則,即使函式內部使用了unsafe功能,也不應標記為unsafe,而是使用unsafe區塊。
判斷準則
- 如果函式可能被誤用而導致未定義行為,則標記為
unsafe。 - 否則,函式是安全的,不應標記為
unsafe。
未定義行為的定義
未定義行為指的是Rust編譯器假設程式不會展現的行為。這種假設使得編譯器能夠進行最佳化,但也意味著一旦出現未定義行為,程式的行為就不可預測。
編譯器最佳化與未定義行為
編譯器在進行最佳化時,會假設程式中不存在未定義行為。例如,對於分享參照,編譯器假設其值不會被改變。如果違反了這一假設,例如透過將分享參照轉換為可變指標並修改其值,就會導致未定義行為。
例項分析
fn very_trustworthy(shared: &i32) {
unsafe {
// 將分享參照轉換為可變指標並修改其值
let mutable = shared as *const i32 as *mut i32;
*mutable = 20;
}
}
內容解密:
- 違反分享參照規則:上述程式碼透過將分享參照轉換為可變指標並修改其值,違反了分享參照的規則。
- 未定義行為的後果:這可能導致編譯器的最佳化產生不可預期的結果,例如在呼叫
very_trustworthy函式後,變數的值可能不是預期的結果。
未定義行為與不安全特徵
在Rust程式設計中,編譯器會對程式碼進行各種轉換和最佳化,以提高執行效率。然而,這些轉換和最佳化都根據一個假設:程式碼是「行為良好」的,也就是說程式碼不會違反Rust的某些基本規則。如果程式碼違反了這些規則,就會出現未定義行為(Undefined Behavior)。
Rust的行為良好規則
為了確保程式碼的正確性和安全性,Rust對行為良好的程式碼有以下規則:
- 程式不能讀取未初始化的記憶體。
- 程式不能建立無效的基本值,例如:
- 空參考或空Box。
- 不是0或1的布林值。
- 具有無效辨別值的列舉值。
- 不是有效的Unicode程式碼點的字元值。
- 不是格式良好的UTF-8的字串值。
- 必須遵循第5章中對參考的規則。參考不能超過其參照物;分享存取是隻讀存取;可變存取是獨佔存取。
- 程式不能取消參照空指標、不正確對齊的指標或懸掛指標。
- 程式不能使用指標存取與指標相關聯的分配之外的記憶體。
- 程式必須沒有資料競爭。資料競爭發生在兩個執行緒在沒有同步的情況下存取相同的記憶體位置,並且至少有一次存取是寫入。
- 程式不能透過外部函式介面(Foreign Function Interface)跨語言呼叫展開(unwind)。
- 程式必須遵守標準函式庫函式的合約。
這些規則是Rust在最佳化和翻譯程式碼成機器語言的過程中所做的假設。未定義行為就是違反這些規則。
不安全特徵
不安全特徵(Unsafe Traits)是一種特徵,它具有Rust無法檢查或執行的合約。實作不安全特徵時,必須將實作標記為unsafe。開發者需要了解特徵的合約,並確保其型別滿足該合約。
典型的例子包括std::marker::Send和std::marker::Sync。這些特徵沒有定義任何方法,因此很容易為任何型別實作。但是,它們具有合約:Send要求實作者可以安全地移動到另一個執行緒,而Sync要求實作者可以透過分享參考線上程之間分享。
另一個例子是core::nonzero::Zeroable,它是一個用於可以安全地透過將所有位元組設為零來初始化的型別的特徵。實作Zeroable時,需要確保該型別滿足其合約,否則可能會導致未定義行為。
pub unsafe trait Zeroable {}
unsafe impl Zeroable for u8 {}
unsafe impl Zeroable for i32 {}
unsafe impl Zeroable for usize {}
有了這些定義,我們可以寫一個函式,快速分配一個包含Zeroable型別的向量:
#![feature(nonzero)]
extern crate core;
use core::nonzero::Zeroable;
fn zeroed_vector<T>(len: usize) -> Vec<T>
where
T: Zeroable,
{
let mut vec = Vec::with_capacity(len);
unsafe {
std::ptr::write_bytes(vec.as_mut_ptr(), 0, len);
vec.set_len(len);
}
vec
}
內容解密:
zeroed_vector函式建立了一個具有指定長度的向量,並使用write_bytes函式將其緩衝區填充為零。write_bytes函式將緩衝區中的每個元素設為零,這裡的長度是指元素的數量,而不是位元組數。set_len方法改變向量的長度,但不對緩衝區做任何事情。這是不安全的,因為你必須確保新包圍的緩衝區空間實際上包含正確初始化的T型別值。T: Zeroable約束確保了零位元組塊代表一個有效的T值,因此使用set_len是安全的。
這個例子展示瞭如何使用不安全特徵來實作高效的記憶體初始化。然而,如果實作不安全特徵時沒有遵守其合約,就可能會導致未定義行為。開發者需要謹慎地實作不安全特徵,並確保其型別滿足特徵的合約。
Rust 中的不安全程式碼處理:原始指標與雜湊器
Rust 是一種強調安全性的系統程式語言,但它也提供了不安全程式碼的選項,讓開發者能夠直接操作記憶體和指標。在本章中,我們將討論 Rust 中的不安全程式碼,特別是原始指標的使用和相關的安全注意事項。
自定義雜湊器的危險性
在 Rust 中,自定義雜湊器(Hasher)需要遵循一定的規則。例如,實作 std::hash::Hasher 特徵時,必須確保相同的位元輸入會產生相同的雜湊值。然而,如果我們實作一個隨機傳回雜湊值的 Hasher,則違反了這個規則。儘管這是不正確的實作,但由於 Hasher 不是一個不安全的特徵,使用它的程式碼不會表現出未定義行為(undefined behavior)。
// 錯誤的雜湊器實作範例
struct RandomHasher;
impl std::hash::Hasher for RandomHasher {
fn write(&mut self, bytes: &[u8]) {
// 隨機生成雜湊值
self.hash = rand::random();
}
fn finish(&self) -> u64 {
self.hash
}
}
內容解密:
- 這段程式碼展示了一個錯誤的
Hasher實作,它隨機生成雜湊值而非根據輸入的位元。 - 違反了
Hasher的基本要求:相同的輸入應該產生相同的輸出。 - 儘管是錯誤的實作,但由於
Hasher不是不安全的特徵,使用它的程式碼(如HashMap)不會產生未定義行為,但會導致錯誤的查詢結果。
原始指標的使用
原始指標(raw pointers)在 Rust 中是一種不受限制的指標,可以用來建立複雜的資料結構,如雙向鏈結串列或任意物件圖。由於原始指標非常靈活,Rust 無法判斷其使用是否安全,因此只能在 unsafe 區塊中對其進行取值運算。
Rust 中的原始指標分為兩種:
*mut T:可修改其參照目標的原始指標。*const T:僅允許讀取其參照目標的原始指標。
let mut x = 10;
let ptr_x = &mut x as *mut i32;
let y = Box::new(20);
let ptr_y = &*y as *const i32;
unsafe {
*ptr_x += *ptr_y;
}
assert_eq!(x, 30);
內容解密:
- 這段程式碼展示瞭如何將參考轉換為原始指標,並在
unsafe區塊中對其進行取值運算。 - 使用原始指標時需要小心,因為它們可以為空(null),並且取值運算需要在
unsafe區塊中進行。 - 原始指標不支援自動取值運算,需要使用
*運算子顯式取值。
原始指標的安全使用
使用原始指標時需要遵循一些基本的安全準則:
- 避免取值空指標或懸掛指標。
- 確保指標對齊其參照型別。
- 借用原始指標的值時,必須遵守參考安全的規則。
fn distance<T>(left: *const T, right: *const T) -> isize {
(left as isize - right as isize) / std::mem::size_of::<T>() as isize
}
let trucks = vec!["garbage truck", "dump truck", "moonstruck"];
let first = &trucks[0];
let last = &trucks[2];
assert_eq!(distance(last, first), 2);
assert_eq!(distance(first, last), -2);
內容解密:
- 這段程式碼展示瞭如何計算兩個指標之間的距離。
- 使用原始指標作為引數,並進行適當的轉換和計算。
- 需要注意的是,這個函式假設兩個指標指向同一個陣列或結構中的元素。