Rust 中的泛型和 Const 泛型

通常,我們編寫的函數參數一般接受具體類型來或任何用戶定義的類型,即當我們傳遞類型給參數時,類型是已知的。

fn sum_i32(i: &[i32]) -> i32 {
    i.iter().sum()
}
fn sum_f32(i: &[f32]) -> f32 {
    i.iter().sum()
}
fn sum_i8(i: &[i8]) -> i8 {
    i.iter().sum()
}
fn main() {
    println!("{}", sum_i8(&[1i8, 4, 6, 7]));
    println!("{}", sum_i32(&[1, 2, 3, 4]));
    println!("{}", sum_f32(&[1.0f32, 2.0, 3.0, 4.0]));
}

在上面的代碼中,用於對給定切片的元素求和。這裏的問題是,Rust 中總共有十個整數類型,還有兩個浮點類型。

如果我們要爲每種類型寫求和函數,那麼我們總共有 12 個實現來完成求和任務。對於其他任務,比如在切片中查找元素,我們關心的每種類型都需要相同數量的實現。對於一個簡單的任務,我們不僅有更多的代碼,而且測試它們也很重要,因爲我們必須爲相同類型和其他類型編寫不同的測試,這增加了代碼的大小,從而增加了 bug 的可能性。

這對於任何涉及不同類型的相同算法的任務都是相同的。爲了解決這個問題,編程語言必須提供一種通過泛型來定義抽象算法的方法,而不僅僅是像上面的代碼那樣爲每個類型去寫具體的函數。

泛型術語

每種語言都提供了一些定義泛型的方法,這裏我們將看到 Rust 如何定義泛型以及泛型的實現。在討論 rust 泛型之前,我們必須討論一些與泛型相關的術語。

泛型是一種重用代碼或算法的方法,它們共享一些共同的操作和屬性,並且避免大量的樣板代碼,減少了源代碼的大小,從而減少了代碼維護和 bug。

Rust 泛型的優勢

Rust 編程語言中泛型的優勢如下:

泛型:靜態分發

下面的代碼是靜態分發的:

use std::iter::Sum;
fn main() {
   println!("i8 Sum: {} \nu16 Sum: {} \nusize Sum: {} \nf64 Sum: {} \nf32 Sum: {} " 
    ,generic_sum(&[1i8, 4, 6, 7])
    ,generic_sum(&[1u16, 5, 9, 56])
    ,generic_sum(&[9usize, 34, 53, 57])
    ,generic_sum(&[1.9, 4.6, 6.7, 7.9])
    ,generic_sum(&[1.0f32, 2.0, 3.0, 4.0]));
}

fn generic_sum<T: Sum<T> + Copy>(i: &[T]) -> T {
    i.iter().copied().sum()
}

類型 T 是泛型,在冒號後面,它們被稱爲 trait 邊界,不是任何具體類型,而是同時實現 Sum 和 Copy trait 的類型。這就是爲什麼像下面這樣的 rust 代碼一開始就無法編譯的原因,因爲不是所有類型都可以 add。與 C++ 模板不同,rust 在編譯時提供了安全性,錯誤消息爲問題提供了更多的上下文。

fn add<T>(x: T,y: T) -> T {
   return x + y;
}

上面的代碼需要強制使用約束類型,從而避免運行時錯誤。我們可以在函數簽名中定義任意多的類型,但最好只使用幾種類型。

use std::ops::{Add, Mul};

fn add_mul<A, B>(x: A, y: A, w: B, z: B) -> B
where
    A: Add<A> + Copy,
    B: Mul<B, Output = B> + Copy,
{
    let var = x + y;
    w * z
}

Const 泛型

Const 泛型是對值而不是類型的泛型。在定義任務時,當我們希望值是通用的而不是特定的值時,這很有用。const 泛型僅支持整數、Bool、Char 類型。

use std::fmt::{Debug, Display};

fn main() {
    const_generics1::<true, &str>("string");
    const_generics1::<false, i32>(67);

    // 在這段代碼中,rust能夠推斷類型和值。
    const_generics2([1, 2, 3, 4]);
    const_generics2(['a''b''c']);

    let m: [char; 3] = ['a''b''a'];
    const_generics2(m);
}

fn const_generics1<const A: bool, T: Display>(i: T) {
    if A {
        println!("This is True");
    } else {
        println!("This is false");
    }
    println!("{}", i);
}

fn const_generics2<T, const N: usize>(i: [T; N])
where
    T: Debug,
{
    println!("{:?}", i);
}

Const 泛型是值而不是類型,這意味着我們不能在需要類型的地方使用它們。在數組類型 [T;N] 中,T 爲類型,N 爲值。

// 使用const值作爲類型會導致編譯錯誤。
fn const_generics3<const T:char,const N:usize>(i:[T;N]){}

這裏不能在 T 位置使用 const 泛型,但可以在 N 的位置使用 const 泛型,我們可以將不變量編碼爲我們的類型,而不必在運行時捕獲它們,即在編譯時捕獲錯誤。

泛型:動態分發

對於某些類型,我們無法在編譯時知道它們的大小。因此,它們被用在指針後面進行動態分發。

Trait Object 是沒有大小的,因此它們必須在指針後面使用,以便在編譯時具有大小,因爲 Rust 需要在編譯時知道所有類型的大小。

use std::mem::size_of;

fn main() {
    // let unsize1: ToString;
    // let unsize2: Fn(&str) -> bool;

    let sized1: &dyn ToString;
    sized1 = &"45";

    let sized2: Box<String>;
    sized2 = Box::new(String::from(
        "Bytes are counted for memory-constrained devices",
    ));

    println!("{:?}", size_of::<&dyn ToString>());
    println!("{:?}", size_of::<Box<String>>());
    println!("{:?}", size_of::<Box<dyn Fn(&str) -> bool>>());
}

如果我們去掉前兩行的註釋,我們會得到一個編譯錯誤,說 “在編譯時沒有已知的大小”。

size1 的大小爲 16 字節,size2 的大小爲 8 字節。對於 size2 來說,這是一個優勢,因爲普通 String 類型佔用 24 字節,而 Box 佔用 8 字節。

注意 trait object 的 Box 佔用 16 個字節,而普通類型的 Box 佔用 8 個字節。這是一種權衡。如果不需要運行時開銷,那麼選擇靜態分發。如果更關心二進制大小而不是效率,請選擇動態調度。這些都是可選擇的,而不是默認選項。

雖然類型在運行時纔是已知的,但是並不意味着缺乏類型安全性。我們不能在編譯時調用與特徵無關的方法。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/bVwV2a6ibef_aCJOEpkZUQ