在 Rust 開發中,理解複合指定運算元與相等性測試的運作原理至關重要。這些操作仰賴 std::ops 模組中的特定 trait,例如 AddAssign、SubAssign 等用於複合指定,而 PartialEq 則用於相等性測試。對於自定義型別,我們需要實作對應的 trait 才能使用這些運算元。由於 IEEE 浮點數標準中 NaN 的特殊性,浮點數的相等性比較並不滿足所有等價關係的特性,因此 Rust 使用 PartialEq 而非 Eq。文章中以 Complex 複數型別為例,示範如何實作 AddAssign 和 PartialEq,並解釋瞭如何處理 NaN 的比較問題。此外,PartialOrd trait 則用於定義大小比較運算元,文章以 Interval 區間型別為例,說明瞭如何實作 partial_cmp 方法來處理區間的比較邏輯,特別是重疊區間的無序性。
複合指定運算元與相等性測試
在Rust中,複合指定運算元(如+=、-=、*=等)以及相等性測試(如==和!=)是透過特定的trait來實作的。本章將詳細介紹這些運算元的實作方式及其背後的trait。
複合指定運算元
Rust的複合指定運算元是由std::ops模組中的特定trait來支援的。例如,+=運算元對應於AddAssign trait。以下是AddAssign trait的定義:
trait AddAssign<RHS = Self> {
fn add_assign(&mut self, rhs: RHS);
}
如表12-4所示,Rust為各種運算元提供了對應的trait:
- 算術運算元:
AddAssign、SubAssign、MulAssign、DivAssign、RemAssign - 位元運算元:
BitAndAssign、BitOrAssign、BitXorAssign、ShlAssign、ShrAssign
所有Rust的數值型別都實作了算術複合指定運算元,而整數型別和布林型別則實作了位元複合指定運算元。
自定義型別實作複合指定運算元
為了讓自定義型別支援複合指定運算元,需要為其實作相應的trait。以我們之前定義的Complex型別為例,實作AddAssign trait如下:
use std::ops::AddAssign;
impl<T> AddAssign for Complex<T>
where
T: AddAssign,
{
fn add_assign(&mut self, rhs: Complex<T>) {
self.re += rhs.re;
self.im += rhs.im;
}
}
這段程式碼實作在Complex<T>型別上的+=運算,使得兩個複數可以相加。
內容解密:
AddAssigntrait定義了add_assign方法,用於實作+=運算。- 在為
Complex<T>實作AddAssign時,要求T型別本身必須實作AddAssign,這樣才能對複數的實部和虛部分別進行加法運算。 add_assign方法更新了當前複數(即self)的實部和虛部,將另一個複數(即rhs)的對應部分加到其上。
相等性測試
Rust中的相等性測試是透過實作std::cmp::PartialEq trait來完成的。該trait定義了兩個方法:eq和ne,分別對應於==和!=運算元。
PartialEq trait的定義
trait PartialEq<Rhs: ?Sized = Self> {
fn eq(&self, other: &Rhs) -> bool;
fn ne(&self, other: &Rhs) -> bool { !self.eq(other) }
}
為自定義型別實作PartialEq
對於我們的Complex<T>型別,可以如下實作:
impl<T: PartialEq> PartialEq for Complex<T> {
fn eq(&self, other: &Complex<T>) -> bool {
self.re == other.re && self.im == other.im
}
}
這裡假設了元件型別T已經實作了PartialEq。這樣,兩個複數被視為相等當且僅當它們的實部和虛部分別相等。
自動派生實作
Rust允許透過在型別定義上新增屬性來自動派生某些trait的實作,例如:
#[derive(Clone, Copy, Debug, PartialEq)]
struct Complex<T> {
// ...
}
這將自動為我們的結構體生成一個類別似於手寫的、逐場比較的實作。
內容解密:
PartialEqtrait用於比較兩個值的相等性。- 在比較非複製(non-Copy)型別的值時,透過參照來進行比較,以避免值的移動。
PartialEq<Rhs>中的界限Rhs: ?Sized = Self>表示右手邊的運算元可以是任意大小的型別,允許比較像是字串切片(&str)或切片(&[T])這樣的無大小(unsized)型別。
為什麼是PartialEq?
傳統數學中,等價關係需要滿足三個條件:反身性、對稱性和傳遞性。然而,Rust中的浮點數遵循IEEE標準,該標準規定NaN(Not a Number)與任何值都不相等,包括它自己。這使得浮點數上的等式不是一個完全的等價關係,因此相關trait被命名為PartialEq而非Eq。
內容解密:
- IEEE浮點數標準規定NaN不等於任何值,包括它自己,這破壞了等價關係中的反身性。
- 因此,Rust使用PartialEq來表示可能不完全滿足等價關係的相等性比較。
Rust 中的 PartialEq 與 PartialOrd 實作
Rust 語言中,對於等式比較和順序比較的運算子提供了特定的 trait 來規範其行為。在本章中,我們將詳細討論 PartialEq 和 PartialOrd 這兩個 trait,以及如何在自定義型別中實作它們。
PartialEq 實作
PartialEq trait 用於定義 == 和 != 運算子的行為。該 trait 要求實作 eq 方法,用於比較兩個值是否相等。Rust 的標準函式庫中,大多數型別都實作了 PartialEq,但對於浮點數型別(如 f32 和 f64),由於 NaN(Not a Number)的存在,它們的比較行為比較特殊。
NaN 的比較行為
根據 IEEE 浮點數標準,NaN 與任何值(包括其自身)的比較結果都是無序的,也就是說,NaN == NaN 和 NaN != NaN 都傳回 false。這種行為違背了等價關係的第三條規則(自反性),因此,浮點數型別的 PartialEq 實作並不滿足完整的等價關係,而是一種部分等價關係。
實作 PartialEq
對於自定義型別,如以下的 Complex 型別,可以透過 derive 屬性自動實作 PartialEq:
#[derive(PartialEq)]
struct Complex<T> {
re: T,
im: T,
}
或者手動實作:
impl<T: PartialEq> PartialEq for Complex<T> {
fn eq(&self, other: &Self) -> bool {
self.re == other.re && self.im == other.im
}
}
Eq 實作
如果一個型別滿足完整的等價關係(即自反性、對稱性和傳遞性),則可以實作 Eq trait。Eq trait 繼承自 PartialEq,並且不新增任何方法。對於大多數型別,如果它們實作了 PartialEq,也應該實作 Eq。浮點數型別是標準函式庫中少數不實作 Eq 的型別。
impl<T: Eq> Eq for Complex<T> {}
或者在 derive 屬性中直接包含 Eq:
#[derive(PartialEq, Eq)]
struct Complex<T> {
re: T,
im: T,
}
PartialOrd 實作
PartialOrd trait 用於定義 <, >, <=, 和 >= 運算子的行為。它要求實作 partial_cmp 方法,該方法傳回一個 Option<Ordering> 值。如果傳回 Some(ordering),則表示兩個值之間存在特定的順序;如果傳回 None,則表示兩個值無序。
NaN 的順序比較行為
對於浮點數型別,如果比較的一方或雙方是 NaN,則 partial_cmp 傳回 None,表示無序。
實作 PartialOrd
對於自定義型別,如以下的 Interval 型別,可以實作 PartialOrd 以定義其順序比較行為:
#[derive(Debug, PartialEq)]
struct Interval<T> {
lower: T, // inclusive
upper: T // exclusive
}
impl<T: PartialOrd> PartialOrd for Interval<T> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if self.upper <= other.lower {
Some(Ordering::Less)
} else if self.lower >= other.upper {
Some(Ordering::Greater)
} else if self == other {
Some(Ordering::Equal)
} else {
None
}
}
}
內容解密:
- 檢查是否完全小於對方區間:如果當前區間的上界小於或等於對方區間的下界,則當前區間完全在對方區間之前,因此傳回
Some(Ordering::Less)。 - 檢查是否完全大於對方區間:如果當前區間的下界大於或等於對方區間的上界,則當前區間完全在對方區間之後,因此傳回
Some(Ordering::Greater)。 - 檢查是否相等:如果當前區間與對方區間相等(根據先前實作的
PartialEq),則傳回Some(Ordering::Equal)。 - 其他情況:如果以上條件都不滿足,表示兩個區間重疊,因此傳回
None表示無序。
Rust 中的運算元多載
Rust 語言允許開發者透過特定的 trait 來多載某些運算元,使自定義型別能夠支援各種運算操作。這種機制增強了語言的表達能力和程式碼的可讀性。
PartialOrd 與 Interval 實作
首先,我們來看如何為自定義的 Interval<T> 型別實作 PartialOrd trait,使其能夠進行比較操作。
use std::cmp::{Ordering, PartialOrd};
impl<T: PartialOrd> PartialOrd<Interval<T>> for Interval<T> {
fn partial_cmp(&self, other: &Interval<T>) -> Option<Ordering> {
if self == other {
Some(Ordering::Equal)
} else if self.lower >= other.upper {
Some(Ordering::Greater)
} else if self.upper <= other.lower {
Some(Ordering::Less)
} else {
None
}
}
}
內容解密:
PartialOrdtrait 的實作允許Interval<T>進行部分比較。- 當兩個區間完全相等時,傳回
Some(Ordering::Equal)。 - 如果
self的下界大於或等於other的上界,則self大於other,傳回Some(Ordering::Greater)。 - 如果
self的上界小於或等於other的下界,則self小於other,傳回Some(Ordering::Less)。 - 當兩個區間重疊時,無法確定它們的相對大小,因此傳回
None。
有了這個實作,我們可以對 Interval 例項進行比較:
assert!(Interval { lower: 10, upper: 20 } < Interval { lower: 20, upper: 40 });
assert!(Interval { lower: 7, upper: 8 } >= Interval { lower: 0, upper: 1 });
assert!(Interval { lower: 7, upper: 8 } <= Interval { lower: 7, upper: 8 });
// 重疊的區間無法比較
let left = Interval { lower: 10, upper: 30 };
let right = Interval { lower: 20, upper: 40 };
assert!(!(left < right));
assert!(!(left >= right));
Index 與 IndexMut
Rust 中的 Index 和 IndexMut trait 用於定義索引操作,例如 a[i]。這兩個 trait 分別用於不可變和可變的索引存取。
trait Index<Idx> {
type Output: ?Sized;
fn index(&self, index: Idx) -> &Self::Output;
}
trait IndexMut<Idx>: Index<Idx> {
fn index_mut(&mut self, index: Idx) -> &mut Self::Output;
}
內容解密:
Indextrait 定義了一個方法index,它接受一個索引並傳回對應值的不可變參照。IndexMuttrait 繼承自Index,並新增了一個方法index_mut,用於傳回可變參照。- 這兩個 trait 的實作使得自定義型別能夠支援類別似陣列的索引操作。
Image 型別的 Index 與 IndexMut 實作
為了使一個影像型別 Image<P> 能夠像二維陣列一樣被索引,我們需要為它實作 Index 和 IndexMut。
struct Image<P> {
width: usize,
pixels: Vec<P>
}
impl<P> std::ops::Index<usize> for Image<P> {
type Output = [P];
fn index(&self, row: usize) -> &[P] {
let start = row * self.width;
&self.pixels[start .. start + self.width]
}
}
impl<P> std::ops::IndexMut<usize> for Image<P> {
fn index_mut(&mut self, row: usize) -> &mut [P] {
let start = row * self.width;
&mut self.pixels[start .. start + self.width]
}
}
內容解密:
- 對
Image<P>的索引操作傳回一個畫素值的切片[P]。 - 索引操作的實作是根據將二維索引轉換為一維索引在
pixels向量上進行的。 - 當對
Image進行索引時,如果行索引超出範圍,將觸發 panic。
其他運算元
Rust 不允許所有運算元被多載。某些運算元,如邏輯運算元、範圍運算元等,有固定的行為,不能被改變。此外,函式呼叫運算元和解參照運算元可以透過其他 trait(如 Deref 和 Fn 相關的 trait)進行多載。
內容解密:
- Rust 對某些運算元的行為有嚴格的規定,不允許透過多載改變其語義。
- 解參照運算元
*和點運算元.可以透過實作特定的 trait(如Deref和DerefMut)來改變其行為。 - 函式呼叫運算元可以透過閉包或實作
Fn、FnMut和FnOncetrait 的型別來使用。
總之,Rust 的運算元多載機制提供了靈活性和表達能力,但也需要謹慎使用,以避免混淆和錯誤。開發者應根據具體情況選擇適當的 trait 和實作方式,以達到最佳的程式碼品質。