答讀者問:函數重載

有讀者問問題:Rust有沒有函數重載?

回答:沒有。

本文結束。

-----------------

開個玩笑。好,下面我們詳細說一下重載(overloading)這個話題。

根據維基百科的定義,重載指的是:

function overloading or method overloading is the ability to create multiple methods of the same name with different implementations.

C 語言是沒有函數重載功能的,C++ 是有這個功能的。

在 C++ 中,一個非常常見的場景是,構造函數重載。比如說:

std::vector<int> first; // empty vector of intsnstd::vector<int> second (4,100); // four ints with value 100nstd::vector<int> third (second.begin(),second.end()); // iterating through secondnstd::vector<int> fourth (third); // a copy of thirdnn// the iterator constructor can also be used to construct from arrays:nint myints[] = {16,2,77,29};nstd::vector<int> fifth (myints, myints + sizeof(myints) / sizeof(int) );n

另外,參數類型的 const引用/非const引用,普通引用和右值引用等,都可以構成重載,來實現不同的語義。

所以C++中,每個函數調用,都不能簡單的通過函數名字來判斷,到底調用的是哪個函數,編譯器需要根據使用的實際參數,找到那個最合理的匹配,這個叫 name resolution。函數重載會增加 name resolution 的複雜性。C++裡面還有一些其它功能,把 name resolution 搞得更複雜。暫時能想到的相關的功能包括:名字遮蔽(name masking),函數參數默認值,模板函數,參數隱式類型轉換,不定長參數(variadic),等等。當這些功能混雜到一起的時候,情況變得非常複雜。

Rust沒有類似C++的這種函數重載功能,主要有幾個方面的考慮。

一方面是因為複雜性。Rust的變數類型自動推理功能比C++強大得多,也複雜得多,這讓函數重載更難實現。因為C++的邏輯是,通過函數的參數類型,來決定調用的函數。Rust中更常見的邏輯是,通過函數的類型,來推測參數的類型。如果在Rust中引入了函數重載功能,那麼類型自動推導的難度將會成幾何程度增加。

另一方面的原因是,函數重載這個功能並沒有很大的必要性,這也是最根本的原因。Rust team 認為找不出什麼場景,是其它功能完全無法做到的,非要函數重載不可的。

就拿上面這個C++構造函數重載的例子來說,Rust裡面根本就沒有構造函數這個概念,對象的構造就是通過各個成員直接賦值完成,如果要封裝成函數,那麼任何一個靜態函數都可以完成這個功能,Rust中對應的可以生成Vec類型的函數有:

fn new() -> Vec<T>;nfn with_capacity(capacity: usize) -> Vec<T>;nunsafe fn from_raw_parts(ptr: *mut T, length: usize, capacity: usize) -> Vec<T>;n

另外還有一堆的泛型 trait,比如 Default Into From Iterator::collect 等等,它們都可以生成新的 Vec 實例,在很多情況下都可以算作「構造器」。對於這種場景,強制要求所有函數名字都是一樣的,並沒有太大意義。不同的參數類型,執行的是不同的邏輯,取不同的名字,更具有可讀性。

還有一種場景,對於不同參數類型,它們內部的邏輯是高度一致的,這種情況下,用同一個函數名,接受不同的參數類型,不僅恰當,而且更簡潔。對於這種場景,Rust可以用泛型來實現。比如說,內置字元串類型,有一個 contains 方法,這個方法,就可以接受許多不同的參數類型:

fn main() {n let s = "hello";n s.contains("h");n s.contains(a);n s.contains(&[a, b, c] as &[char]);n s.contains(| c: char | c.is_lowercase());n}n

這是因為 contains 方法接收的參數類型是一個泛型參數,它只要滿足 Pattern 這個 trait 即可。

fn contains<a, P>(&a self, pat: P) -> bool n where P: Pattern<a>n

我們可以為希望支持的參數類型,實現這個 trait,那麼這個類型就可以被用到這個函數的參數中了。

從實現細節角度來說,泛型函數不等於重載。但從目的上來說,跟重載達到的目的一模一樣。而且,這個做法比 C++ 那種 ad hoc 式的重載更有利於提高代碼質量。C++ 對函數重載是沒有任何檢查規範的,用戶可以為完全不同的邏輯起相同的函數名字,這個功能很容易濫用,很可能造成可讀性的降低。Rust 的這種做法,實際上強制要求了不同參數類型,必須能夠用一個 trait 將它們統一抽象,而函數體內部無需關心不同參數類型的區別,而只需針對這個統一抽象來寫邏輯,邏輯一定是高度一致的。如果你做不到這一點,那麼就說明這兩組參數類型,對應的邏輯區別相當大,那麼它們理應對應不同的函數名字,它們有權利為自己取不同的名字。

除此之外,C++中還有一些使用重載的場景,在Rust中也是可以通過其它功能替代的。比如,使用 Builder 模式,取代非常複雜的構造函數重載,參見 std::thread::Builder。再比如,使用 Option 類型描述可選參數,使用 Option::unwrap_or 函數實現參數默認值,等等。

總之,函數重載是一個開發代價非常大,而收益幾乎等於0(甚至小於0)的一個功能。Rust現在不會,將來也不會加入ad hoc式的函數重載功能,這已經是整個社區的共識。


推薦閱讀:

Erlang入門教程 - 10. 模式匹配,Guard,變數作用域
C語言為何不改進數組?
Lisp 和 Haskell 各有什麼優缺點?學哪個好?
C# 兩種遍歷列表的方式,哪種更高效?
為什麼數據分析需要會編程語言?

TAG:Rust编程语言 | 编程学习 | 编程语言 |