iT邦幫忙

2024 iThome 鐵人賽

DAY 7
1
自我挑戰組

從 Python 開發者的角度學習 Rust —— 從語法基礎到實戰應用系列 第 7 篇

[Day 7] 函數與方法定義:如何在 Rust 中設計函數

  • 分享至 

  • xImage
  •  

在程式設計中,函數(functions)是將代碼邏輯模組化並重複使用的核心工具。Rust 作為一個強類型語言,在函數設計方面與 Python 有很多相似之處,但也有其獨特的設計特性。本篇文章將深入探討 Rust 中的函數定義、參數處理、返回值與方法(methods)定義,並對比 Python 的實現方式,幫助 Python 開發者快速掌握 Rust 中函數的使用。


一、Rust 中的函數基礎

1. 函數定義

在 Rust 中,函數使用 fn 關鍵字來定義,並且類型系統非常嚴謹,因此必須明確指定參數和返回值的類型。與 Python 的動態類型不同,Rust 需要在編譯時確定所有類型。

Rust 的函數定義範例:

fn add(x: i32, y: i32) -> i32 {
    x + y
}

在這個範例中:

  • fn 是用來定義函數的關鍵字。
  • add 是函數名稱。
  • (x: i32, y: i32) 是函數的參數,分別有 x, y兩個參數,皆為 i32 類型。
  • -> i32 表示這個函數會返回一個 i32 類別的值。
  • 最後的 x + y 是這個函數的返回值,在 Rust 中,函數的返回值可以省略 return,只需要去掉分號即可。

Python 的函數定義範例:

def add(x, y):
    return x + y

Python 的函數定義較為簡潔,不需要聲明參數和返回值的類型,這給開發帶來靈活性,但也可能導致類型錯誤。

比較:

  • Python:函數參數和返回值的類型可以是動態的,允許更靈活的開發方式。
  • Rust:需要在編譯時期明確指定參數和返回值的類型,確保代碼的安全性和效能。

二、參數與返回值

1. 參數定義

Rust 中的函數參數需要明確指定其類型,這有助於提高程式的可讀性與安全性。例如,Rust 不允許模糊的類型推斷,必須明確地定義每個參數的類型。

fn multiply(x: i32, y: i32) -> i32 {
    x * y
}

Rust 可以使用可變參數,透過 &mut 來將變數以可變借用的方式傳遞給函數進行修改:

fn increment(value: &mut i32) {
    *value += 1;
}

在這裡,&mut 用來表示傳遞一個可變引用,使得函數可以修改原變數的值。

Python 的參數定義

Python 的參數沒有類型約束,可以自由傳遞和修改。

def increment(value):
    return value + 1

2. 返回值

Rust 函數的返回值可以是任意類型,且不需要像 Python 一樣顯式使用 return,只要將最後一行作為表達式即可:

fn add_one(x: i32) -> i32 {
    x + 1
}

當需要提前返回值時,Rust 也可以使用 return 關鍵字:

fn early_return(x: i32) -> i32 {
    if x > 10 {
        return x;
    }
    x + 1
}

比較:

  • Python:參數可以動態變化,沒有類型約束;返回值必須使用 return 關鍵字。
  • Rust:參數必須明確指定類型,函數的最後一行會自動作為返回值,無需 return 關鍵字,但需要定義返回值的類型。

三、函數作為一等公民(Functions as First-Class Citizens)特性

在 Rust 中,函數是一等公民,「函數作為一等公民」(Functions as First-Class Citizens)是程式設計中的正式概念,用來描述語言的某一特性。當我們說「函數是程式中的一等公民」,意思是函數在語言中被視為和其他基本資料型態(如數字、字串、變數等)一樣的對待。這意味著函數可以被賦值給變數、作為參數傳入其他函數、或從其他函數中返回,這些功能提供了更大的靈活性和可擴展性,這與 Python 的函數具有類似特性。

以下展示函數作為變數與參數的範例

函數作為變數

fn add(x: i32, y: i32) -> i32 {
    x + y
}

fn main() {
    let operation = add;
    println!("結果:{}", operation(5, 3));
}

在這裡,我們將 add 函數賦值給 operation 變數,然後透過 operation 來調用該函數。

函數作為參數

Rust 中,函數也可以作為參數傳遞給其他函數,例如:

fn apply_function(f: fn(i32, i32) -> i32, a: i32, b: i32) -> i32 {
    f(a, b)
}

fn add(x: i32, y: i32) -> i32 {
    x + y
}

fn main() {
    let result = apply_function(add, 2, 3);
    println!("結果:{}", result);
}

這裡 apply_function 函數接收一個函數作為參數,然後在內部調用該函數。

Python 的函數作為參數

Python 也可以將函數作為參數傳遞:

def add(x, y):
    return x + y

def apply_function(f, a, b):
    return f(a, b)

result = apply_function(add, 2, 3)
print(f"結果:{result}")

四、方法定義

在 Rust 中,方法是與結構體(struct)或其他類型(如 enum、trait)相關聯的函數。方法的定義使用 impl 區塊,該區塊內部包含了一個或多個與該結構體相關聯的函數。Rust 中的方法透過 &self、&mut self 或 self 參數來訪問或修改結構體的屬性,這類似於 Python 中的方法使用 self 來表示實例。

Rust 的方法定義詳解

在下面的範例中,我們定義了一個 Rectangle 結構體,並為它實現了一個計算面積的 area 方法。

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect = Rectangle { width: 30, height: 50 };
    println!("面積:{}", rect.area());
}

詳細說明:

  1. 結構體定義 (struct):
    Rectangle 是一個結構體,擁有兩個屬性:width 和 height,它們的類型都是 u32(無符號 32 位整數)。這些屬性用來表示矩形的寬度和高度。

  2. impl 區塊:
    impl 區塊用於為結構體 Rectangle 定義方法。Rust 中的方法定義必須放在 impl 區塊內,並且這些方法會與該結構體實例相關聯。

  3. 方法定義 (fn area(&self) -> u32):

    • fn 是定義函數或方法的關鍵字。
    • area 是方法的名稱,用來計算矩形的面積。
    • &self 是這個方法的第一個參數,表示方法作用於 Rectangle 的一個實例上。
    • &self 是一個不可變引用,這意味著該方法只能讀取結構體的屬性,而不能修改它們。如果需要修改屬性,可以使用 &mut self 或 self。
    • -> u32 表示該方法的返回值類型是 u32,即一個無符號 32 位整數。
  4. 方法內部實作:
    self.width * self.height 計算矩形的面積,這裡的 self 代表方法所作用的 Rectangle 實例。self.width 和 self.height 分別訪問矩形的寬度和高度屬性。

  5. 呼叫方法:
    在 main 函數中,創建了一個 Rectangle 實例 rect,並設定寬度為 30、高度為 50。接著,我們呼叫 rect.area() 方法來計算並打印矩形的面積。

關於 self 的三種使用方式:

  • &self:不可變引用,只能讀取屬性,不能修改。
  • &mut self:可變引用,允許修改屬性。
  • self:取得實例的所有權,通常用於需要消耗或轉移所有權的操作。

這種方法定義方式確保了結構體的屬性訪問和修改是安全且受控的,這是 Rust 語言中所有權和借用機制的一部分,有助於避免常見的記憶體錯誤。

比較 Python 的方法定義

Python 的方法定義使用 class 關鍵字來創建類別,並透過 def 關鍵字來定義方法。在 Python 中,self 必須是方法的第一個參數,用來指代實例本身,但並不需要使用特殊的語法來標示它是引用或所有權:

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

rect = Rectangle(30, 50)
print(f"面積:{rect.area()}")

Python 的方法設計較為簡單,但不提供 Rust 的所有權控制和借用的靈活性。在 Rust 中明確指定 &self 或 &mut self,可以大大提高程式的安全性,避免非預期的行為。


五、泛型函數

泛型函數的意思是,你可以寫一個函數,讓它可以處理不同類別的資料,而不需要針對每一種資料類別寫重複的程式碼。這就像是寫了一個萬用的工具,無論是整數、浮點數、字元等,只要符合某些規則,它都可以使用。

在 Rust 中,泛型類別是透過尖括號 <T> 來標示的,其中 T 可以是任何類別。這樣設計的目的是讓程式碼更靈活、重複利用,同時保持安全性和效能。

Rust 泛型函數範例解釋

這是我們的泛型函數範例:

fn largest<T: PartialOrd>(list: &[T]) -> &T { // 定義泛型函數 largest,接收一個 T 類別的切片,並返回 T 類別元素的引用
    let mut largest = &list[0]; // 將列表的第一個元素的引用設為初始最大值
    for item in list { // 遍歷列表中的每一個元素
        if item > largest { // 如果當前元素大於目前的最大值
            largest = item; // 更新最大值為當前元素
        }
    }
    largest // 返回最大的元素的引用
}

這個範例定義了一個叫 largest 的泛型函數,用來找出一個列表中最大的元素。這邊 T 是我們的「萬用類別」,就像是占位符一樣,可以代表任何符合條件的類別。

Python 類似範例

在 Python 裡,我們可以用內建的 max() 函數來實現相似的功能:

def largest(list):
    return max(list)

number_list = [34, 50, 25, 100, 65]
print(f"最大值是:{largest(number_list)}")  # 輸出:最大值是:100

char_list = ['y', 'm', 'a', 'q']
print(f"最大字母是:{largest(char_list)}")  # 輸出:最大字母是:y

但在 Python 裡,max() 是針對特定類別實作的,而 Rust 的 largest 泛型函數允許我們針對任何實作了 PartialOrd 的類別來使用,同時它在編譯時會檢查類別是否符合要求,保證了安全性。

泛型中常用的Trait

在 Rust 中,泛型常常與 trait 結合使用來增加約束,這些約束決定了泛型類別能夠使用哪些方法或行為。下面是一些常見的 trait 及其用途,並以表格整理。

Trait 用途簡介 常見用途
PartialOrd 用於實現部分排序,允許比較大小。 用於需要比較大小的情境,如排序、查找最大值或最小值等。
Ord 用於完全排序,實現全域比較(如 ==, <= 等操作)。 完全排序需要的情境,如排序集合。
PartialEq 提供部分相等性比較,允許使用 == 和 != 比較。 用於比較兩個值是否相等,常見於查找或過濾操作。
Eq 表示完全相等,通常與 PartialEq 搭配使用。 用於需要完全相等比較的情況。
Clone 用於深複製,允許創建一個資料的拷貝。 當你需要複製物件或結構體,而不希望改變原始資料時。
Copy 表示簡單資料可以被位元複製,無需實作深複製。 適用於簡單類別(如整數、浮點數),方便複製數值而不需要消耗記憶體。
Debug 提供格式化輸出,允許使用 {:?} 來打印物件。 用於開發時的除錯和顯示物件內部狀態。
Default 提供預設值的能力,可以為類別生成一個預設的實例。 用於需要創建預設值的物件,例如空結構體或初始化資料。
Hash 提供雜湊運算的能力,允許物件被雜湊處理。 用於集合類別如 HashMap 或 HashSet,需要物件能夠被雜湊的情境。
Display 用於格式化輸出,允許物件以人類可讀的方式被輸出(如 println!("{}", obj))。 用於需要以清晰方式輸出文字或數值的情境,常見於用戶介面輸出。
From 用於類別轉換,允許一個類別轉換為另一個類別。 使用於需要類別間互相轉換的情境,方便進行類別間的適配操作。
Into 類似 From,但操作方向相反,允許類別轉換。 常用於方法內自動轉換參數的類別,以增強程式碼的靈活性。
Iterator 提供遍歷序列的功能,允許物件作為迭代器使用。 用於需要遍歷集合或自定義序列的情境,例如 for 迴圈或 map, filter 等操作。
Deref 提供自動解引用能力,允許像使用指標一樣訪問內部資料。 常見於智能指標(如 Box, Rc)中,自動解引用以簡化程式碼。
Drop 定義清理資源的方式,在物件被釋放時執行。 用於釋放記憶體或其他資源(如檔案、網路連線)時,避免記憶體洩漏。
AsRef 提供引用轉換,允許將物件引用轉換為另一類別的引用。 用於需要對物件做參考轉換時,避免不必要的拷貝。
AsMut 與 AsRef 類似,但用於可變引用的轉換。 用於需要修改物件的情境,例如操作資料結構時。
Send 標誌類別是安全地傳遞給其他執行緒使用的。 用於多執行緒程式設計中,保證物件在執行緒間傳遞的安全性。
Sync 標誌類別可以安全地被多個執行緒共享。 用於多執行緒程式中,共享資料的安全性保證。
Fn、FnMut、FnOnce 表示不同類型的函數或閉包,可以用於回呼、函數指標等場景。 用於需要將函數作為參數傳遞或做為回呼時,分別控制函數的呼叫次數和行為。

這些 trait 提供了許多常用的能力,讓泛型函數能在不同情境下保持靈活性,同時不失去安全性。透過合理運用這些約束,你可以寫出更泛用且高效的程式碼。

建立泛型函數的語法總結

在 Rust 中,建立泛型函數需要遵循特定的語法結構。以下是建立泛型函數的基本語法模板,並搭配簡單的說明,幫助你在實際應用時能快速上手。

泛型函數的語法模板

fn function_name<T: Trait>(parameters: &T) -> ReturnType {
    // 函數邏輯
}

語法拆解與說明

  1. T 是什麼?

    • T 是一個泛型類別的佔位符,可以代表任意類別(例如:整數、字串、浮點數等)。
    • T 並不限制於某一個特定類別,而是通用的表示方式。
  2. Trait 是什麼?

    • Trait 是用來約束 T 的篩選器,可以理解為一組行為或能力的集合。
    • 這些約束用來篩選 T 必須具備哪些特性,例如 PartialOrd 代表必須能夠進行比較,Clone 代表必須能夠被複製。
    • 換句話說,Trait 定義了 T 可以做什麼,或是被要求具備什麼樣的行為。
  3. parameters: &T 是什麼意思?

    • parameters 是函數所使用的參數名稱。
    • &T 表示這個參數是類別 T 的引用。引用在 Rust 中用來避免複製整個物件,節省記憶體並提升效能。
    • 這表示我們傳遞給函數的資料不會被消耗或改變。
  4. 總結理解:

    • T 是泛型資料的代名詞,可以代表各種類別。
    • Trait 是用來約束 T 的能力或行為的篩選器。
    • parameters 是泛型函數所接收的實際參數,&T 說明了這個參數是引用類型的 T。
    • 整體結構的作用是讓函數可以更靈活地處理不同類別的資料,且能夠確保資料符合所需的特定行為(由 Trait 定義)。
  5. -> ReturnType:這部分定義了函數的返回類別。可以是 T、&T 或其他具體類別,依據函數的設計需求來決定。

使用時的注意事項:

  • 確認泛型類別 T 是否符合你設定的 trait 約束,例如:PartialOrd 是用於比較大小的情況。
  • 泛型函數可以大幅提高程式碼重用性,並且可以輕鬆適應多種不同的資料類別。
  • 如果需要更複雜的操作,考慮搭配多個 trait 使用,例如 T: PartialOrd + Clone 允許同時比較大小並進行複製。

六、結論

可以看得出來,其實Python的函數與類別定義依舊具有相當的自由性,因此當我們開始轉到Rust進行開發時,函數定義的參數與返回值的類別需要事先定義一定會很不習慣,但正也是因為如此,Rust函數中避開了資料類型的不確定性,而能夠提升程式運作的效能與安全性,所以也只能多多熟悉Rust的函數撰寫方式囉。


上一篇
[Day 6] Rust 的流程控制(if, loop, match):與 Python 的對比
下一篇
[Day 8] 結構體與元組:自定義類別
系列文
從 Python 開發者的角度學習 Rust —— 從語法基礎到實戰應用 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言