為什么要使用 Rust 語言、Rust 語言有什么優(yōu)勢
Rust 是一種采用過去的知識解決將來的問題的技術。” ——Graydon Hoare
Rust 是一種快速、高并發(fā)、安全且具有授權性的編程語言,最初由 Graydon Hoare 于2006 年創(chuàng)造和發(fā)布?,F(xiàn)在它是一種開源語言,主要由 Mozilla 團隊和許多開源社區(qū)成員共同維護和開發(fā)。

雖然 Rust 是一種通用的多范式語言,但它的目標是 C 和 C++占主導地位的系統(tǒng)編程領域。這意味著你可以使用 Rust 編寫操作系統(tǒng)、游戲引擎和許多性能關鍵型應用程序。同時,它還具有足夠的表現(xiàn)力,你可以使用它構建高性能的 Web 應用程序、網絡服務,類型安全的數據庫對象關系映射(Object Relational Mapping,ORM)庫,還可以將程序編譯成WebAssembly 在 Web 瀏覽器上運行。Rust 還在為嵌入式平臺構建安全性優(yōu)先的實時應用程序方面獲得了相當大的關注,例如 Arm 基于 Cortex-M 的微控制器,目前該領域主要由 C語言主導。Rust 因其廣泛的適用性在多個領域都表現(xiàn)良好,這在單一編程語言中是非常罕見的。
Rust 作為一門靜態(tài)和強類型語言而存在。靜態(tài)屬性意味著編譯器在編譯時具有所有相關變量和類型的信息,并且在編譯時會進行大量檢查,在運行時只保留少量的類型檢查。它的強類型屬性意味著不允許發(fā)生諸如類型之間自動轉換的事情,并且指向整數的變量不能在代碼中更改為指向字符串。例如在 JavaScript 等弱類型語言中,你可以輕松地執(zhí)行類似“two = "2"; two = 2 + two;”這樣的操作。JavaScript 在運行時將 2 的類型弱化為字符串,因此會將 22 作為字符串存儲到變量 two 中,這與你的意圖完全相反并且毫無意義。在 Rust 中,與上述代碼意義相同的代碼是“let mut two = "2"; two = 2 + two;”,該代碼將會在編譯時捕獲異常,并提示信息:“cannot add '&str' to '{integer}'”。
因此,強類型屬性使 Rust 可以安全地重構代碼,并在編譯時捕獲大多數錯誤,而不是在運行時出錯。用 Rust 編寫的程序表現(xiàn)力和性能都非常好,因為使用它你可以擁有高級函數式語言的大部分特性,例如高階函數和惰性迭代器,這些特性使你可以編譯像 C/C++程序這樣高效的程序。它的很多設計決策中強調的首要理念是編譯期內存安全、零成本抽象和支持高并發(fā)。讓我們來詳細說明這些理念。
編譯期內存安全:Rust 編譯期可以在編譯時跟蹤程序中資源的變量,并在沒有垃圾收集器(Garbage Collectors,GC)的情況下完成所有這些操作。
這意味你不會遇到在 free、double free 命令之后調用指針,或者運行時掛起指針等“臭名昭著”的問題。Rust 中的引用類型(類型名稱前面帶有&標記的類型)與生命周期標記隱式關聯(lián)('foo),有時由程序員顯式聲明。在生命周期中,編譯器可以跟蹤代碼中可以安全使用的位置,如果它是非法的,那么會在編譯期報告異常。為了實現(xiàn)這一點,Rust 通過這些引用上的生命周期標簽來運行借用/引用檢查算法,以確保你永遠不能訪問已釋放的內存地址。這樣做也可以防止你釋放被其他某些變量調用的任何指針。
零成本抽象:編程的目的就是管理復雜性,這是通過良好的抽象來實現(xiàn)的。接下來讓我們來看一個 Rust 和 Kotlin 的良好抽象示例。抽象讓我們能夠編寫高級并且易于閱讀和推斷的代碼。我們將比較 Kotlin 的流和 Rust 的迭代器在處理數字列表時的性能,并參照 Rust提供的零成本抽象原則。這里的抽象是指能夠使用以其他方法作為參數的方法,根據條件過濾數字而不使用手動循環(huán)。在這里引入 Kotlin 是因為它看上去和 Rust 存在相似性。代碼很容易理解,我們的目標是給出更高層面的解釋,并對代碼中的細節(jié)進行詳細闡述,因為這個示例的重點是理解零成本特性。
首先,我們來看 Kotlin 中的代碼:
import java.util.stream.Collectors
fun main(args: Array<String>)
{
//創(chuàng)建數字流
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).stream()
val evens = numbers.filter { it -> it % 2 == 0 }
val evenSquares = evens.map { it -> it * it }
val result = evenSquares.collect(Collectors.toList())
println(result) // prints [4,16,36,64,100]
println(evens)
println(evenSquares)
}我們創(chuàng)建了一個數字流(第 6 行)并調用了一系列方法(filter 和 map)來轉換元素, 以收集僅包含偶數的序列。這些方法可以采用閉包或函數(第 8 行中的“ it -> it * it”)來轉換集合中的元素。在函數式編程語言中,當我們在流/迭代器上調用這些方法時,對于每個這樣的調用,該語言會創(chuàng)建一個中間對象來保存與正在執(zhí)行的操作有關的任何狀態(tài)或元數據。因此,evens 和 evenSquares 將在 JVM 堆上分配兩個不同的中間對象。在堆上分配資源將會產生內存開銷,這是我們在 Kotlin 中為抽象必須額外付出的代價。
當我們輸出 evens 和 evenSquares 的值時,確實得到了兩個不同的對象,如下所示:
java.util.stream.ReferencePipeline$Head@51521cc1 java.util.stream.ReferencePipeline$3@1b4fb997
@之后的十六進制值是 JVM 對象的哈希值。由于哈希值不同,所以它們是不同的對象。在 Rust 中,我們會做相同的事情:
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10].into_iter();
let evens = numbers.filter(|x| *x % 2 == 0);
let even_squares = evens.clone().map(|x| x * x);
let result = even_squares.clone().collect::<Vec<_>>();
println!("{:?}", result); // 輸出 [4,16,36,64,100]
println!("{:?}\n{:?}", evens, even_squares);
} 接下來將解釋上述代碼的細節(jié)。在第 2 行中,我們調用 vec![]創(chuàng)建一個數字列表,然 后調用 into_iter()方法使其成為一個數字的迭代器/流。使用 into_iter()方法從集合中創(chuàng)建 了一個包裝器的迭代器類型(這里 Vec<i32>是一個有符號的 32 位整數列表),即 IntoIter([1,2,3,4,5,6, 7,8,9,10]),此迭代器類型引用原始的數字列表。然后我們執(zhí)行 filter 和 map 轉換(第 3 行和第 4 行),就像我們在 Kotlin 中所做的那樣。第 7 行輸出 evens 和 even_squares 的類型,如下所示(為了簡潔,省略了一些細節(jié)):
evens: Filter { iter: IntoIter( <numbers> ) }
even_squares: Map { iter: Filter { iter: IntoIter( <numbers> ) }} 中間對象 Filter 和 Map 是基礎迭代器結構上的包裝器類型(未在堆上分配),它本身是一個包裝器,包含對第 2 行的原始數字列表的引用。第 4 行和第 5 行的包裝器結構在分別調用 filter 和 map 時創(chuàng)建,它們之間沒有任何指針解引用,并且不會像 Kotlin 那樣產生堆分配的開銷。所有這些可歸結為高效的匯編代碼,這相當于使用循環(huán)(語句)的手動編寫版本。
支持高并發(fā):當我們說 Rust 是并發(fā)安全的時,其含義是該語言具有應用程序接口(Application Programming Interface,API)和抽象能力,使得編寫正確和安全的并發(fā)代碼變得非常容易。而在 C++中,并發(fā)代碼出錯的可能性非常大。在 C++中同步訪問多個線程的數據時,需要在每次進入臨界區(qū)時調用 mutex.lock(),并在退出它時調用 mutex.unlock():
// C++ mutex.lock(); // 互斥鎖鎖定 // 執(zhí)行某些關鍵操作 mutex.unlock(); // 執(zhí)行完畢
在大量開發(fā)人員共同協(xié)作的大型代碼庫中,你可能會忘記在多線程訪問共享對象之前調用 mutex.lock(),這可能導致數據訪問沖突。在其他情況下,你可能忘記解開互斥鎖(Mutex),并使其他想要訪問數據的線程一直處于等待狀態(tài)。
Rust 對此有不同的處理方式。在這里,你將數據包裝成 Mutex 類型,以確保來自多個線程的數據進行同步可變訪問:
// Rust
use std::sync::Mutex;
fn main() {
let value = Mutex::new(23);
*value.lock().unwrap() += 1; // 執(zhí)行一些修改
} // 這里自動解鎖在上述代碼中,我們能夠在變量 value 調用 lock()方法之后修改數據。Rust 采用了保護共享數據自身,而不是代碼的概念。Rust 與 Mutex 和受保護的數據的交互并不是獨立的,這和 C++中的情況一樣。你無法在 Mutex 類型不調用 lock()方法的情況下訪問內部數據。
那么 lock()方法的作用是什么?調用 lock()方法之后會返回一個名為 MutexGuard 的東西,它會在變量超出作用域范圍之后自動解除鎖定,它是 Rust 提供的眾多安全并發(fā)抽象之一。
另一個新穎的想法是標記特征的概念,它在編譯期驗證,并確保在并發(fā)代碼中同步和安全地訪問數據,第 4 章詳細介紹了該特征。類型會被稱為 Send 和 Sync 的標記特征進行注釋標記,以指示它們是否可以安全地發(fā)送到線程或者在線程之間共享。當程序向線程發(fā)送值時,編譯器會檢查該值是否實現(xiàn)了所需的標記特征,如果沒有,則禁止使用該值。通過這種方式,Rust 允許你毫無顧慮地編寫并發(fā)代碼,編譯器在編譯時會捕獲多線程代碼中的異常。
編寫并發(fā)代碼已經很難了,使用 C/C++會讓它變得更加困難和神秘。當前 CPU 沒有獲得更多的時鐘頻率;相反,我們添加了更多內核。因此,并發(fā)編程是正確的發(fā)展方向。Rust 使得編寫并發(fā)代碼變得輕而易舉,并且降低了編寫安全的并發(fā)代碼的門檻。
Rust 還借鑒了 C++的 RAII 原則用于資源初始化,這種技術的本質是將資源的生命周期和對象的生命周期綁定,而堆分配類型的解除分配是通過執(zhí)行 drop 特征上的 drop()方法實現(xiàn)的。當變量超出作用域時,程序會自動調用此方法。它還用 Result 和 Option 類型替代了空指針的概念,我們將在第 6 章對此進行詳細介紹。這意味著 Rust 不允許代碼中出現(xiàn)null/undefined 的值,除非通過外部函數接口與其他語言交互,以及使用不安全代碼時。該語言還強調組合,而不是繼承,并且有一套特征系統(tǒng),它由數據類型實現(xiàn),類似于 Haskell的類型類,也被稱為加強型的 Java 接口。
但同樣重要的是,Rust 社區(qū)非?;钴S和友好。該語言包含非常全面的文檔,可以在Rust 官網中找到。Rust 在 Stack Overflow 的開發(fā)者調查上連續(xù) 3 年(2016 年、2017 年和2018 年)被評為最受歡迎的編程語言,因此編程社區(qū)對它非常青睞??偠灾?,如果你希望編寫具有較少錯誤的高性能軟件,又希望感受當前流行語言的特性和極佳的社區(qū)文化, 那么 Rust 應該是一個不錯的選擇。
到此這篇關于為什么要使用 Rust 語言、Rust 語言有什么優(yōu)勢的文章就介紹到這了,更多相關Rust 語言優(yōu)勢內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Rust語言從入門到精通之Tokio的Channel深入理解
這篇文章主要為大家介紹了Rust語言從入門到精通之Tokio的Channel深入理解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-05-05
Rust語言從入門到精通系列之Iterator迭代器深入詳解
這篇文章主要為大家介紹了Rust語言從入門到精通系列之Iterator迭代器深入詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-04-04

