Any 與 'static

在 Rust 中,如果想轉換成動態型別,會用到 std::any::Any,而 Any 的定義是:

pub trait Any: 'static { ... }

為什麼是 'static 的一個主因,就是 型別標示 (TypeId) 的唯一性與生命週期無關

Rust 的編譯器為每個類型生成一個唯一的 TypeId,然而 TypeId 不包含生命周期資訊

如果 Rust 允許 Any 處理帶有 非 static 生命周期的類型

你可能會把一個生命周期較短的引用偽裝 成一個生命周期較長的類型

因此,Any 限制 T 必須滿足 'static(意即該類型不包含任何非永久的引用),以確保運行時的類型轉換是絕對安全的

偽裝成「長生命週期」

首先先建立一個容器,準備存放「任何類型」的資料,這個變數活得比較久

use std::any::Any;

fn main() {
    let mut storage: Option<Box<dyn Any>> = None;
    {
        // ...
    }
}

接著建立一個短命的局部變數,並取得它的引用 &str

這個引用的生命週期 'a 只在大括號內有效

// ...

{
    let short_lived = String::from("我是短命的字串");
    let reference = &short_lived;

    // ...
}

最關鍵的操作如下,我們把這個短命的引用轉成 Any,也就是假設 Any 允許非 'static,則這行會成功

並將這個動態物件存入外部的容器 storage

// ...
{
    // ...

    let dynamic_val: Box<dyn Any> = Box::new(reference);
    storage = Some(dynamic_val);

    // ...
}

接著大括號結束,short_lived 在這裡被 Drop,記憶體被回收

此時 storage 裡面還存著 dynamic_val,而裡面還存著指向 short_lived 的指標

接著我們嘗試從外部容器拿回資料

// ...
{
    // ...
}

if let Some(any_val) = storage {
        
    // ...
}

透過 downcast_refAny 轉回原本的 &str,因為 TypeId 只紀錄型別是 &str,不知道它的生命週期已經結束了

// ...
{
    // ...
}

if let Some(any_val) = storage {
        
    // ...
    if let Some(recovered_str) = any_val.downcast_ref::<&str>() {

        //...
    }
}

最後錯誤發生的位置就會在裡面使用 recovered_str

// ...
if let Some(recovered_str) = any_val.downcast_ref::<&str>() {
        
    // 這裡的 
    println!("讀取到的內容:{}", recovered_str); 
}

因為 recovered_str 指向的記憶體已經被釋放或是被別的資料蓋掉了

當然,實際上這不會發生,編譯時就會失敗了:

cd examples/lifetime/any_static
cargo run --bin bad_any --release
error[E0597]: `short_lived` does not live long enough
  --> bad_any.rs:14:25
   |
10 |         let short_lived = String::from("我是短命的字串");
   |             ----------- binding `short_lived` declared here
...
14 |         let reference = &short_lived;
   |                         ^^^^^^^^^^^^ borrowed value does not live long enough
...
18 |         let dynamic_val: Box<dyn Any> = Box::new(reference);
   |                                         ------------------- coercion requires that `short_lived` is borrowed for `'static`
...
22 |     } // 6. <--- 這裡是大括號結束點!
   |     - `short_lived` dropped here while still borrowed

效能測試

將 1,000,000 個整數放入「動態類型」容器中,然後遍歷並進行 型別檢查(Downcast) 後累加

use std::any::Any;

fn main() {
    let mut vec: Vec<Box<dyn Any>> = Vec::new();
    for i in 0..1_000_000 {
        vec.push(Box::new(i as i32));
    }

    let start = std::time::Instant::now();
    let mut sum = 0i64;
    for item in vec {
        if let Some(val) = item.downcast_ref::<i32>() {
            sum += *val as i64;
        }
    }
    println!("Rust Sum: {}, Time: {:?}", sum, start.elapsed());
}
/*
cd examples\lifetime\any_static
cargo run --bin test --release

Rust Sum: 499999500000, Time: 15.3325ms
*/

其中 Rust Box<dyn Any> 涉及到了兩次間接定址

第一次是尋找 Box 指向的記憶體,第二次是透過虛擬表 (vtable) 來處理 dyn 相關的操作

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

typedef struct
{
    void *data;
    int tag; // 0 for int
} Any;

int main()
{
    int count = 1000000;
    Any *list = malloc(sizeof(Any) * count);
    for (int i = 0; i < count; i++)
    {
        int *val = malloc(sizeof(int));
        *val = i;
        list[i].data = val;
        list[i].tag = 0;
    }

    clock_t start = clock();
    long long sum = 0;
    for (int i = 0; i < count; i++)
    {
        if (list[i].tag == 0)
        { // 模擬 downcast 檢查
            sum += *(int *)list[i].data;
        }
    }
    double time_taken = (double)(clock() - start) / CLOCKS_PER_SEC;
    printf("C Sum: %lld, Time: %f s\n", sum, time_taken);

    // 清理記憶體...
    return 0;
}
/*
cd examples\lifetime\any_static
gcc test.c -O3 -march=native -o test
./test

C Sum: 499999500000, Time: 0.001000 s
*/

C 只是做一個簡單的整數比對 if (list[i].tag == 0)

這在 CPU 層面非常快,甚至可以被分支預測器(Branch Predictor)優化得幾乎沒有成本

而 Rust 呼叫 downcast_ref::<i32>() 內部會進行 TypeId 的比對

TypeId 實際上是一個 64 位元或 128 位元的 Hash,比對雜湊值的開銷比比對一個簡單的 int 要高

另外 C 的指標轉換 (int *)list[i].data 在編譯後幾乎不產生額外指令

而 Rust 的 downcast 是一個函式呼叫,雖然會被 inline,但內部的邏輯判斷仍比 C 複雜

然而如果在 C 中不小心把 tag 設為 0 int,但其實裡面存的是 float

C 會毫無怨言地執行,然後給你一個無意義的亂數結果,甚至導致崩潰

如果在 Rust 中想追求類似 C 的效能,但又想要安全性,通常得改用 Enum 而不是 Box<dyn Any>

// 底層就是 Tagged Union
enum MyAny {
    Int(i32),
    Float(f32),
}