經(jīng)過(guò)上一章[1]的學(xué)習(xí),我想現(xiàn)在你已經(jīng)成功安裝好一個(gè)Rust開(kāi)發(fā)環(huán)境了,是時(shí)候擼起袖子開(kāi)始寫(xiě)Rust代碼了!
程序員這個(gè)歷史并不算悠久的行當(dāng),卻有著一個(gè)歷史悠久的傳統(tǒng),那就是每種編程語(yǔ)言都將一個(gè)名為“hello, world”的示例作為這門(mén)語(yǔ)言學(xué)習(xí)的第一個(gè)例子,這個(gè)傳統(tǒng)始于20世紀(jì)70年代那本大名鼎鼎的由布萊恩·科尼根(Brian W. Kernighan)與C語(yǔ)言之父丹尼斯·里奇(Dennis M. Ritchie)合著的《C程序設(shè)計(jì)語(yǔ)言》。
圖片
在這一章中,我們也將遵從傳統(tǒng),從編寫(xiě)和運(yùn)行一個(gè)可以打印出“hello, world”的Rust示例程序開(kāi)始我們正式的Rust編碼之旅。我希望通過(guò)這個(gè)示例程序你能夠?qū)ust程序結(jié)構(gòu)有一個(gè)直觀且清晰的認(rèn)識(shí)。
“Hello, World”是一門(mén)編程語(yǔ)言的最簡(jiǎn)單示例的表達(dá)形式。在Go中,我們可以像下面這樣編寫(xiě)Go版本的Hello, World程序:
package mainfunc main() { println("Hello, World!")}
為了簡(jiǎn)單,我們甚至沒(méi)有使用fmt包的Printf系列函數(shù)(這樣就可以減少一行導(dǎo)入包的語(yǔ)句),而是用了內(nèi)置函數(shù)println來(lái)完成將“Hello, World”輸出到控制臺(tái)(更準(zhǔn)確的說(shuō)是標(biāo)準(zhǔn)錯(cuò)誤(stderr))的任務(wù)。
Rust版本的Hello, World可以比Go還要簡(jiǎn)潔,我們?cè)谝粋€(gè)目錄下(比如rust-guide-for-gopher/helloworld/rustc)創(chuàng)建一個(gè)hello_world.rs的文件。哦,沒(méi)錯(cuò)!rust的源碼文件都是以.rs作為源文件擴(kuò)展名的。并且對(duì)于多個(gè)單詞構(gòu)成的文件名,rust的慣例是采用全小寫(xiě)單詞+下劃線連接的方式命名。這個(gè)hello_world.rs文件的內(nèi)容如下:
fn main() { println!("Hello, World!");}
相比于Go在每個(gè)源文件中都要使用package指定該文件歸屬的包名,Rust無(wú)需這樣的一行。和Go一樣,這里的main是函數(shù),所有可執(zhí)行的Rust程序都必須有一個(gè)main函數(shù),它是Rust程序的入口函數(shù)。和Go使用func函數(shù)聲明函數(shù)不同,Rust聲明函數(shù)的關(guān)鍵字為fn。在這個(gè)main函數(shù)中,我們調(diào)用println!將“Hello, World!”輸出到控制臺(tái)上。
不過(guò),和Go內(nèi)置的println函數(shù)不同的是,這里的println!并非是一個(gè)函數(shù),而是一個(gè)**Rust宏(macro)**。
如果你只是學(xué)過(guò)Go,而沒(méi)有學(xué)過(guò)C/C++語(yǔ)言,你甚至都不會(huì)知道宏(macro)是什么。在Rust中,宏是一種用于代碼生成和轉(zhuǎn)換的元編程工具。宏允許你在編譯時(shí)根據(jù)一定的模式或規(guī)則來(lái)擴(kuò)展代碼。Rust宏分為聲明宏(Declarative Macros)和過(guò)程宏(Procedural Macros)。println!就屬于聲明宏,它由macro_rules! 宏定義,我們?cè)赗ust標(biāo)準(zhǔn)庫(kù)的源碼中可以看到其定義:
// $(rustc --print sysroot)/lib/rustlib/src/rust/library/std/src/macros.rs#[macro_export]#[stable(feature = "rust1", since = "1.0.0")]#[cfg_attr(not(test), rustc_diagnostic_item = "println_macro")]#[allow_internal_unstable(print_internals, format_args_nl)]macro_rules! println { () => { $crate::print!("/n") }; ($($arg:tt)*) => {{ $crate::io::_print($crate::format_args_nl!($($arg)*)); }};}
在Rust源碼編譯過(guò)程中,聲明宏是在最開(kāi)始的預(yù)處理階段進(jìn)行擴(kuò)展的,我們也可以通過(guò)nightly版的rustc命令來(lái)查看println!宏展開(kāi)后的結(jié)果(-Z選項(xiàng)只能在nightly版本中使用):
$rustc +nightly-2022-07-14-x86_64-apple-darwin -Zunpretty=expanded hello_world.rs#![feature(prelude_import)]#![no_std]#[prelude_import]use ::std::prelude::rust_2015::*;#[macro_use]extern crate std;fn main() { { ::std::io::_print(::core::fmt::Arguments::new_v1(&["Hello, World!/n"], &[])); };}
我們看到:println!宏被替換為一個(gè)標(biāo)準(zhǔn)庫(kù)下的函數(shù)(_print)的調(diào)用。btw,到這里,你可能和我一樣,看不懂println!展開(kāi)后的代碼,沒(méi)關(guān)系,我們后續(xù)會(huì)逐步學(xué)習(xí)并掌握這些語(yǔ)法的。此外,宏是Rust的高級(jí)特性,這里也不展開(kāi)說(shuō)了。
另外一個(gè)和Go在語(yǔ)法上有所不同的是,Rust在每行語(yǔ)句后面都要顯式使用分號(hào),對(duì)于Gopher而言,這個(gè)很容易遺忘。
接下來(lái),我們來(lái)編譯和運(yùn)行一下這個(gè)Rust版的Hello,World!,編譯運(yùn)行Rust代碼的最簡(jiǎn)單方法就是通過(guò)rustc編譯器將rust源碼文件編譯為可執(zhí)行程序:
$rustc hello_world.rs$lshello_world* hello_world.rs
我們看到,示例通過(guò)調(diào)用rustc將hello_world.rs編譯為了hello_world可執(zhí)行文件。
運(yùn)行rustc編譯后的可執(zhí)行文件將得到下面輸出結(jié)果:
$./hello_worldHello, World!
我們看到"Hello, World!"被打印到控制臺(tái)。
如果覺(jué)得默認(rèn)編譯出的hello_world文件名字較長(zhǎng),我們也可以像go build -o那樣指定rustc編譯后得到的目標(biāo)可執(zhí)行文件的名字,下面的命令通過(guò)-o選項(xiàng)將編譯后的程序命名為hello:
$rustc -o hello hello_world.rs
rustc編譯出來(lái)的二進(jìn)制文件size并不大,僅有400多KB(而Go默認(rèn)構(gòu)建的Hello, World!有1.3MB,在我的macOS上):
$ls -lhtotal 856-rwxr-xr-x 1 tonybai staff 423K 4 20 17:56 hello_world*
我們還可以通過(guò)去掉symbols的方式繼續(xù)讓其“瘦身”到不到300KB(通過(guò)go build -ldflags="-s -w" helloworld.go去除符號(hào)表和調(diào)試信息的Go二進(jìn)制程序還有近900K的大小):
$rustc -C strip=symbols hello_world.rs $ll -htotal 608-rwxr-xr-x 1 tonybai staff 297K 4 20 17:57 hello_world*
上面的"Hello, World"程序雖然足夠簡(jiǎn)單,也能夠運(yùn)行,但對(duì)于初學(xué)者而言,它有兩個(gè)“不足”:一來(lái)這個(gè)例子的確“太簡(jiǎn)單”,簡(jiǎn)單到無(wú)法充分展示單個(gè)Rust源碼文件的結(jié)構(gòu);二來(lái)這個(gè)示例只使用了一個(gè)單個(gè)源文件,與實(shí)際開(kāi)發(fā)中那種由多個(gè)文件組成的Rust實(shí)用工程有差別,同樣無(wú)法幫助我們理解實(shí)用性的Rust工程的結(jié)構(gòu)。
為了更好地理解Rust工程與單個(gè)源文件的構(gòu)成,我們將編寫(xiě)一個(gè)稍微復(fù)雜一點(diǎn)的版本,它將使用Rust的構(gòu)建管理工具cargo建立,并使用Rust標(biāo)準(zhǔn)庫(kù)中的std::io模塊進(jìn)行輸入/輸出操作。
在實(shí)際開(kāi)發(fā)中,Rust程序通常由多個(gè)源文件組成,并使用Cargo作為構(gòu)建系統(tǒng)和包管理器。Cargo可以幫助我們管理項(xiàng)目的源代碼、依賴庫(kù)、構(gòu)建任務(wù)等。下面我們就來(lái)創(chuàng)建一個(gè)使用Cargo的"Hello, World"。
我們?cè)谝粋€(gè)目錄下(比如:rust-guide-for-gopher/helloworld/cargo)執(zhí)行下面命令來(lái)創(chuàng)建hello_world:
$cargo new hello_world Created binary (application) `hello_world` package
cargo默認(rèn)創(chuàng)建了一個(gè)binary(application)類型的rust package,我們來(lái)看看初始情況下這個(gè)rust package下都有哪些內(nèi)容:
$tree hello_world hello_world├── Cargo.toml└── src └── main.rs1 directory, 2 files
其中,Cargo.toml是Rust包的清單(manifest)文件。它包含有關(guān)包及其依賴項(xiàng)的元數(shù)據(jù)。以下是上面Cargo.toml文件的全部?jī)?nèi)容:
// Cargo.toml[package]name = "hello_world"version = "0.1.0"edition = "2021"# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html[dependencies]
其中package下面的字段含義如下:
dependencies下面則是會(huì)記錄該package對(duì)第三方依賴的情況,這個(gè)示例中并無(wú)三方依賴,因此這里為空。
我們的代碼放在了src目錄下,這也是rust包的標(biāo)準(zhǔn)布局。為了更好地理解Rust程序的構(gòu)成,我們將編寫(xiě)一個(gè)稍微復(fù)雜一點(diǎn)的Hello, World!版本,它使用Rust標(biāo)準(zhǔn)庫(kù)中的std::io模塊進(jìn)行輸入/輸出操作:
// rust-guide-for-gopher/helloworld/cargo/hello_world/src/main.rsuse std::io;use std::io::Write;fn main() { let mut output = io::stdout(); output.write(b"Hello, World!").unwrap(); output.flush().unwrap();}
這個(gè)Rust的"Hello, World"程序展示了一個(gè)典型的Rust源文件結(jié)構(gòu),包括導(dǎo)入語(yǔ)句、主函數(shù)定義以及一系列的方法調(diào)用。它演示了如何使用標(biāo)準(zhǔn)庫(kù)的io模塊來(lái)向標(biāo)準(zhǔn)輸出流打印"Hello, World!"。下面是對(duì)其程序結(jié)構(gòu)的簡(jiǎn)單總結(jié):
源文件在最開(kāi)始處使用use std::io; 和use std::io::Write;這兩行導(dǎo)入了標(biāo)準(zhǔn)庫(kù)中的io模塊及其Write trait。這樣程序就可以在后面的代碼中直接使用io和Write,而無(wú)需完整地寫(xiě)出它們的命名空間。這里我們先不用關(guān)心trait是什么,你大可將其理解為和Go interface差不多的語(yǔ)法元素就行了。
main定義了程序的入口點(diǎn)。Rust 程序從main函數(shù)開(kāi)始執(zhí)行。
let mut output = io::stdout(); 這行代碼創(chuàng)建了一個(gè)可變變量output,它綁定到了一個(gè)標(biāo)準(zhǔn)輸出流(stdout)。mut關(guān)鍵字表示該變量是可變的,可以在后續(xù)代碼中修改它的值。關(guān)于變量以及綁定,我們?cè)诤竺嬗袑iT(mén)的章節(jié)說(shuō)明。這里要注意的是,和Go變量不同的是,Rust中的變量默認(rèn)是不可變的,只有顯式用mut聲明的變量才是可變的。
output.write(b"Hello, World!").unwrap(); 調(diào)用了output的write方法,傳遞了一個(gè)字節(jié)串作為參數(shù)。該方法用于將字節(jié)寫(xiě)入輸出流。unwrap方法用于處理方法調(diào)用可能產(chǎn)生的錯(cuò)誤,它在這里表示“我相信這個(gè)方法調(diào)用會(huì)成功,如果不成功,就讓程序 panic”。同理,output.flush().unwrap()也是這樣的。關(guān)于錯(cuò)誤以及異常處理的話題,我們會(huì)在后面進(jìn)行專題性學(xué)習(xí)。
理解了源碼后,我們來(lái)編譯和運(yùn)行一下這個(gè)程序,這次我們不再使用rustc,而是用cargo來(lái)實(shí)現(xiàn)。
要構(gòu)建上面的示例程序,我們只需在項(xiàng)目根目錄下運(yùn)行下面命令:
$cargo build Compiling hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/helloworld/cargo/hello_world) Finished dev [unoptimized + debuginfo] target(s) in 1.23s
構(gòu)建成功后,我們?cè)賮?lái)查看一下當(dāng)前項(xiàng)目下的結(jié)構(gòu)變化:
$tree -F.├── Cargo.lock├── Cargo.toml├── src/│ └── main.rs└── target/ ├── CACHEDIR.TAG └── debug/ ├── build/ ├── deps/ │ ├── hello_world-07284f5d84374479* │ ├── hello_world-07284f5d84374479.1atc14vk0u28taij.rcgu.o │ ├── hello_world-07284f5d84374479.1bu89c2i9mazzqif.rcgu.o │ ├── hello_world-07284f5d84374479.26e3nxhmk9lhy9zy.rcgu.o │ ├── hello_world-07284f5d84374479.29l81xyv0i4g8s88.rcgu.o │ ├── hello_world-07284f5d84374479.41i7ln85cwseljfw.rcgu.o │ ├── hello_world-07284f5d84374479.4iz3ubiqrvegnjdp.rcgu.o │ ├── hello_world-07284f5d84374479.53vu8cjirf8g6rnw.rcgu.o │ ├── hello_world-07284f5d84374479.5f6ye0ayl23rccqv.rcgu.o │ └── hello_world-07284f5d84374479.d ├── examples/ ├── hello_world* ├── hello_world.d └── incremental/ └── hello_world-16yuztatbr0vh/ ├── s-gvfwmugno5-1gy801r-1i2g78r4nmg489ix0nuktmqgb/ │ ├── 1atc14vk0u28taij.o │ ├── 1bu89c2i9mazzqif.o │ ├── 26e3nxhmk9lhy9zy.o │ ├── 29l81xyv0i4g8s88.o │ ├── 41i7ln85cwseljfw.o │ ├── 4iz3ubiqrvegnjdp.o │ ├── 53vu8cjirf8g6rnw.o │ ├── 5f6ye0ayl23rccqv.o │ ├── dep-graph.bin │ ├── query-cache.bin │ └── work-products.bin └── s-gvfwmugno5-1gy801r.lock*9 directories, 28 files
我們看到cargo build執(zhí)行后,項(xiàng)目下多出了好多目錄和文件。這些目錄和文件都是做什么的呢?我們挑選主要的來(lái)看一下。
Cargo的鎖定文件,用于記錄每個(gè)依賴項(xiàng)的確切版本號(hào),以保證構(gòu)建的可重復(fù)性。
這個(gè)示例中由于沒(méi)有使用第三方依賴,這個(gè)Cargo.lock文件中的內(nèi)容不具典型性:
# This file is automatically @generated by Cargo.# It is not intended for manual editing.version = 3[[package]]name = "hello_world"version = "0.1.0"
另外Cargo.lock文件完全由cargo自動(dòng)管理,開(kāi)發(fā)人員不需要也不應(yīng)該對(duì)其進(jìn)行手動(dòng)修改。
存放構(gòu)建輸出的目錄,用于存儲(chǔ)編譯后的目標(biāo)文件和可執(zhí)行文件。
用于標(biāo)記target目錄為一個(gè)緩存目錄的文件。它的內(nèi)容如下:
$cat CACHEDIR.TAG Signature: 8a477f597d28d172789f06886806bc55# This file is a cache directory tag created by cargo.# For information about cache directory tags see https://bford.info/cachedir/
這是一個(gè)符合Cache Directory Tagging Specification[2]的Tag文件。
調(diào)試模式下的構(gòu)建輸出目錄,存儲(chǔ)生成的可執(zhí)行文件和相關(guān)文件。
增量編譯的目錄,用于存儲(chǔ)增量編譯過(guò)程中的臨時(shí)文件和緩存。
Rust編譯過(guò)程緩慢,這個(gè)對(duì)比Go簡(jiǎn)直就是地下天上。在日常開(kāi)發(fā)中,基于增量編譯的文件進(jìn)行增量構(gòu)建可以大幅縮短編譯時(shí)間。
編譯過(guò)程中生成的臨時(shí)構(gòu)建文件的目錄。
存儲(chǔ)編譯生成的目標(biāo)文件(.o 文件)和相關(guān)的依賴項(xiàng)。
調(diào)試模式下生成的可執(zhí)行文件。
與hello_world相關(guān)的依賴關(guān)系信息的文件。
執(zhí)行debug目錄下的hello_world將得到如下輸出:
$./target/debug/hello_world Hello, World!
在Go中我們可以使用go run來(lái)直接編譯和運(yùn)行Go源碼文件,cargo也提供了該功能,我們?cè)陧?xiàng)目根目錄下運(yùn)行cargo run也可以編譯和執(zhí)行hello_world:
$cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.05s Running `target/debug/hello_world`Hello, World!
無(wú)論是cargo run還是cargo build,默認(rèn)構(gòu)建的都是debug版本的可執(zhí)行程序,程序中包含大量符號(hào)信息和調(diào)試信息,并且其優(yōu)化級(jí)別也不是很高。發(fā)布到生產(chǎn)環(huán)境的程序應(yīng)該是release模式下的,通過(guò)--release參數(shù),我們可以構(gòu)建release版本的可執(zhí)行程序:
$cargo build --release Compiling hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/helloworld/cargo/hello_world) Finished release [optimized] target(s) in 1.06s
構(gòu)建后,target目錄下會(huì)多出一個(gè)release目錄,其下面的內(nèi)容如下:
$tree -F target/release target/release├── build/├── deps/│ ├── hello_world-c41defdc625f9244*│ └── hello_world-c41defdc625f9244.d├── examples/├── hello_world*├── hello_world.d└── incremental/4 directories, 4 files
相對(duì)于debug版本,release版本由于實(shí)時(shí)了大量?jī)?yōu)化,通常其構(gòu)建時(shí)間會(huì)比debug版本要長(zhǎng)。但構(gòu)建出的release版本的size則要小很多。
無(wú)論是debug,還是release版,target下面都生成了許多中間文件,如果要清理文件并重頭構(gòu)建,我們可以使用cargo clean命令將target徹底清除:
$cargo clean Removed 40 files, 2.1MiB total
當(dāng)然cargo clean也支持一些命令行參數(shù),可以選擇清除哪些文件。
通過(guò)上面的例子,我們知道cargo new默認(rèn)創(chuàng)建的binary類型的rust package,如果我們要?jiǎng)?chuàng)建library類型的rust package,我們需要向cargo new傳遞--lib選項(xiàng)。下面的命令創(chuàng)建一個(gè)名為foo的library類型的rust package:
$cargo new --lib foo Created library `foo` package
我們看一下foo package下的目錄結(jié)構(gòu):
$tree -F foofoo├── Cargo.toml└── src/ └── lib.rs1 directory, 2 files
和binary類不同的是,src目錄下不再是main.rs,而是lib.rs,它是library類package的入口:
//rust-guide-for-gopher/helloworld/cargo/foo/lib.rspub fn add(left: usize, right: usize) -> usize { left + right}#[cfg(test)]mod tests { use super::*; #[test] fn it_works() { let result = add(2, 2); assert_eq!(result, 4); }}
lib.rs中只是一個(gè)library類package的入口模板,開(kāi)發(fā)人員需要根據(jù)自己的需要對(duì)其進(jìn)行調(diào)整。關(guān)于lib.rs中的內(nèi)容,我們將在下一章講解Rust代碼組織時(shí)做細(xì)致說(shuō)明,這里就不展開(kāi)說(shuō)了。
對(duì)于library類Rust package,我們同樣可以通過(guò)cargo build和cargo build --release構(gòu)建,下面是執(zhí)行構(gòu)建后目錄文件情況:
$tree.├── Cargo.lock├── Cargo.toml├── src│ └── lib.rs└── target ├── CACHEDIR.TAG ├── debug │ ├── build │ ├── deps │ │ ├── foo-24c6d6228c521501.2k5t0f94hnorqpgh.rcgu.o │ │ ├── foo-24c6d6228c521501.d │ │ ├── libfoo-24c6d6228c521501.rlib │ │ └── libfoo-24c6d6228c521501.rmeta │ ├── examples │ ├── incremental │ │ └── foo-m2biu8poxl6i │ │ ├── s-gvg68shtlp-1oqrf4n-irxhgoe7rhwmtvj6jwexcu0h │ │ │ ├── 2k5t0f94hnorqpgh.o │ │ │ ├── dep-graph.bin │ │ │ ├── query-cache.bin │ │ │ └── work-products.bin │ │ └── s-gvg68shtlp-1oqrf4n.lock │ ├── libfoo.d │ └── libfoo.rlib └── release ├── build ├── deps │ ├── foo-9f2dd76beda509bd.d │ ├── libfoo-9f2dd76beda509bd.rlib │ └── libfoo-9f2dd76beda509bd.rmeta ├── examples ├── incremental ├── libfoo.d └── libfoo.rlib14 directories, 20 files
我們看到,無(wú)論是debug還是release,cargo build構(gòu)建的結(jié)果都是libfoo.rlib。.rlib文件是Rust的靜態(tài)庫(kù)文件,通常用于代碼的模塊化和重用,我們?cè)诤罄m(xù)章節(jié)講解中,會(huì)詳細(xì)說(shuō)明如何使用這些構(gòu)建出來(lái)的靜態(tài)庫(kù)。
本文介紹了如何使用Rust編寫(xiě)"Hello, World"程序,并分別給出了rustc版和cargo版的hello, world程序版本。
在這個(gè)過(guò)程中,文章還介紹了Rust中的宏概念,并展示了如何使用println!宏來(lái)輸出文本。
之后,文章聚焦于使用Cargo構(gòu)建的hello,world程序版本,介紹了cargo的構(gòu)建、清理、debug和release版本的區(qū)別等,最后還提及了如何使用cargo創(chuàng)建library類的Rust package。
cargo貫穿Rust程序的整個(gè)生命周期,在后續(xù)的每一章中可能都會(huì)提及cargo。
本文鏈接:http://www.tebozhan.com/showinfo-26-92740-0.htmlGopher的Rust第一課:第一個(gè)Rust程序
聲明:本網(wǎng)頁(yè)內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問(wèn)題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com
上一篇: SpringBoot項(xiàng)目保證接口冪等的五種方法!
下一篇: 克服403錯(cuò)誤:Python爬蟲(chóng)的反爬蟲(chóng)機(jī)制應(yīng)對(duì)指南