一、前言
為通用類型賦予一個默認的類型,大部分的語言是沒有這個特性的,但是也有例外的,例如TypeScript(可能還有其它)。
例如TypeScript可以這樣使用:
class MyClass<T = number> {
value: T;
constructor(value: T) {
this.value = value;
}
printValue(): void {
console.log(`Value is ${this.value}`);
}
}
const obj1 = new MyClass(42); // 使用默認類型 number
const obj2 = new MyClass<string>("Hello"); // 使用指定類型 string
而運算符重載,則不少語言也支持,最典型的莫過於C++,C#.
但是rust的運算符重載是比較特別的一種,該怎麼説了?
rustc做了太多的工作,而且我覺得有點違背一些通用的設計規則。這是因為例子中的方法必須要求對象實現Copy,但是方法的參數又沒有帶&,會讓人誤會!
不喜歡有太多默認約定的設計,更喜歡每個東西都明明白白地定義。
二、通用類型參數默認類型
讀取來有點拗口,意思就是:
1.在有關對象(struc,特質等)或者方法中使用通用參數T
2.可以為T指定一個默認的類型,語法是T=xxx,其中xxx是某個具體類型
如果你不喜歡T,也可以換成任意合法的rust標識符.
就目前來看,通用參數的默認參數的作用有兩點:運算符重載+方便
三、運算符重載和其它作用
3.1、運算符重載
所謂運算符重載就是除了運算符最原始的功能(編譯器默認支持的)之外,還可以支持其它類型的運算數。
例如+通常用於整數、浮點數等的相加,但通過重載,其它類型對象實例也可以使用+。
以此類推,-*/等運算符號也可以。
不管怎麼説,這算是一個好東西!
只不過類型參數的默認類型好像就是為了運算符重載而存在。
3.2、其它作用
查了一些資料,據説可以結合條件編譯。其它作用就是無關緊要的。
條件編譯示例
// 定義一個特性標誌,用於條件編譯
#[cfg(feature = "use_f64")]
type DefaultNumType = f64;
#[cfg(not(feature = "use_f64"))]
type DefaultNumType = i32;
struct Point<T = DefaultNumType> {
x: T,
y: T,
}
impl<T> Point<T> {
fn new(x: T, y: T) -> Self {
Point { x, y }
}
}
四、示例
4.1、示例代碼
由於例子涉及到Add,Sub兩個特質,所以先列出此二特質的定義:
pub trait Add<Rhs = Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
pub trait Sub<Rhs = Self> {
type Output;
fn sub(self, rhs: Rhs) -> Self::Output;
}
對書本上的例子稍微改造了下:
use std::ops::{Add,Sub};
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
/**
* 這個使用默認類型,來自rust編程語言官方文檔的例子
*/
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
/**
* 實現相減運算符(從而實現Point的-重載),需要實現Sub trait
*/
impl Sub for Point {
type Output = Point;
/**
* 需要特別注意的是兩個參數的定義
* self - 沒有使用引用
* other - 沒有要求引用
* 這種不引用的方式,不同於一般的方法定義
*/
fn sub(self, other: Point) -> Point {
Point {
x: self.x - other.x,
y: self.y - other.y,
}
}
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
//使用重載的方式調用
println!("{:?}+{:?}={:?}",p1,p2, p1 + p2);
println!("{:?}-{:?}={:?}",p1,p2, p1 - p2);
//不使用重載的方式調用
let p3 = p1.add(p2).sub(p2);
let p4 = (p1.sub(p2)).add(p2);
println!("{:?}+{:?}-{:?}={:?}",p1,p2, p2,p3);
println!("{:?}-{:?}+{:?}={:?}",p1,p2,p2, p4);
let lml= Person {name: "lml".to_string()};
let ww= Animal {name: "ww".to_string()};
lml.attack(ww);
println!("{:?}",lml);
}
// --------------------------------------------------------
// 以下的代碼是為了演示 參數不帶&是什麼情況
trait Fight {
type Item;
//fn attack(&self, other: &Self::Item);
fn attack(self, other: Self::Item);
}
#[derive(Debug)]
struct Person {name: String}
#[derive(Debug)]
struct Animal {name: String}
impl Fight for Person {
type Item = Animal;
fn attack(self, other: Self::Item) {
println!(
"{}攻擊了{}",
self.name,
other.name
);
}
}
這個例子做了三件事情:
1.重載+
2.重載-
3.如果不使用Copy特質會怎麼樣
特質Fight和結構體Person,Animal就是為了驗證第3點。
4.2、名詞解釋
在開始執行代碼前,先解釋兩個重要的內容
Rhs
Rhs是 "Right-Hand Side"(右側操作數)的縮寫
關鍵字self和Self
仔細看看,才發現是兩個,不是一個,要知道rust中是區分大小寫的。
self-全小寫,表示對象實例本身
Self-首字母大寫,其它小寫,表示類型本身
例如以下代碼中:
在方法attack中,第一個self表示具體對象實例,第二個Self則表示具體對象類型。
pub trait Add<Rhs = Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
pub trait Sub<Rhs = Self> {
type Output;
fn sub(self, rhs: Rhs) -> Self::Output;
}
現在代碼應該容易看了。
4.3、執行
看看輸出:

只要把示例中如下一部分:
trait Fight {
type Item;
//fn attack(&self, other: &Self::Item);
fn attack(self, other: Self::Item);
}
#[derive(Debug)]
struct Person {name: String}
#[derive(Debug)]
struct Animal {name: String}
impl Fight for Person {
type Item = Animal;
fn attack(self, other: Self::Item) {
println!(
"{}攻擊了{}",
self.name,
other.name
);
}
}
修改為:
trait Fight {
type Item;
fn attack(&self, other: &Self::Item);
//fn attack(self, other: Self::Item);
}
#[derive(Debug)]
struct Person {name: String}
#[derive(Debug)]
struct Animal {name: String}
impl Fight for Person {
type Item = Animal;
fn attack(&self, other: &Self::Item) {
println!(
"{}攻擊了{}",
self.name,
other.name
);
}
}
再把main的調用修改為:
那麼就可以正確輸出:

為什麼在Point上沒有這個問題了?這是因為Point實現了Copy特質,看下面的代碼:#[derive(Debug, Copy, Clone, PartialEq)]
rust的Copy特質奇怪的作用:允許類型進行隱式的、按位的複製,適用於簡單數據類型,避免不必要的所有權移動,提升代碼效率和便利性。同時,強調其使用條件和限制,幫助用户正確理解和應用
什麼是按位複製?
也就是説,當賦值或作為函數參數傳遞時,不需要移動所有權,而是直接複製。不過,只有滿足某些條件的類型才能實現Copy,比如所有字段都實現了Copy,並且類型本身沒有實現Drop trait
所以,上例中,即使在方法中沒有定義為引用類型,它也不會報錯。而Person並沒有實現Copy特質,所以會發生這個問題。
五、示例2
以下的示例演示了一個只包含字符串切片的struct如何相加
use std::ops::Add;
#[derive(Debug,Clone,Copy)]
struct Name<'a>{
name:&'a str
}
impl<'a> Add for Name<'a>{
type Output = Name<'a>;
fn add(self, other: Self) -> Self {
let tmp=format!("{} {}",self.name,other.name);
//Box::leak的作用是 將堆上的內存轉換為 'static 生命週期的引用
let leaked_str: &'static str = Box::leak(tmp.into_boxed_str());
Self{name: leaked_str }
}
}
fn main() {
let name1 = Name{name:"lu"};
let name2 = Name{name:" mula"};
let name3=name1+name2;
println!("name1={:?},name2={:?}", name1,name2);
println!("name3={:?}", name3);
let name4=String::from("lu");
let name5=String::from("lu");
let name6=add(name4,name5);
println!("name6={:?}", name6);
}
fn add(name1:String,name2:String)->String{
format!("{} {}",name1,name2)
}
運行結果:

本例主要説明以下幾個問題:
1.字符串切片可以Copy.所以包含字符串切片的類型也可以實現+等重載
2.實現和字符串切片有關的內容太麻煩,導出是奇怪的生命週期符號
六、小結
1.rust通過定義通用類型參數的默認類型來實現運算符重載
2.但也不是什麼對象都可以重載,最好是能夠實現Copy特質的類型,否則可能失敗