-
在Rust中,string.as_str()和&string都用于获取字符串的引用,但它们在语义和使用场景上有一些区别。&string含义:&string是直接对String类型的变量取引用,得到一个&String类型的引用。使用场景:当你需要一个&String类型的引用时,可以直接使用&string。自动转换:在大多数需要&str的地方,Rust会自动将&String转换为&str,因为String实现了Deref<Target=str> trait。let s = String::from("hello"); let string_ref: &String = &s; // 直接获取String的引用 string.as_str()含义:string.as_str()是String类型的一个方法,它返回一个&str类型的引用。使用场景:当你明确需要一个&str类型的引用时,可以使用as_str()方法。显式调用:需要显式调用该方法来获取&str。let s = String::from("hello"); let str_ref: &str = s.as_str(); // 获取str的引用 关键区别类型:&string的类型是&String。string.as_str()的类型是&str。自动转换:&String可以自动转换为&str,因为String实现了Deref trait。as_str()是显式获取&str的方法。使用意图:使用&string通常表示你需要一个String的引用,或者让Rust自动处理类型转换。使用as_str()通常表示你明确需要一个str的引用,比如在函数参数明确要求&str时。示例fn process_str(s: &str) { println!("Processing str: {}", s); } fn process_string(s: &String) { println!("Processing string: {}", s); } fn main() { let s = String::from("hello"); // 自动转换 process_str(&s); // 自动将&String转换为&str // 显式获取str引用 process_str(s.as_str()); // 直接使用String引用 process_string(&s); } 在这个例子中,process_str可以接受&String和&str,因为Rust会自动进行类型转换。而process_string则需要一个&String类型的参数。
-
Rust 相比 C/C++ 在内存安全方面的优势,以及大厂选择 Rust 替代 C/C++ 的原因,可以从技术特性、安全模型、生态发展和业务需求等角度深入分析:一、Rust 如何实现内存安全?Rust 通过以下核心机制从语言层面杜绝内存安全问题:1. 所有权(Ownership)系统核心规则:每个值有唯一所有者:所有权转移时,旧所有者失效(类似“移动语义”)。离开作用域时自动释放内存:无需手动调用 free 或 delete。示例:let s1 = String::from("hello"); let s2 = s1; // 所有权从 s1 转移到 s2,s1 失效 // println!("{}", s1); // 编译错误:s1 已失效 优势:避免悬垂指针(Dangling Pointer)和重复释放(Double Free)。2. 借用(Borrowing)与生命周期(Lifetime)借用规则:可变借用与不可变借用互斥:同一时间只能有一个可变借用或多个不可变借用。生命周期标注:编译器强制检查引用的有效性,确保引用不会超出数据的作用域。示例:fn print_str(s: &str) { println!("{}", s); } let s = String::from("world"); print_str(&s); // 不可变借用,s 仍可用 优势:避免空指针解引用(Null Pointer Dereference)和数据竞争(Data Race)。3. 类型系统与模式匹配枚举(Enum)与模式匹配:强制处理所有可能状态,避免未定义行为。enum Option<T> { Some(T), None, } let x: Option<i32> = Some(5); match x { Some(v) => println!("Value: {}", v), None => println!("No value"), // 必须处理 None 情况 } 优势:杜绝空指针异常(类似 C++ 的 std::optional,但更严格)。4. 无垃圾回收(GC)的内存安全Rust 通过所有权和借用规则在编译时管理内存,无需运行时 GC,兼顾性能和安全性。二、大厂为何选择 Rust 替代 C/C++?1. 降低安全漏洞风险C/C++ 的痛点:内存错误(如缓冲区溢出、空指针解引用)是安全漏洞的主要来源(如 Heartbleed、Log4j 等)。Rust 的优势:从语言层面消除内存安全问题,减少安全审计成本。案例:Mozilla Firefox:用 Rust 重写部分关键组件(如 CSS 引擎),减少崩溃和漏洞。Microsoft:在 Windows 内核和安全组件中引入 Rust,降低攻击面。2. 提升开发效率与可维护性C/C++ 的痛点:手动内存管理、复杂指针操作和头文件依赖导致开发效率低。Rust 的优势:现代工具链:Cargo 包管理器、内置测试框架、文档生成工具。清晰的错误处理:通过 Result 和 Option 强制处理错误,避免未捕获异常。案例:Dropbox:用 Rust 重写文件同步核心模块,减少 30% 的代码量。Discord:用 Rust 重写后端服务,提升性能并降低资源占用。3. 性能与安全兼顾C/C++ 的优势:直接内存操作和低级控制,适合高性能场景。Rust 的优势:零成本抽象:高级特性(如迭代器、闭包)编译后与 C 性能相当。无 GC 停顿:适合实时系统(如游戏引擎、嵌入式设备)。案例:Figma:用 Rust 重写渲染引擎,性能提升 10 倍。AWS Lambda:用 Rust 实现高性能运行时,冷启动时间缩短。4. 生态与社区支持Rust 的生态:WebAssembly:Rust 是 WebAssembly 的首选语言之一,用于高性能前端开发。区块链:Solana、Polkadot 等区块链项目用 Rust 实现。操作系统:Redox OS、Tock OS 等系统级项目。大厂投入:Google:探索 Rust 替代 Android 系统中的 C/C++。Amazon:在 AWS 中广泛使用 Rust,并开源工具链。三、Rust 与 C/C++ 的对比总结维度RustC/C++内存安全编译时强制检查,杜绝内存错误依赖开发者手动管理,易出错性能零成本抽象,与 C 相当直接内存操作,高性能开发效率现代工具链、清晰错误处理手动内存管理、复杂构建系统生态WebAssembly、区块链、系统级开发游戏引擎、操作系统、嵌入式学习曲线较陡峭(所有权、生命周期)较陡峭(指针、内存管理)四、结论Rust 通过所有权系统、借用规则和生命周期检查,从语言层面解决了 C/C++ 的内存安全问题,同时保持高性能和低级控制能力。大厂选择 Rust 的核心原因包括:降低安全风险:减少内存错误导致的漏洞。提升开发效率:现代工具链和清晰的错误处理。性能与安全兼顾:适合高性能场景(如系统级开发、区块链)。生态与社区支持:广泛用于新兴领域(如 WebAssembly、区块链)。尽管 Rust 的学习曲线较陡峭,但其长期收益(如安全性、可维护性)使其成为 C/C++ 的有力替代者,尤其在安全敏感和高性能要求的场景中。
-
在 Rust 编程语言中,Cow(全称 Copy-on-Write,即“写时复制”)是一种智能指针,用于优化数据操作,避免不必要的内存复制。它通过延迟复制(仅在需要修改时复制数据)来提升性能,尤其适用于需要灵活处理借用(&T)和拥有(T)数据的场景。Rust Cow 的核心特性类型定义:pub enum Cow<'a, B> where B: 'a + ToOwned + ?Sized, { Borrowed(&'a B), // 借用状态(不拥有数据) Owned(<B as ToOwned>::Owned), // 拥有状态(已复制数据) } Borrowed:直接引用外部数据(不复制)。Owned:当需要修改时,自动复制数据到新内存中。适用场景:避免不必要的复制:例如解析字符串时,若输入数据已满足要求,则无需复制。延迟复制:直到必须修改时才分配内存(如 get_or_insert 操作)。统一接口:处理可能为借用或拥有的数据(如 &str 和 String)。示例代码use std::borrow::Cow; // 处理数据:若输入是 &str,则不复制;若是 String,则直接使用。 fn process_data(data: Cow<str>) { let processed = data.to_uppercase(); // 可能触发复制(如果 data 是 Borrowed) println!("{}", processed); } fn main() { let borrowed = "hello"; let owned = String::from("world"); process_data(Cow::Borrowed(borrowed)); // 无复制 process_data(Cow::Owned(owned)); // 无需复制(但所有权已转移) } Cow 的常见用法解析数据:fn parse_input(input: &str) -> Cow<str> { if input.contains(' ') { Cow::Owned(input.to_uppercase()) // 需要修改,触发复制 } else { Cow::Borrowed(input) // 直接引用输入 } } 避免 clone() 调用:fn get_name(data: &Cow<str>) -> String { data.to_string() // 自动处理 Borrowed 或 Owned 状态 } 与 Deref 结合:Cow 实现了 Deref trait,可直接像引用一样使用:let cow = Cow::Borrowed("hello"); assert_eq!(cow.len(), 5); // 无需显式解引用 优势与注意事项优势:性能优化:减少不必要的内存分配和复制。灵活性:统一处理借用和拥有的数据。注意事项:生命周期管理:Borrowed 状态需确保原始数据的生命周期有效。修改触发复制:对 Cow::Borrowed 调用修改方法(如 to_mut())会强制复制数据。总结Rust 的 Cow 是一种高效的智能指针,通过写时复制机制优化内存使用。它在需要灵活处理数据所有权或避免复制的场景中非常有用,例如解析输入、处理不可变数据或实现零拷贝接口。
-
在 Rust 中,打印一个 struct 的内容通常需要为该结构体实现 std::fmt::Display 或 std::fmt::Debug trait。以下是具体方法:1. 使用 Debug trait(默认实现)Rust 为所有结构体自动实现了 Debug trait(如果结构体的所有字段也实现了 Debug)。你可以直接使用 println! 或 dbg! 宏打印结构体。示例代码#[derive(Debug)] // 自动生成 Debug 实现 struct Person { name: String, age: u32, } fn main() { let person = Person { name: String::from("Alice"), age: 30, }; // 使用 println! 打印 println!("Person: {:?}", person); // 输出: Person: Person { name: "Alice", age: 30 } // 使用 dbg! 宏打印(同时返回值) dbg!(&person); // 输出: [src/main.rs:14] &person = Person { name: "Alice", age: 30 } } 关键点#[derive(Debug)]:自动生成 Debug trait 的实现。{:?}:格式化字符串,表示使用 Debug 输出。dbg!:调试宏,打印值和文件位置,并返回值。2. 自定义 Display trait(更友好的输出)如果需要更可读的输出(如用户友好的字符串),可以为结构体实现 Display trait。示例代码use std::fmt; #[derive(Debug)] struct Person { name: String, age: u32, } // 手动实现 Display trait impl fmt::Display for Person { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Person(name={}, age={})", self.name, self.age) } } fn main() { let person = Person { name: String::from("Bob"), age: 25, }; // 使用 Display 输出 println!("Person: {}", person); // 输出: Person(name=Bob, age=25) // 仍然可以使用 Debug 输出 println!("Person (Debug): {:?}", person); // 输出: Person { name: "Bob", age: 25 } } 关键点impl fmt::Display for Person:手动实现 Display trait。write!:用于格式化输出到 Formatter。{}:格式化字符串,表示使用 Display 输出。3. 自定义 Debug 输出如果需要修改 Debug 的默认输出格式,可以手动实现 Debug trait。示例代码use std::fmt; struct Point { x: i32, y: i32, } // 手动实现 Debug trait impl fmt::Debug for Point { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Point(x={}, y={})", self.x, self.y) } } fn main() { let p = Point { x: 10, y: 20 }; println!("Point: {:?}", p); // 输出: Point(x=10, y=20) } 4. 使用第三方库(如 serde)如果需要更复杂的序列化/反序列化(如 JSON 输出),可以使用 serde 库。示例代码# Cargo.toml [dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize, Debug)] struct User { id: u32, username: String, } fn main() { let user = User { id: 1, username: String::from("alice"), }; // 打印为 JSON let json = serde_json::to_string(&user).unwrap(); println!("JSON: {}", json); // 输出: JSON: {"id":1,"username":"alice"} } 总结方法用途格式化字符串是否需要手动实现Debug trait调试输出(默认实现){:?}否(可手动覆盖)Display trait用户友好的输出{}是serde序列化为 JSON/其他格式-是推荐:优先使用 Debug trait 进行调试,必要时实现 Display 或使用 serde。
-
在 Rust 中,#[cfg(feature = "json")] 是一个条件编译属性,用于根据指定的**特性(feature)**是否启用,来决定是否编译包含该属性的代码块。这是 Rust 中 Cargo 工具链提供的功能,通常与 Cargo.toml 文件中的 [features] 部分配合使用。具体解释#[cfg(...)]:这是 Rust 的条件编译宏,全称为 “configuration”(配置)。用于在编译时根据条件决定是否包含某段代码。feature = "json":feature 是 Cargo 的特性标志(feature flag)。"json" 是一个自定义的特性名称,需要在 Cargo.toml 中定义。典型用法1. 在 Cargo.toml 中定义特性[package] name = "my_library" version = "0.1.0" edition = "2021" [features] # 定义一个名为 "json" 的特性 json = [] # 空列表表示该特性不依赖其他 crate2. 在代码中使用条件编译#[cfg(feature = "json")] mod json_utils { // 仅当启用 "json" 特性时,才会编译此模块 pub fn parse_json(data: &str) -> Result<serde_json::Value, serde_json::Error> { serde_json::from_str(data) } } fn main() { #[cfg(feature = "json")] { // 仅当启用 "json" 特性时,才会编译此代码块 let data = r#"{"name": "Alice", "age": 30}"#; let value = json_utils::parse_json(data).unwrap(); println!("Parsed JSON: {:?}", value); } #[cfg(not(feature = "json"))] { println!("JSON support is disabled."); } } 常见场景可选依赖:如果你的库支持 JSON 功能,但用户可能不需要它,可以通过特性启用。例如:serde_json 依赖可以标记为可选,并通过 features 控制是否编译。功能开关:大型项目可能包含多个可选功能(如日志、调试工具等),通过特性分离。减少二进制体积:禁用不需要的特性可以减少最终二进制文件的大小。高级用法1. 依赖其他特性[features] json = ["serde", "serde_json"] # 启用 "json" 时需要同时启用 "serde" 和 "serde_json" 2. 默认特性[features] default = ["json"] # 默认启用 "json" 特性3. 组合特性[features] full = ["json", "xml", "yaml"] # 启用所有功能验证特性是否启用在运行时检查特性是否启用(需配合 cfg! 宏):if cfg!(feature = "json") { println!("JSON support is enabled."); } else { println!("JSON support is disabled."); } 总结#[cfg(feature = "json")] 是一种条件编译机制,用于根据用户是否启用 "json" 特性来控制代码的编译。用途:管理可选依赖、功能开关、减少二进制体积。配置:在 Cargo.toml 的 [features] 部分定义特性及其依赖。通过合理使用特性,可以使你的 Rust 库或程序更加模块化和灵活。
-
在 Rust 中使用 ureq 库发送 JSON POST 请求非常简单。以下是一个完整的示例:use serde_json::json; use ureq; fn main() -> Result<(), ureq::Error> { // 准备要发送的 JSON 数据 let data = json!({ "name": "John Doe", "age": 30, "email": "john@example.com" }); // 发送 POST 请求 let response = ureq::post("https://example.com/api/endpoint") .set("Content-Type", "application/json") // 设置请求头 .send_json(data)?; // 发送 JSON 数据 // 处理响应 let status = response.status(); let body = response.into_string()?; println!("Status: {}", status); println!("Response: {}", body); Ok(()) } 关键点说明:依赖:确保你的 Cargo.toml 中包含:[dependencies] ureq = "2.0" serde_json = "1.0" JSON 构建:使用 serde_json::json! 宏可以方便地构建 JSON 数据请求头设置:必须设置 Content-Type: application/json 头错误处理:ureq 的方法返回 Result 类型,需要处理可能的错误更简洁的写法(Rust 2021+)use serde_json::json; use ureq; fn main() -> Result<(), ureq::Error> { let response = ureq::post("https://example.com/api/endpoint") .set("Content-Type", "application/json") .send_json(json!({ "query": "Rust", "limit": 10 }))?; println!("{}", response.into_string()?); Ok(()) } 注意事项对于复杂的 JSON 结构,可以先定义结构体并使用 serde 序列化考虑使用 ureq::Agent 来重用连接池以提高性能如果需要更高级的功能,可以考虑 reqwest 库
-
在 Rust 中,你可以使用 webbrowser 库来打开浏览器并访问指定的网站。这个库提供了一个简单的接口来打开默认浏览器并导航到指定的 URL。以下是一个示例代码:首先,你需要在你的 Cargo.toml 文件中添加 webbrowser 依赖:[dependencies] webbrowser = "0.5" 然后,你可以使用以下代码来打开浏览器并访问指定的网站:use webbrowser; fn main() { let url = "https://www.example.com"; // 替换为你想要访问的网站 match webbrowser::open(url) { Ok(_) => println!("成功打开浏览器并访问: {}", url), Err(e) => println!("无法打开浏览器: {}", e), } } 在这个示例中,webbrowser::open(url) 会尝试在默认浏览器中打开指定的 URL。如果成功,它将返回 Ok(()),否则返回一个错误信息。请注意,webbrowser 库的行为可能因操作系统而异,并且它可能无法在所有平台上工作,尤其是在某些嵌入式或非标准环境中。
-
在 Rust 中,#[cfg(target_os = "...")] 是一个条件编译属性,用于根据目标操作系统(Target OS)有条件地编译代码。Rust 支持多种目标操作系统,以下是完整的 target_os 列表及其对应的值:Rust 支持的 target_os 列表target_os 值对应的操作系统"android"Android"dragonfly"DragonFly BSD"freebsd"FreeBSD"fuchsia"Fuchsia"illumos"Illumos"ios"iOS"linux"Linux"macos"macOS"netbsd"NetBSD"openbsd"OpenBSD"redox"Redox"solaris"Solaris"tvos"tvOS"vxworks"VxWorks"windows"Windows"haiku"Haiku(较少见)"l4re"L4Re(较少见)"none"裸机环境(无操作系统)说明常见操作系统:"windows":Windows 系统。"linux":Linux 系统。"macos":macOS 系统。"android":Android 系统。"ios":iOS 系统。Unix-like 系统:"freebsd"、"openbsd"、"netbsd"、"dragonfly"、"illumos"、"solaris" 等属于 Unix-like 操作系统。其他系统:"fuchsia":Google 的 Fuchsia 操作系统。"redox":Rust 编写的类 Unix 操作系统。"haiku" 和 "l4re" 是较为少见的操作系统。"none":用于裸机环境(如嵌入式系统,无操作系统)。移动操作系统:"ios" 和 "android" 分别用于 iOS 和 Android 平台的开发。使用示例#[cfg(target_os = "windows")] fn main() { println!("Running on Windows!"); } #[cfg(target_os = "linux")] fn main() { println!("Running on Linux!"); } #[cfg(target_os = "macos")] fn main() { println!("Running on macOS!"); } // 默认情况(如果没有匹配的 target_os) #[cfg(not(any( target_os = "windows", target_os = "linux", target_os = "macos" )))] fn main() { println!("Running on an unsupported OS!"); } 注意事项可以通过 rustc --print target-list 命令查看所有支持的编译目标(包括 target_os)。在跨平台开发中,合理使用条件编译可以确保代码在不同操作系统上的兼容性。希望这个列表能帮助你更好地理解 Rust 中的条件编译和目标操作系统支持!
-
在 Rust 中执行外部程序(例如打开记事本)可以使用标准库中的 std::process::Command 结构体。以下是详细示例和说明:1. 打开记事本(Windows)use std::process::Command; fn main() -> std::io::Result<()> { // Windows 下打开记事本 Command::new("notepad.exe") .arg("C:\\path\\to\\file.txt") // 可选:指定要打开的文件 .spawn()?; // 使用 spawn 异步执行 // 或者使用 status() 等待程序结束 // let exit_status = Command::new("notepad.exe").status()?; // println!("Exit status: {:?}", exit_status); Ok(()) } 2. 跨平台示例(Windows/macOS/Linux)use std::process::Command; fn main() -> std::io::Result<()> { #[cfg(windows)] { // Windows Command::new("notepad.exe").spawn()?; } #[cfg(target_os = "macos")] { // macOS Command::new("open").arg("-a").arg("TextEdit").spawn()?; } #[cfg(unix)] { // Linux (使用 xdg-open 或指定编辑器) Command::new("xdg-open").arg("/path/to/file.txt").spawn()?; // 或者指定编辑器如 gedit/nano // Command::new("gedit").arg("file.txt").spawn()?; } Ok(()) } 3. 关键方法说明Command::new(program) - 指定要执行的程序.arg(argument) - 添加参数(可多次调用).spawn() - 异步执行,返回 Result<Child>.status() - 同步执行并等待完成,返回 ExitStatus.output() - 同步执行并捕获输出(适用于命令行程序)4. 错误处理建议使用 ? 运算符或 match 处理错误:match Command::new("notepad").spawn() { Ok(_) => println!("成功启动记事本"), Err(e) => eprintln!("启动失败: {}", e), } 5. 传递参数示例Command::new("cmd.exe") .args(&["/C", "echo Hello && pause"]) .spawn()?; 注意事项Windows 程序通常不需要路径(如 notepad.exe 在系统 PATH 中)Linux/macOS 可能需要完整路径(如 /usr/bin/gedit)异步执行时,父进程退出会导致子进程终止敏感操作建议检查退出状态码如果需要更复杂的进程控制(如管道通信),可以查看 std::process::Stdio 和 Child 结构体的文档。
-
一、 新建项目使用 cargo new <项目名称> 命令新建项目,得到如下目录结构二、 添加依赖库cd <项目名> 进入项目目录,然后使用 cargo add serde-json 命令添加依赖库三、反序列化(字符串转json)这段 Rust 代码演示了如何使用 serde_json 库来解析 JSON 字符串并访问其中的数据。下面是对代码的逐步解释:1. 导入依赖use serde_json::{Result, Value, from_str}; 从 serde_json 库中导入三个关键组件:Result: 用于处理可能的解析错误(实际上是 serde_json::Result 类型,等同于 Result<Value, serde_json::Error>)Value: 表示可以表示任何 JSON 值的枚举类型from_str: 一个将 JSON 字符串解析为 Value 的函数2. 主函数fn main() -> Result<()>{ 定义主函数,返回类型为 Result<()>,表示可能返回 serde_json::Error 或成功返回 ()(空元组)3. JSON 字符串 let data = r#" { "code": 200, "msg": "插入成功", "data": { "name": "bob", "age": 35, "isMale": true } } "#; 定义一个原始字符串(使用 r#""# 语法允许包含未转义的引号和换行)这个 JSON 对象包含:code: 数字 200msg: 字符串 “插入成功”data: 嵌套对象,包含 name、age 和 isMale 字段4. 打印原始 JSON println!("data: {}", data); 打印原始的 JSON 字符串5. 解析 JSON let v: Value = from_str(data)?; 使用 from_str 函数将 JSON 字符串解析为 Value 类型? 操作符用于错误处理:如果解析失败,会提前返回错误6. 访问解析后的数据 println!("code: {}, msg: {}, data: {}", v["code"], v["msg"], v["data"]); 打印解析后的 JSON 数据:v["code"] 访问 code 字段(数字)v["msg"] 访问 msg 字段(字符串)v["data"] 访问 data 对象(会打印为 JSON 字符串)7. 返回成功 Ok(()) } 表示程序成功执行,返回 Ok(())8. 补充说明Value 类型是 serde_json 提供的枚举,可以表示任何 JSON 值(对象、数组、字符串、数字、布尔值等)当访问不存在的字段时,程序会 panic(因为使用了 [] 索引)更安全的访问方式是使用 get() 方法,它会返回 Option<&Value>完整代码为use serde_json::{Result, Value, from_str}; fn main() -> Result<()>{ let data = r#" { "code": 200, "msg": "插入成功", "data": { "name": "bob", "age": 35, "isMale": true } } "#; println!("data: {}", data); let v: Value = from_str(data)?; println!("code: {}, msg: {}, data: {}", v["code"], v["msg"], v["data"]); Ok(()) } 执行结果如下三、序列化(对象转字符串)use serde_json::{Result, Value, from_str, json}; fn main() -> Result<()>{ let obj = json!({ "name": "Donald John Trump", "age": 78, "phone": [ "+86 13700000001", "+86 18700000002" ] }); println!("first phone number: {}", obj["phone"][0]); println!("{}", obj.to_string()); Ok(()) } json!是一个宏定义,所有以!结尾的函数都是宏定义函数,该函数的作用是将map对象转为json对象
-
Rust所有权1、认识所有权所有权(系统)是 Rust 最独特的功能,其令 Rust 无需垃圾回收(garbage collector)即可保障内存安全。因此,理解Rust 中所有权如何工作是十分重要的。本文我们将讲到所有权以及相关功能:借用、slice 以及 Rust 如何在内存中布局数据。1.1 什么是所有权Rust 的核心功能(之一)是 所有权(ownership)。虽然这个功能说明起来很直观,不过它对语言的其余部分有着更深层的含义。所有程序都必须管理其运行时使用计算机内存的方式。一些语言中使用垃圾回收GC在程序运行过程中来时刻寻找不再被使用的内存,但是stw(stop the world)对性能的伤害极大;在另一些语言中,程序员必须亲自分配和释放内存,比如C、C++。Rust 则选择了第三种方式:内存被一个所有权系统管理,它拥有一系列的规则使编译器在编译时进行检查。任何所有权系统的功能都不会导致运行时开销。因为所有权对很多程序员来说都是一个新概念,需要一些时间来适应。好消息是随着你对 Rust 和所有权系统的规则越来越有经验,你就越能自然地编写出安全和高效的代码。持之以恒!当你理解了所有权系统,你就会对这个使 Rust 如此独特的功能有一个坚实的基础。总结:Rust 中的每一个值都有一个 所有者(owner)。值在任一时刻有且只有一个所有者。当所有者(变量)离开作用域,这个值将被丢弃。Rust通过所有权机制来管理内存,编译器在编译时就会根据所有权规则对内存使用进行检查。Rust内存管理模型:所谓内存管理,就是对内存的分配和释放Rust采用Ownership rules、semantics、Borrow Checker、Lifetime等在编译时期做这些检查,在编译期,如果发现内存有问题的话,直接不让编译通过而且通过所有权机制,来限制内存错误的产生,直接将错误扼杀在摇篮之中,这一套整体的就是所有权机制。Rust只能无限接近C/C++的性能,并不能超越。因为很多底层的还是C/C++术语介绍:STW(Stop the world)“Stop the world"是与垃圾回收(Garbage Collection)相关的术语,它指的是在进行垃圾回收时系统暂停程序的运行。这个术语主要用于描述一种全局性的暂停,即所有应用线程都被停止,以便垃圾回收器能够安全地进行工作。这种全局性的停止会导致一些潜在的问题,特别是对于需要低延迟和高性能的应用程序。需要注意的是,并非所有的垃圾回收算法都需要"stop the world”,有一些现代的垃圾回收器采用了一些技术来减小全局停顿的影响,比如并发垃圾回收和增量垃圾回收。但是,编程语言只要使用了GC,就没办法和没有GC的语言进行性能比较。1.2 栈(Stack)与堆(Heap)在很多语言中并不经常需要考虑到栈与堆。不过在像 Rust 这样的系统编程语言中,值是位于栈上还是堆上在更大程度上影响了语言的行为以及为何必须做出这样的选择。我们会在本文的稍后部分描述所有权与堆与栈相关的部分,所以这里只是一个用来预热的简要解释。栈和堆都是代码在运行时可供使用的内存部分,不过他们以不同的结构组成。栈以放入值的顺序存储并以相反顺序取出值。这也被称作 后进先出(last in, first out)。栈:即一个后进先出的模式。增加数据叫进栈, 移出数据叫出栈。栈中所有的数据必须暂用已知且固定大小。编译的时候,数据的类型和大小是固定的,就分配在栈上堆:即内存中的一块区域, 编译时大小未知或大小可能变化的数据, 存储在堆上。栈比堆分配内存要快, 因为入栈时无需为新存储的数据查找合适空间, 因为其位置总是在栈顶。相反, 堆分配要做更多的工作,要找一个足够的空间,并记录。访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。当你的代码调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。想象一下一叠盘子:当增加更多盘子时,把他们放在盘子堆的顶部,当需要盘子时,也从顶部拿走。不能从中间也不能从底部增加或拿走盘子!增加数据叫做 进栈(pushing ontothe stack),而移出数据叫做 出栈(popping off the stack)。操作栈是非常快的,因为它访问数据的方式:永远也不需要寻找一个位置放入新数据或者取出数据因为这个位置总是在栈顶。另一个使得栈快速的性质是栈中的所有数据都必须有一个已知且固定的大小。对于在编译时未知大小或大小可能变化的数据,可以把他们储存在堆上。堆是缺乏组织的:当向堆放入数据时,我们请求一定大小的空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回给我们一个其位置的 指针(pointer)。这个过程称作 在堆上分配内存(allocating on the heap),并且有时这个过程就简称为“分配”(allocating)。向栈中放入数据并不被认为是分配。因为指针是已知的固定大小的,我们可以将指针储存在栈上,不过当需要实际数据时,必须访问指针。想象一下去餐馆就坐吃饭。当进入时,你说明有几个人,餐馆员工会找到一个够大的空桌子并领你们过去。如果有人来迟了,他们也可以通过询问来找到你们坐在哪。访问堆上的数据要比访问栈上的数据要慢因为必须通过指针来访问。现代处理器在内存中跳转越少就越快(缓存)。继续类比,假设有一个服务员在餐厅里处理多个桌子的点菜。在一个桌子报完所有菜后再移动到下一个桌子是最有效率的。从桌子 A 听一个菜,接着桌子 B 听一个菜,然后再桌子 A,然后再桌子 B 这样的流程会更加缓慢。出于同样原因,处理器在处理的数据之间彼此较近的时候(比如在栈上)比较远的时候(比如可能在堆上)能更好的工作。在堆上分配大量的空间也可能消耗时间。当调用一个函数,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。记录何处的代码在使用堆上的什么数据,最小化堆上的冗余数据的数量以及清理堆上不再使用的数据以致不至于耗尽空间,这些所有的问题正是所有权系统要处理的。一旦理解了所有权,你就不需要经常考虑栈和堆了,不过理解如何管理堆内存可以帮助我们理解所有权为何存在以及为什么要以这种方式工作。1.3 所有权规则首先,让我们看一下所有权的规则。请记住它们,我们将讲解一些它们的例子:Rust 中每一个值都有一个称之为其 所有者(owner)的变量。值有且只能有一个所有者。当所有者(变量)离开作用域,这个值将被丢弃。1.4 变量作用域作为所有权的第一个例子,我们看看一些变量的 作用域(scope)。作用域是一个项(原文:item) 在程序中有效的范围。假设有这样一个变量:let s = “hello”;变量 s 绑定到了一个字符串字面值,这个字符串值是硬编码进程序代码中的。这个变量从声明的点开始直到当前 作用域 结束时都是有效的。如下示例的注释标明了变量 s 在何处是有效的:{ // s is not valid here, it’s not yet declared let s = "hello"; // s is valid from this point forward // do stuff with s} // this scope is now over, and s is no longer valid换句话说,这里有两个重要的点:当 s 进入作用域 时,它就是有效的。这一直持续到它 离开作用域 为止。目前为止,变量是否有效与作用域的关系跟其他编程语言是类似的。如下,变量y只在花括号中有效,在花括号外面无效,因此打印y会报错上面的x和y就分配在栈上1.5 String 与 &strRust中表示字符串的两种类型:String和&strString是一个堆分配的可变字符串类型,可以动态增长或缩小。拥有所有权String类型。编译器在编译时,是不知道它的大小的,因此,String类型的数据分配在堆上String源码pub struct String { vec: Vec<u8>}123&str 字符串字面量,是指字符串切片引用,是借用的字符串切片,不拥有数据。是在栈上分配的。是不可变引用,指向存储在其他地方的UTF-8编码的字符串数据,由指针和长度构成可以指向堆上的 String 数据(如 &String 可强转为 &str)。也可以是静态字符串字面量(如 “hello”,存储在程序的只读内存中)。如何相互转换?&str → String(获取所有权)let s: &str = "hello";let s_string: String = s.to_string(); // 或 String::from(s)let s1 = "景天".to_owned();let s2 = String::from("Hello World")String → &str(借用)let s: String = String::from("hello");let s_slice: &str = &s; // 自动解引用为 &strString与&str如何选择注意String是具有所有权的,而&str并没有Struct中属性使用String如果不使用显式声明生命周期无法使用&str不只是麻烦,还有更多的隐患函数参数推荐使用&str(如果不想交出所有权)&str为参数,可以传递&str和&String&String为参数,只能传递&String不能传递&str用 String:需要修改字符串(如 push_str、replace)。需要拥有字符串(如返回字符串、存储到结构体)。用 &str:只需要读取字符串(如函数参数)。使用字符串字面量(如 “hello”)。&String 和 &str 区别&String 和 &str 都是字符串的引用,但它们在类型、灵活性、自动转换等方面有重要区别。以下是它们的详细对比:类型与本质类型 本质 存储方式&String 对 String 的不可变引用 指向堆上的 String 数据&str 字符串切片(任意字符串的只读视图) 可以指向堆或静态内存(如 “abc”)关键区别&String 只能引用 String 类型的数据。&str 可以引用:String 的数据(如 &some_string[…])静态字符串字面量(如 “hello”)其他任意合法的字符串切片。灵活性与兼容性&str 更通用函数参数通常使用 &str 而不是 &String,因为:&String 可以自动隐式转换为 &str(通过 Deref Coercion)。&str 可以接受 String 或字面量,而 &String 只能接受 String。内存布局&String是一个指向 String 结构的指针(String 本身包含堆上的数据指针、长度和容量)。内存布局:ptr → (data_ptr, len, capacity)。&str是一个胖指针(包含数据指针和长度)。内存布局:(data_ptr, len)。let s = String::from("Hello");let string_ref: &String = &s; // 指向 String 结构let str_slice: &str = &s[..]; // 指向实际字符串数据123使用场景用 &String需要明确限制参数必须是 String 的引用(罕见情况)。需要访问 String 的方法(如 .capacity())。用 &str(更常见)函数参数(兼容性更好)。只读操作(如字符串查找、切片)。避免不必要的所有权转移。自动转换(Deref Coercion)Rust 会自动将 &String 转换为 &str,因此几乎不需要手动写 &String:fn print_str(s: &str) { /* ... */ }let s = String::from("Rust");这些方面也同样适用于其他标准库提供的或你自己创建的复杂数据类型。我们已经见过字符串字面值了,它被硬编码进程序里。字符串字面值是很方便的,不过他们并不总是适合所有需要使用文本的场景。原因之一就是他们是不可变的。另一个原因是不是所有字符串的值都能在编写代码时就知道:例如,如果想要获取用户输入并储存该怎么办呢?为此,Rust 有第二个字符串类型, String 。这个类型储存在堆上所以能够储存在编译时未知大小的文本。可以用 from 函数从字符串字面值来创建 String ,如下:let s = String::from(“hello”);这两个冒号( :: )运算符允许将特定的 from 函数置于 String 类型的命名空间(namespace)下而不需要使用类似string_from 这样的名字。这类字符串 可以 使用mut 被修改:let mut s = String::from("hello");s.push_str(", world!"); // push_str() appends a literal to a Stringprintln!("{}", s); // This will print `hello, world!`123原先s的指针指向hello的内存地址。当执行s.push_str(“, world”)时。操作系统将重新开辟一块新的内存,用来存放追加后的字符串因此String类型的大小是不固定的,分配在堆上1.6 内存与分配对于字符串字面值的情况,我们在编译时就知道其内容所以它直接被硬编码进最终的可执行文件中,这使得字符串字面值快速且高效。不过这些属性都只来源于其不可变性。不幸的是,我们不能为了每一个在编译时未知大小的文本而将一块内存放入二进制文件中而它的大小还可能随着程序运行而改变。对于 String 类型,为了支持一个可变,可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容。这意味着:内存必须在运行时向操作系统请求。需要一个当我们处理完 String 时将内存返回给操作系统的方法。第一部分由我们完成:当调用 String::from 时,它的实现 (implementation) 请求其所需的内存。这在编程语言中是非常通用的。然而,第二部分实现起来就各有区别了。在有 垃圾回收(garbage collector,GC)的语言中, GC 记录并清除不再使用的内存,而我们作为程序员,并不需要关心他们。没有 GC 的话,识别出不再使用的内存并调用代码显式释放就是我们程序员的责任了,正如请求内存的时候一样。从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过早回收了,将会出现无效变量。如果重复回收,这也是个 bug。我们需要 allocate 和free 一一对应。Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。下面是示例 中作用域例子的一个使用 String 而不是字符串字面值的版本:{ let s = String::from("hello"); // s is valid from this point forward // do stuff with s} // this scope is now over, and s is no // longer valid12345这里是一个将 String 需要的内存返回给操作系统的很自然的位置:当 s 离开作用域的时候。当变量离开作用域,Rust为其调用一个特殊的函数。这个函数叫做 drop ,在这里 String 的作者可以放置释放内存的代码。Rust 在结尾的 } 处自动调用 drop 。注意:在 C++ 中,这种 item 在生命周期结束时释放资源的方法有时被称作 资源获取 即初始化(Resource Acquisition Is Initialization (RAII))。如果你使用过 RAII 模式的话应该对 Rust 的 drop 函数并不陌生。这个模式对编写 Rust 代码的方式有着深远的影响。现在它看起来很简单,不过在更复杂的场景下代码的行为可能是不可预测的,比如当有多个变量使用在堆上分配的内存时。现在让我们探索一些这样的场景。1.7 变量与数据交互的方式(一):移动(move)Rust 中的多个变量可以采用一种独特的方式与同一数据交互。让我们看看如下示例中一个使用整型的例子:let x = 5;let y = x;12将变量 x 赋值给 y根据其他语言的经验我们大致可以猜到这在干什么:“将 5 绑定到 x ;接着生成一个值 x 的拷贝并绑定到 y ”。现在有了两个变量, x 和 y ,都等于 5 。这也正是事实上发生了的,因为正数是有已知固定大小的简单值,所以这两个5 被放入了栈中。现在看看这个 String 版本:let s1 = String::from("hello");let s2 = s1;12这看起来与上面的代码非常类似,所以我们可能会假设他们的运行方式也是类似的:也就是说,第二行可能会生成一个s1 的拷贝并绑定到 s2 上。不过,事实上并不完全是这样。为了更全面的解释这个问题,让我们看看如下图 中 String 真正是什么样的。String 由三部分组成,如图所示:一个指向存放字符串内容内存的指针,一个长度,和一个容量。这一组数据储存在栈上。右侧则是堆上存放内容的内存部分。一个绑定到 s1 的拥有值 “hello” 的 String 的内存表现长度代表当前 String 的内容使用了多少字节的内存。容量是 String 从操作系统总共获取了多少字节的内存。长度与容量的区别是很重要的,不过这在目前为止的场景中并不重要,所以可以暂时忽略容量。当我们把 s1 赋值给 s2 , String 的数据被复制了,这意味着我们从栈上拷贝了它的指针、长度和容量。我们并没有复制堆上指针所指向的数据。换句话说,内存中数据的表现如图所示。这个表现形式看起来 并不像 如下图中的那样,但是如果 Rust 也拷贝了堆上的数据后内存看起来会是如何呢。如果 Rust这么做了,那么操作 s2 = s1 在堆上数据比较大的时候可能会对运行时性能造成非常大的影响。之前,我们提到过当变量离开作用域后 Rust 自动调用 drop 函数并清理变量的堆内存。不过如下图 展示了两个数据指针指向了同一位置。这就有了一个问题:当 s2 和 s1 离开作用域,他们都会尝试释放相同的内存。这是一个叫做 二次释放(double free)的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。为了确保内存安全,这种场景下 Rust 的处理有另一个细节值得注意。与其尝试拷贝被分配的内存,Rust 则认为 s1 不再有效,因此 Rust 不需要在 s1 离开作用域后清理任何东西。看看在 s2 被创建之后尝试使用 s1 会发生什么:let s1 = String::from("hello");let s2 = s1;println!("{}, world!", s1);println!("{}, world!", s2);你会得到一个类似如下的错误,因为 Rust 禁止你使用无效的引用。如果你在其他语言中听说过术语 “浅拷贝”(“shallow copy”)和 “深拷贝”(“deep copy”),那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效化了,这个操作被称为 移动(move),而不是浅拷贝。上面的例子可以解读为 s1 被 移动 到了 s2 中。那么具体发生了什么,如图 所示。这样就解决了我们的麻烦!因为只有 s2 是有效的,当其离开作用域,它就释放自己的内存,完毕。另外,这里还隐含了一个设计选择:Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何 自动 的复制可以被认为对运行时性能影响较小。Rust针对move的赋值方式在move的赋值方式下,它的所有权ownership会发生改变比如struct,String等复杂的数据类型的赋值都是move的。因为复杂的数据类型,在赋值时,将所有权交出去,会节省很大的空间,提高性能,防止数据竞争的出现因此,对于move类型的数据操作,你必须非常清楚值的所有权在什么地方,如果一个变量,将其值的所有权交给另一个变量后,继续去使用,编译器将会报错1.8 变量与数据交互的方式(二):克隆如果我们 确实 需要深度复制 String 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone 的通用函数。let s1 = String::from("hello");let s2 = s1.clone();println!("{}, world!", s1);println!("{}, world!", s2);这段代码能正常运行,这里堆上的数据 确实 被复制了。当出现 clone 调用时,你知道一些特定的代码被执行而且这些代码可能相当消耗资源。你很容易察觉到一些不寻常的事情正在发生。1.9 只在栈上的数据:拷贝这里还有一个没有提到的小窍门。这些代码使用了整型并且是有效的let x = 5;let y = x;println!("x = {}, y = {}", x, y);123他们似乎与我们刚刚学到的内容相抵触:没有调用 clone ,不过 x 依然有效且没有被移动到 y 中。原因是像整型这样的在编译时已知大小的类型被整个储存在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 y 后使 x 无效。换句话说,这里没有深浅拷贝的区别,所以这里调用 clone 并不会与通常的浅拷贝有什么不同,我们可以不用管它。Rust 有一个叫做 Copy trait 的特殊注解,可以用在类似整型这样的储存在栈上的类型。如果一个类型拥有 Copy trait,一个旧的变量在将其赋值给其他变量后仍然可用。Rust 不允许自身或其任何部分实现了 Droptrait 的类型使用 Copy trait。如果我们对其值离开作用域时需要特殊处理的类型使用 Copy 注解,将会出现一个编译时错误。那么什么类型是 Copy 的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则,任何简单标量值的组合可以是 Copy 的,任何需要分配内存,或者本身就是某种形式资源的类型不会是 Copy 的。如下是一些 Copy 的类型:所有整数类型,比如 u32 。布尔类型, bool ,它的值是 true 和 false 。所有浮点数类型,比如 f64 。字符类型,char元组,当且仅当其包含的类型也都是 Copy 的时候。 (i32, i32) 是 Copy 的,不过 (i32, String) 就不是。1.10 所有权与函数将值传递给函数在语义上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。如下示例是一个展示变量何时进入和离开作用域的例子:fn main() { //所有权与函数 let s = String::from("hello"); takes_ownership(s); let x = 5; makes_copy(x);}fn takes_ownership(s:String){ println!("s= {}",s)}fn makes_copy(i:i32){ println!("i= {}",i) }运行,能拿到数据我们知道,变量s的作用域就在takes_ownership()函数之中,当执行完这个函数,s就被回收了我们在函数执行完,在打印s看看fn main() { //所有权与函数 let s = String::from("hello"); takes_ownership(s); println!("执行完函数的s={}",s); let x = 5; makes_copy(x);}fn takes_ownership(s:String){ println!("s= {}",s)}fn makes_copy(i:i32){ println!("i= {}",i) }可以看到s已经被回收了所有权规则,会阻止我们在作用域之外使用变量如果想要使用s,可以在定义函数时,将字符串返回。然后在main函数中创建个变量接收但是x在执行完函数后还是可以使用的x是整形,声明在栈上的。因为整形值的组合可以是 Copy 的,不需要分配内存1.11 返回值与作用域上面的两个函数,字符串类型的参数,执行完函数之后,变量就被回收了。如果想要延长该变量的作用域,可以将该变量返回。返回值也可以转移作用域。fn main() { //所有权与函数 let s = String::from("hello"); let y= takes_ownership(s); println!("执行完函数的y={}",y); let x = 5; makes_copy(x); //我们试用下x println!("执行完函数之后的x={}",x)}fn takes_ownership(s:String) ->String{ println!("s= {}",s); s}fn makes_copy(i:i32){ println!("i= {}",i) }变量的所有权总是遵循相同的模式:将值赋值给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。在每一个函数中都获取并接着返回所有权可能有些冗余。如果我们想要函数使用一个值但不获取所有权该怎么办呢?如果我们还要接着使用它的话,每次都传递出去再传回来就有点烦人了,另外我们也可能想要返回函数体产生的任何(不止一个)数据。可以使用元组来返回多个值,像这样:fn main() { let s1 = String::from("hello"); let (s2, len) = calculate_length(s1); println!("The length of '{}' is {}.", s2, len);}fn calculate_length(s: String) -> (String, usize) { let length = s.len(); // len() returns the length of a String. (s, length)}但是这未免有些形式主义,而且这种场景应该很常见。幸运的是,Rust 对此提供了一个功能,叫做 引用(references)。在 Rust 中,字符串(String 类型)是可变且拥有所有权的,而字符串字面值(&str 类型)是不可变的引用。如果你想通过函数修改字符串并返回新的字符串,有几种常见的方法:方法 1:传入 String,修改后返回fn main() { // let a = String::from("hello"); let b = modyfy_string(a); // println!("{:?}", a); // 这里会报错,因为a的所有权已经被转移到b println!("{:?}", b); // 这里可以使用b,因为b是一个新的字符串}//函数传个字符串,修改后返回字符串fn modyfy_string(s: String) -> String { let mut s = s; s.push_str(" world"); s}2、引用与借用上面结尾的元组代码有这样一个问题:我们不得不将 String 返回给调用函数,以便仍能在调用 calculate_length后使用 String ,因为 String 被移动到了 calculate_length 内。下面是如何定义并使用一个(新的) calculate_length 函数,它以一个对象的 引用 作为参数而不是获取值的所有权:2.1 引用语法引用的用法:就是取地址&,将地址作为参数传进去。创建一个指向值的引用,但不拥有它,因为不拥有这个值,所以当引用离开其值指向的作用域后也不会被丢弃。fn main() { //为了防止变量每次用一次,就不能再使用了,Rust可以借助引用来实现 let s1 = String::from("hello"); //注意,这里根据函数的定义,参数是内存地址,所以把s1的地址传进去 let len = calc_length(&s1); //如果按照之前的方法,到这里s1就不能使用了,但是我们这里传的是引用。因此s1可以继续使用 println!("s1={}, len={}",s1,len); } //定义一个返回字符串函数//将字符串的引用作为传参类型,就不必考虑作用域问题了fn calc_length(s:&String)->usize{ s.len()}我们看到s1在函数调用后,可以继续使用首先,注意变量声明和函数返回值中的所有元组代码都消失了。其次,注意我们传递 &s1 给 calculate_length ,同时在函数定义中,我们使用的是 &String 而不是 String 。这些 & 符号就是 引用,他们允许你使用值但不获取其所有权。注意:与使用 & 引用相对的操作是 解引用(dereferencing),解引用运算符是 *我们仔细看看这个函数的调用let len = calc_length(&s1);&s1 语法允许我们创建一个 指向 值 s1 的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域时其指向的值也不会被丢弃。同理,函数定义使用了 & 来表明参数 s 的类型是一个引用。让我们增加一些解释性的注解:fn calculate_length(s: &String) -> usize { // s is a reference to a Strings.len()} // Here, s goes out of scope. But because it does not have ownership of what// it refers to, nothing happens.变量 s 有效的作用域与函数参数的作用域一样,不过当引用离开作用域后并不丢弃它指向的数据,因为我们没有所有权。函数使用引用而不是实际值作为参数意味着无需返回值来交还所有权,因为就不曾拥有所有权。我们将获取引用作为函数参数称为 借用(borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。传参字符串,根据空格分隔返回第一个单词fn main() { let a = String::from("hello world"); let b = first_word(&a); println!("{:?}", a); // 这里可以使用a,因为a的所有权没有被转移 println!("{:?}", b); // 这里可以使用b,因为b是一个字符串切片,引用了a的内存}//传字符串,根据空格返回第一个单词fn first_word(s: &String) -> &str { //将字符串地址转换为字节数组 let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..]}传入的是切片引用,传出的也是切片引用这样做的前提是只有一个传入和一个传出,如果有多个,生命周期不可推算,就无法运行2.2 可变引用如果我们尝试修改借用的变量按照常规思路,直接在函数中修改,是行不通的,因为正如变量默认是不可改变的,引用也一样,(默认)不允许修改引用的值。我们需要在函数定义时,将引用改为可变引用,使用mut关键字来定义可变引用并且,在调用时,使用&mut 来借用fn main() { //为了防止变量每次用一次,就不能再使用了,Rust可以借助引用来实现 let mut s1 = String::from("hello"); //调用可变引用 //借用 &mut change(&mut s1); //如果按照之前的方法,到这里s1就不能使用了,但是我们这里传的是引用。因此s1可以继续使用 println!("s1={}",s1); }//可变引用fn change(s :&mut String){ s.push_str(", world!");}修改成功对于某个可变引用,发生过借用之后,是不可以再使用的。因为借用之后,s1可能会发生变化,如果再使用s1没可能会发生数据竞争———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/littlefun591/article/details/147412048
-
在 Rust 中,'a 是一种生命周期标注(lifetime annotation),用于描述引用(reference)的生命周期范围。Rust 的生命周期机制是为了确保内存安全,避免悬垂引用(dangling reference)或引用超出其作用域。生命周期标注的基本含义生命周期的概念:生命周期是引用有效的范围,即引用从创建到被销毁的整个过程。Rust 的编译器需要知道引用的生命周期,以确保引用始终指向有效的数据。'a 的含义:'a 是一个生命周期标识符,通常由开发者命名(也可以使用其他名称,比如 'b、'x 等)。它表示某个引用的生命周期范围。生命周期标注的语法:生命周期标注紧跟在 & 后面,形式为 &'a T,表示一个具有生命周期 'a 的类型 T 的引用。生命周期标注的使用场景1. 函数参数和返回值当函数接受引用作为参数,并返回引用时,Rust 编译器需要知道这些引用的生命周期关系。例如:fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } 解释:'a 是一个生命周期参数,表示 x 和 y 的生命周期,以及返回值的生命周期。编译器会确保返回值的生命周期不会超过 x 和 y 中较短的那个。2. 结构体中的引用如果结构体中包含引用,需要显式标注引用的生命周期。例如:struct ImportantExcerpt<'a> { part: &'a str, } fn main() { let novel = String::from("Call me Ishmael."); let first_sentence = novel.split('.').next().expect("Could not find a '.'"); let excerpt = ImportantExcerpt { part: first_sentence }; println!("{}", excerpt.part); } 解释:ImportantExcerpt 结构体包含一个引用 part,其生命周期被标注为 'a。'a 表示 part 的生命周期不能超过它所引用的数据(novel)的生命周期。3. 生命周期省略(Lifetime Elision)在许多情况下,Rust 编译器能够自动推断引用的生命周期,而不需要显式标注。这种现象称为生命周期省略。例如:fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } 解释:这里没有显式标注生命周期,但编译器能够根据上下文推断出 s 和返回值的生命周期关系。生命周期标注的规则每个引用都有一个生命周期:即使没有显式标注,Rust 也会为每个引用分配一个隐式的生命周期。生命周期参数:生命周期参数通常以单引号开头(如 'a),用于表示泛型生命周期。生命周期省略规则:Rust 编译器遵循一组规则来省略生命周期标注,但在复杂情况下(如多个引用参数或返回引用时),可能需要显式标注。总结'a 是 Rust 中的生命周期标识符,用于描述引用的生命周期范围。它帮助 Rust 编译器确保引用始终指向有效的数据,避免悬垂引用。生命周期标注通常用于函数参数、返回值和结构体中的引用。在许多情况下,Rust 编译器能够自动推断生命周期,但在复杂情况下需要显式标注。通过理解生命周期,你可以更好地掌握 Rust 的内存安全机制,并编写更健壮的代码。
-
Rust 是一种系统编程语言,但它也支持许多函数式编程的概念。这使得开发者可以利用函数式编程的优点,如不可变性、高阶函数和简洁的代码风格。以下是一些在 Rust 中应用函数式编程概念的方式:1. 不可变性Rust 的默认变量绑定是不可变的,这意味着一旦赋值,就不能再改变。这类似于函数式编程中的不可变数据结构。let x = 5; // x = 10; // 这行代码会导致编译错误,因为 x 是不可变的 2. 借用与所有权Rust 的所有权系统允许以安全的方式在函数之间传递数据,而不需要垃圾回收。通过借用,你可以临时使用数据而不转移所有权。fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{}' is {}", s1, len); } fn calculate_length(s: &String) -> usize { s.len() } 在上面的例子中,calculate_length 函数借用了 s1 的引用,而不是获取它的所有权。3. 闭包和高阶函数闭包是匿名函数,可以捕获其环境中的变量。在 Rust 中,闭包是函数式编程的重要组成部分。fn main() { let numbers = vec![1, 2, 3, 4, 5]; let closure = |&x| x * x; let results: Vec<i32> = numbers.iter().map(closure).collect(); println!("{:?}", results); } 这里,我们定义了一个闭包 |&x| x * x,并将其传递给 map 函数,map 是一个高阶函数,它接受一个闭包并应用于向量的每个元素。4. 模式匹配Rust 的模式匹配功能非常强大,允许对数据结构进行解构和匹配,这在函数式编程中很常见。enum Option<T> { Some(T), None, } fn main() { let some_number = Some(5); match some_number { Some(x) => println!("Matched: {}", x), None => println!("No match"), } } 5. 不可变数据结构与持久性Rust 鼓励使用不可变数据结构,这有助于编写更安全和并发的代码。许多函数式语言也强调不可变性。6. 函数组合子虽然 Rust 没有像 Haskell 那样内建的函数组合子库,但你可以通过定义高阶函数和使用闭包来实现类似的功能。总之,虽然 Rust 是一种系统编程语言,但它提供了足够的支持来实现函数式编程风格。这种结合使得 Rust 成为一种强大且灵活的编程语言,适用于从底层系统编程到高级函数式编程的各种场景。
-
在Rust中,实现多进程编程通常涉及创建子进程,并在父进程与子进程之间进行通信和同步。Rust标准库提供了std::process模块,用于创建和管理进程。以下是对Rust多进程编程的详细解释:一、创建子进程在Rust中,可以使用std::process::Command来创建子进程。以下是一个简单的示例:use std::process::Command; fn main() { // 创建一个新的进程,执行`ls -l`命令 let mut child = Command::new("ls") .arg("-l") .spawn() .expect("Failed to spawn child process"); // 等待子进程结束,并获取其退出状态 let exit_status = child.wait().expect("Failed to wait on child process"); // 打印子进程的退出状态 println!("Child process exited with status: {}", exit_status); } 二、进程间通信进程间通信(IPC)是多进程编程中的一个重要方面。Rust提供了多种方式来实现进程间通信,例如通过管道、文件、共享内存或消息队列等。以下是一些常见的IPC方法:管道:Rust的std::process::Stdio支持管道重定向,可以实现父进程与子进程之间的数据传输。例如,可以将子进程的输出重定向到管道,并在父进程中读取该管道的数据。文件:通过共享文件来实现进程间通信。父进程和子进程可以读写同一个文件,从而实现数据交换。消息队列:在更复杂的场景中,可以使用消息队列等高级IPC机制。Rust的std::os::unix::prelude模块提供了对Unix域套接字的支持,可以用于实现跨进程的消息传递。三、同步机制在多进程编程中,同步机制用于确保进程之间的协调和数据一致性。Rust提供了多种同步原语,例如互斥锁(Mutex)、条件变量(Condvar)和通道(Channel)等。然而,这些同步原语主要用于线程之间的同步。在多进程编程中,由于进程之间的内存空间是隔离的,因此需要使用进程间的同步机制,如信号量、消息队列或文件锁等。四、使用第三方库除了标准库提供的功能外,Rust社区还开发了许多用于多进程编程的第三方库。例如,tokio-process库提供了对异步进程管理的支持,可以更方便地处理大量进程的创建和管理。五、注意事项资源管理:在多进程编程中,需要仔细管理资源(如文件描述符、内存等),以避免资源泄漏或竞争条件。错误处理:在创建和管理进程时,可能会遇到各种错误(如命令不存在、权限不足等)。因此,需要妥善处理这些错误,以确保程序的健壮性。性能考虑:进程创建和上下文切换的开销较大,因此在设计多进程程序时需要考虑性能因素,避免创建过多的进程。六、示例代码以下是一个使用管道进行进程间通信的示例:use std::io::{self, BufReader, BufRead}; use std::process::{Command, Stdio}; fn main() { // 创建一个新的进程,执行`echo "Hello, World!"`命令,并将输出重定向到管道 let mut child = Command::new("echo") .arg("Hello, World!") .stdout(Stdio::piped()) .spawn() .expect("Failed to spawn child process"); // 获取子进程的输出管道 let stdout = child.stdout.take().expect("Failed to get stdout pipe"); // 读取子进程的输出 let mut reader = BufReader::new(stdout); let mut line = String::new(); if reader.read_line(&mut line).expect("Failed to read from stdout pipe") > 0 { println!("Received from child: {}", line.trim()); } // 等待子进程结束 child.wait().expect("Failed to wait on child process"); } 在这个示例中,父进程创建了一个子进程来执行echo "Hello, World!"命令,并将子进程的输出重定向到管道。父进程通过读取该管道来获取子进程的输出。总结来看,Rust提供了丰富的工具和功能来支持多进程编程。通过合理使用标准库和第三方库,可以方便地实现进程创建、通信和同步等功能。
-
在 Rust 中,宏和函数是两种强大的元编程工具,但它们有不同的用途和特性。下面是它们之间的主要区别,以及如何使用宏生成函数。宏和函数的区别定义和调用方式:函数:使用 fn 关键字定义,调用时使用函数名和括号。例如:fn add(a: i32, b: i32) -> i32 { a + b } let result = add(2, 3); 宏:使用 macro_rules! 定义,调用时使用宏名和感叹号。例如:macro_rules! add { ($a:expr, $b:expr) => { $a + $b }; } let result = add!(2, 3); 代码展开:函数:在编译时,函数调用被解析为一个具体的地址调用。宏:在编译早期(语法分析阶段),宏调用被展开为代码。宏是模式匹配的,基于输入生成代码。作用域和可见性:函数:作用域和可见性由模块系统控制。宏:同样由模块系统控制,但宏可以在其作用域内通过 #[macro_use] 属性导入。性能和内联:函数:可以标记为 inline 以提示编译器进行内联优化。宏:代码在编译时直接展开,通常没有运行时开销。用途:函数:适用于需要执行特定任务的场景,尤其是在需要运行时行为时。宏:适用于代码生成、减少重复代码、以及需要编译时计算的场景。使用宏生成函数有时你可能希望使用宏来生成类似的函数,以避免重复代码。下面是一个简单的示例,展示如何使用宏生成多个函数:macro_rules! generate_function { ($func_name:ident, $op:tt) => { fn $func_name(a: i32, b: i32) -> i32 { a $op b } }; } // 使用宏生成加法函数 generate_function!(add, +); // 使用宏生成乘法函数 generate_function!(multiply, *); fn main() { println!("Add: {}", add(3, 4)); // 输出: Add: 7 println!("Multiply: {}", multiply(3, 4)); // 输出: Multiply: 12 } 解释macro_rules!:定义宏,$func_name:ident 和 $op:tt 是宏的参数,其中 $func_name 是一个标识符,$op 是一个标记树(token tree),用于匹配运算符。代码展开:宏根据传入的参数生成对应的函数代码。在上面的例子中,add 和 multiply 函数分别被生成为执行加法和乘法的函数。调用宏:通过 generate_function! 宏,我们可以轻松生成多个具有相似结构的函数,而无需重复编写相同的代码。这种方式在需要生成大量类似函数时非常有用,可以减少代码重复并提高可维护性。
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签