iT邦幫忙

2025 iThome 鐵人賽

DAY 17
0
Rust

Rust 逼我成為更好的工程師:從 Borrow Checker 看軟體設計系列 第 17

(Day17) Rust 迭代器:所有權決定一切

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250917/20124462KA2M7PfuNm.png

Rust 逼我成為更好的工程師 迭代器:所有權決定一切

在 Rust,迭代器是關於資料流所有權的語法。
想對了資料,程式碼自然就對了。

三個入口,決定資料的命運

你對集合做的第一件事,就決定了整條鏈的效率。選錯了入口,後面就等著用 clone() 來彌補。

  • iter()借用元素 (&T)

    • 用途:讀取、計算、過濾資料。

    • 這是預設選項,90% 的情況下你都該用它。

  • iter_mut()可變借用元素 (&mut T)

    • 用途:原地修改集合中的資料。

    • 當你需要改變現有資料,而不是創造新資料時使用。

  • into_iter()移動元素 (T)

    • 用途:轉移所有權,消耗原集合,建立一個新集合。

    • 當資料的生命週期需要延續到集合之外時使用。

入口選對,整條鏈就不該有 .clone()

鏈式操作:零成本的配方

map(), filter(), take() 這樣的鏈式方法,它們本身幾乎不做任何事

它們只是在建立一個處理資料的配方(或者叫「視圖」),這個過程沒有記憶體分配,成本極低。

為什麼鏈式操作中間不能有 .clone()

  • 因為如果你一開始選了 1(我就看看),卻在半路想把東西拿走(clone),這就代表你從一開始就沒想清楚。你自相矛盾了。程式碼裡的 .clone() 就是你邏輯混亂的證明。正確的做法是回到起點,直接選 3(我要拿走)。

收斂點:在出口一次性付費

真正的計算和記憶體分配,發生在鏈的末端,被稱之為「收斂點」或「消費者」。

  • collect():根據你指定的型別,把配方計算出來,變成一個新的集合。這是付費的地方

  • sum(), count(), for_each():消耗迭代器,計算出最終結果。

心智模型:建立一個廉價的配方,在最後一刻才執行它並支付成本。

範例

// 1. iter(): 讀取資料來計算
let data = vec!["a".to_string(), "b".to_string()];
// data 在此之後依然可用
let lens: Vec<usize> = data.iter() // &String
    .map(|s| s.len())
    .collect();

// 2. iter_mut(): 原地修改
let mut nums = vec![1, 2, 3];
// nums 在此之後被修改了
nums.iter_mut() // &mut i32
    .for_each(|n| *n += 1);

// 3. into_iter(): 轉移所有權
// data 在此之後被消耗,無法再使用
let moved: Vec<String> = data.into_iter() // String
    .collect();

只有三個選擇:

  1. 我就看看,不動它。

    • 那你就用 iter()。它會給你一個指向資料的「唯讀通行證」(&T)。你只能看,不能改,更不能拿走。原來的東西還在。
  2. 我要修改它,讓它在原地變個樣子。

    • 那你就用 iter_mut()。它會給你一個「可修改通行證」(&mut T)。你可以直接在上面塗改。原來的東西被你改了。
  3. 這東西現在歸我了,我要拿走。

    • 那你就用 into_iter()。這是「所有權轉移證明」(T)。東西被你拿走了,原來的箱子空了,別人不能再用了。

為什麼鏈式操作中間不能有 .clone()

  • 因為如果你一開始選了 1(我就看看),卻在半路想把東西拿走(clone),這就代表你從一開始就沒想清楚。你自相矛盾了。程式碼裡的 .clone() 就是你邏輯混亂的證明。正確的做法是回到起點,直接選 3(我要拿走)。

什麼是「零成本的配方」?

  • map, filter 這些東西,只是你紙上寫下的「處理步驟」。電腦很懶,它看到你的計畫,但它不動手。直到最後說 collect()(交作業),它才一口氣把所有步驟做完。這樣最有效率

收斂時的型別推導與所有權

有時你需要一個擁有其資料的新集合,但你的入口是 iter(),它只提供參考。

let words = vec!["hello".to_string(), "world".to_string()];
// 我們想要一個 BTreeSet<String>,它需要擁有 String,而不是 &String
let set: std::collections::BTreeSet<String> = words.iter() // 產生 &String
    .cloned() // 把 &String 變成 String,這是必要的複製
    .collect();

這裡的 .cloned() 很重要。它不是一個錯誤,而是一個明確的所有權轉換。你告訴編譯器:「我知道 iter 只給了我參考,但我現在需要一份擁有權的拷貝來建立新集合。」這是唯一應該在鏈中出現 clone 的合理場景。

關鍵思考與反模式

  1. 永遠先用 iter()

  2. 需要原地修改?換成 iter_mut()

  3. 需要轉移所有權或消耗原集合?換成 into_iter()

  4. collect() 在最後處理所有分配。

反模式(如果你這麼做,就表示設計錯了):

  • map() 中途 .clone().to_owned():你的入口選錯了。你應該一開始就用 into_iter() 來取得所有權。

  • 用了 into_iter() 還想保留原集合:邏輯衝突。你不能同時吃掉蛋糕又擁有它。回去重新思考你的資料流。

結論重點

  • 先決定意圖:看、改,還是拿走?

  • 再選擇工具iter, iter_mut, into_iter

  • 然後寫下計畫.map().filter()...

  • 最後執行.collect()


上一篇
(Day16) Rust 內部可變性與封裝風險:Cell、RefCell
下一篇
(Day18) Rust 模式匹配:用模式匹配消除分支的雜訊
系列文
Rust 逼我成為更好的工程師:從 Borrow Checker 看軟體設計22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言