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 selfself 參數來訪問或修改結構體的屬性,這類似於 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 是一個結構體,擁有兩個屬性:widthheight,它們的類型都是 u32(無符號 32 位整數)。這些屬性用來表示矩形的寬度和高度。

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

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

    • fn 是定義函數或方法的關鍵字。
    • area 是方法的名稱,用來計算矩形的面積。
    • &self 是這個方法的第一個參數,表示方法作用於 Rectangle 的一個實例上。
    • &self 是一個不可變引用,這意味著該方法只能讀取結構體的屬性,而不能修改它們。如果需要修改屬性,可以使用 &mut selfself
    • -> u32 表示該方法的返回值類型是 u32,即一個無符號 32 位整數。
  4. 方法內部實作
    self.width * self.height 計算矩形的面積,這裡的 self 代表方法所作用的 Rectangle 實例。self.widthself.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 提供雜湊運算的能力,允許物件被雜湊處理。 用於集合類別如 HashMapHashSet,需要物件能夠被雜湊的情境。
Display 用於格式化輸出,允許物件以人類可讀的方式被輸出(如 println!("{}", obj))。 用於需要以清晰方式輸出文字或數值的情境,常見於用戶介面輸出。
From 用於類別轉換,允許一個類別轉換為另一個類別。 使用於需要類別間互相轉換的情境,方便進行類別間的適配操作。
Into 類似 From,但操作方向相反,允許類別轉換。 常用於方法內自動轉換參數的類別,以增強程式碼的靈活性。
Iterator 提供遍歷序列的功能,允許物件作為迭代器使用。 用於需要遍歷集合或自定義序列的情境,例如 for 迴圈或 map, filter 等操作。
Deref 提供自動解引用能力,允許像使用指標一樣訪問內部資料。 常見於智能指標(如 Box, Rc)中,自動解引用以簡化程式碼。
Drop 定義清理資源的方式,在物件被釋放時執行。 用於釋放記憶體或其他資源(如檔案、網路連線)時,避免記憶體洩漏。
AsRef 提供引用轉換,允許將物件引用轉換為另一類別的引用。 用於需要對物件做參考轉換時,避免不必要的拷貝。
AsMut AsRef 類似,但用於可變引用的轉換。 用於需要修改物件的情境,例如操作資料結構時。
Send 標誌類別是安全地傳遞給其他執行緒使用的。 用於多執行緒程式設計中,保證物件在執行緒間傳遞的安全性。
Sync 標誌類別可以安全地被多個執行緒共享。 用於多執行緒程式中,共享資料的安全性保證。
FnFnMutFnOnce 表示不同類型的函數或閉包,可以用於回呼、函數指標等場景。 用於需要將函數作為參數傳遞或做為回呼時,分別控制函數的呼叫次數和行為。

這些 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 —— 從語法基礎到實戰應用14
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言