使用Rust的10个显而易见的好处

Rust是一种年轻且雄心勃勃的系统编程语言。 它实现了自动内存管理,而没有垃圾收集器和其他执行时间开销。 另外,Rust语言中使用了默认语言,访问可变数据的规则空前,并且还考虑了链接寿命。 由于缺乏数据竞速,这使他能够保证内存安全性并促进多线程编程。



所有这一切对于至少跟随现代编程技术发展的每个人都是众所周知的。 但是,如果您不是系统程序员,并且项目中没有很多多线程代码,该怎么办,但是Rust的性能仍然吸引着您。 在应用程序中使用它还会获得其他好处吗? 还是他还会给您带来的全部挑战是与编译器的艰苦斗争,这将迫使您编写程序,使其始终遵循借用和所有权语言的规则?


本文收集了数十种使用Rust的非显而易见的和未特别宣传的优点,我希望这些优点将帮助您决定在项目中选择这种语言。


1.语言的普遍性


尽管Rust定位为系统编程语言,但它也适合解决高级应用问题。 除非您的任务需要原始指针,否则不必使用原始指针。 标准语言库已经实现了应用程序开发中可能需要的大多数类型和功能。 您也可以轻松连接外部库并使用它们。 Rust中的类型系统和通用编程允许使用相当高水平的抽象,尽管该语言没有直接支持OOP。


让我们看一些使用Rust的简单示例。


在两个成对的元素上将两个迭代器组合为一个迭代器的示例:


let zipper: Vec<_> = (1..).zip("foo".chars()).collect(); assert_eq!((1, 'f'), zipper[0]); assert_eq!((2, 'o'), zipper[1]); assert_eq!((3, 'o'), zipper[2]); 


注意:对格式name!(...)的调用name!(...)是对功能宏的调用。 Rust中此类宏的名称始终以符号结尾! 这样就可以将它们与函数名和其他标识符区分开。 使用宏的好处将在下面讨论。

使用外部regex库处理正则表达式的示例:


 extern crate regex; use regex::Regex; let re = Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap(); assert!(re.is_match("2018-12-06")); 


为自己的Point结构实现Add以重载加法运算符的示例:


 use std::ops::Add; struct Point { x: i32, y: i32, } impl Add for Point { type Output = Point; fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y } } } let p1 = Point { x: 1, y: 0 }; let p2 = Point { x: 2, y: 3 }; let p3 = p1 + p2; 


在结构中使用通用类型的示例:


 struct Point<T> { x: T, y: T, } let int_origin = Point { x: 0, y: 0 }; let float_origin = Point { x: 0.0, y: 0.0 }; 


在Rust上,您可以编写高效的系统实用程序,大型桌面应用程序,微服务,Web应用程序(包括客户端部分,因为Rust可以在Wasm中进行编译),移动应用程序(尽管在这个方向上语言生态系统的开发仍然很差)。 这种多功能性对于多项目团队而言可能是一个优势,因为它使您可以在许多不同项目中使用相同的方法和相同的模块。 如果您习惯于每种工具都是针对其狭窄的应用领域而设计的,那么请尝试将Rust视为具有相同可靠性和便利性的工具箱。 也许这正是您所缺少的。


2.便捷的构建和依赖管理工具


显然,这没有广告,但是许多人注意到Rust具有当今可用的最佳构建和依赖管理系统之一。 如果您使用C或C ++进行编程,并且无痛使用外部库的问题对您来说非常紧迫,那么使用Rust及其构建工具和Cargo依赖管理器将是您新项目的不错选择。


Cargo除了会为您下载依赖关系并管理其版本,构建和运行您的应用程序,运行测试并生成文档这一事实外,还可以使用其他有用功能的插件进行扩展。 例如,有一些扩展使Cargo可以确定项目的过时依赖项,对源代码执行静态分析,构建和重新部署Web应用程序的客户端部分,等等。


货物配置文件使用友好且最小的toml标记语言来描述项目设置。 这是典型的Cargo.toml配置Cargo.toml的示例:


 [package] name = "some_app" version = "0.1.0" authors = ["Your Name <you@example.com>"] [dependencies] regex = "1.0" chrono = "0.4" [dev-dependencies] rand = "*" 

以下是使用货运的三个典型命令:


 $ cargo check $ cargo test $ cargo run 

在他们的帮助下,将分别检查源代码中的编译错误,项目的汇编和测试的启动,程序的汇编和启动以执行。


3.内置测试


在Rust中编写单元测试是如此简单容易,您想一次又一次地进行。 :)通常,编写单元测试比尝试以其他方式测试功能要容易。 这是功能和测试示例:


 pub fn is_false(a: bool) -> bool { !a } pub fn add_two(a: i32) -> i32 { a + 2 } #[cfg(test)] mod test { use super::*; #[test] fn is_false_works() { assert!(is_false(false)); assert!(!is_false(true)); } #[test] fn add_two_works() { assert_eq!(1, add_two(-1)); assert_eq!(2, add_two(0)); assert_eq!(4, add_two(2)); } } 


test模块中标记为#[test]属性的功能是单元测试。 当调用cargo test命令时,它们将并行执行。 条件编译属性#[cfg(test)]标记整个模块)将导致以下事实:仅在执行测试时才编译模块,而不会进入常规程序集。


只需将test子模块添加到其中,将testtest功能放在同一模块中非常方便。 而且,如果您需要集成测试,只需将测试放在项目根目录下的tests目录中,然后在其中将您的应用程序用作外部软件包即可。 在这种情况下,不需要添加单独的test模块和条件编译指令。


作为测试执行的文档的特殊示例值得特别注意,但是下面将对此进行讨论。


还可以使用内置的性能测试(基准),但是它们还不稳定,因此仅在编译器夜间程序集中可用。 在稳定的Rust中,您将必须使用外部库进行此类测试。


4.良好的文档和当前示例


标准的Rust库有很多文档。 HTML文档是由源代码自动生成的,在码头注释中带有markdown描述。 此外,Rust代码中的文档注释包含在运行测试时执行的示例代码。 这样可以确保示例的相关性:


 /// Returns a byte slice of this `String`'s contents. /// /// The inverse of this method is [`from_utf8`]. /// /// [`from_utf8`]: #method.from_utf8 /// /// # Examples /// /// Basic usage: /// /// ``` /// let s = String::from("hello"); /// /// assert_eq!(&[104, 101, 108, 108, 111], s.as_bytes()); /// ``` #[inline] #[stable(feature = "rust1", since = "1.0.0")] pub fn as_bytes(&self) -> &[u8] { &self.vec } 

该文件


这是使用String类型的as_bytes方法的示例


 let s = String::from("hello"); assert_eq!(&[104, 101, 108, 108, 111], s.as_bytes()); 

将在测试启动期间作为测试执行。


此外,Rust库通常以位于项目根目录的examples目录中的小型独立程序的形式创建其使用示例的做法。 这些示例也是文档的重要组成部分,它们也在测试运行期间进行编译和执行,但是它们可以独立于测试运行。


5.智能自动推断类型


在Rust程序中,如果编译器能够根据使用上下文自动输出表达式,则不能显式指定表达式的类型。 这不仅适用于声明了变量的地方。 让我们看一个例子:


 let mut vec = Vec::new(); let text = "Message"; vec.push(text); 


如果我们安排类型注释,那么此示例将如下所示:


 let mut vec: Vec<&str> = Vec::new(); let text: &str = "Message"; vec.push(text); 

也就是说,我们有一个字符串切片的向量和一个字符串切片类型的变量。 但是在这种情况下,指定类型是完全多余的,因为编译器可以自行输出它们(使用Hindley-Milner算法的扩展版本)。 vec是向量的事实已经通过Vec::new()的返回值类型进行了明确说明,但尚不清楚其元素将是哪种类型。 text类型是字符串切片的事实可以通过为其分配文字类型的事实来理解。 因此,在vec.push(text) ,矢量元素的类型变得明显。 请注意, vec变量的类型完全取决于它在执行线程中的使用,而不是在初始化阶段。


这种类型推断系统消除了代码中的噪音,并使之与某些动态类型化编程语言中的代码一样简洁。 而这同时保持严格的静态类型!


当然,我们不能完全摆脱使用静态类型语言的键入。 程序必须具有确保知道对象类型的位置,以便可以在其他位置显示这些类型。 Rust中的这些要点是对用户定义的数据类型和函数签名的声明,在其中必须指定所使用的类型。 但是,您可以使用通用编程在其中输入“类型的元变量”。


6.变量声明点的模式匹配


let操作


 let p = Point::new(); 

并不仅限于声明新变量。 她实际上所做的是将等号右边的表达式与左侧的模式匹配。 并且可以将新变量作为示例的一部分引入(并且仅如此)。 看下面的示例,它对您来说将变得更加清晰:


 let Point { x, y } = Point::new(); 


在这里执行解构:这样的比较将引入变量xy ,它们将使用Point结构的对象的xy字段的值初始化,该值通过调用Point::new() 。 同时,由于右侧表达式的类型与左侧Point类型的Point模式相对应,因此比较是正确的。 以类似的方式,您可以采用例如数组的前两个元素:


 let [a, b, _] = [1, 2, 3]; 

还有更多要做的事情。 最值得注意的是,在可以在Rust中输入新变量名称的所有位置执行这样的比较,即:在matchletif letwhile let if let ,在for循环的头文件,函数和闭包的参数中。 这是在for循环中优雅地使用模式匹配的示例:


 for (i, ch) in "foo".chars().enumerate() { println!("Index: {}, char: {}", i, ch); } 


在迭代器上调用的enumerate方法构造了一个新的迭代器,该迭代器将不迭代初始值,而是遍历元组,将“序数索引,初始值”配对。 这些循环中的每个元组都将映射到指定的模式(i, ch) ,其结果是变量i将接收来自元组的第一个值-索引,变量ch接收第二个值,即第二个,即字符串的字符。 在循环主体中,我们可以使用这些变量。


for循环中使用模式的另一个流行示例:


 for _ in 0..5 { //   5  } 

在这里,我们仅使用_模式忽略迭代器的值。 因为我们不在循环主体中使用迭代编号。 可以使用例如函数参数来完成此操作:


 fn foo(a: i32, _: bool) { //      } 

或在match语句中进行match


 match p { Point { x: 1, .. } => println!("Point with x == 1 detected"), Point { y: 2, .. } => println!("Point with x != 1 and y == 2 detected"), _ => (), //        } 


模式匹配使代码非常紧凑和富有表现力,并且在match语句中通常是不可替代的。 match运算符是完整变量分析的运算符,因此您将不会偶然忘记检查其中的已分析表达式的某些可能匹配项。


7.语法扩展和自定义DSL


Rust语法受到限制,这在很大程度上是由于该语言所使用的类型系统的复杂性。 例如,Rust没有命名函数参数或具有可变数量参数的函数。 但是您可以使用宏来克服这些限制和其他限制。 Rust有两种宏:声明性和过程性。 使用声明性宏,您将永远不会遇到与C中的宏相同的问题,因为它们是卫生的,在文本替换级别不起作用,而在抽象语法树中的替换级别不起作用。 宏允许您在语言语法级别创建抽象。 例如:


 println!("Hello, {name}! Do you know about {}?", 42, name = "User"); 

除了此宏扩展了调用打印格式化字符串的“函数”的语法功能外,它还将在其实现中在编译时而不是在运行时验证输入参数是否与指定的格式字符串匹配。 使用宏,您可以为自己的设计需求输入简洁的语法,创建和使用DSL。 这是在Wasm中编译的Rust程序中使用JavaScript代码的示例:


 let name = "Bob"; let result = js! { var msg = "Hello from JS, " + @{name} + "!"; console.log(msg); alert(msg); return 2 + 2; }; println!("2 + 2 = {:?}", result); 

js! 定义在stdweb软件包中,它允许您将完整的JavaScript代码嵌入程序中(单引号字符串和未使用分号完成的运算符除外),并使用语法@{expr}使用Rust代码中的对象。


宏为将Rust程序的语法适应特定主题领域的特定任务提供了巨大的机会。 在开发复杂的应用程序时,它们将节省您的时间和精力。 不是通过增加运行时开销,而是通过增加编译时间。 :)


8.自动生成相关代码


Rust的过程派生宏被广泛用于自动实现特征和其他代码生成。 这是一个例子:


 #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] struct Point { x: i32, y: i32, } 

由于标准库中的所有这些类型( CopyCloneDebugDefaultPartialEqEq )都是针对i32结构的字段类型实现的,因此它们的实现可以在整个结构中自动显示。 另一个例子:


 extern crate serde_derive; extern crate serde_json; use serde_derive::{Serialize, Deserialize}; #[derive(Serialize, Deserialize)] struct Point { x: i32, y: i32, } let point = Point { x: 1, y: 2 }; //  Point  JSON . let serialized = serde_json::to_string(&point).unwrap(); assert_eq!("{\"x\":1,\"y\":2}", serialized); //  JSON   Point. let deserialized: Point = serde_json::from_str(&serialized).unwrap(); 


在这里,使用来自serde库的DeserializeDeserialize用于Point结构,可以自动生成其序列化和反序列化的方法。 然后,您可以将此结构的实例传递给各种序列化函数,例如,将其转换为JSON字符串。


您可以创建自己的程序宏,以生成所需的代码。 或者使用其他开发人员已经创建的许多宏。 除了使程序员免于编写样板代码之外,宏还具有不需要将代码的不同部分保持一致状态的优点。 假设,如果将第三个字段z添加到Point结构中,则如果使用了derive,则可以正确地将其用于序列化,无需执行任何其他操作。 如果我们自己将实现Point序列化的必要特征,那么我们将必须确保该实现始终与Point结构的最新更改保持一致。


9.代数数据类型


简而言之,代数数据类型是复合数据类型,是结构的并集。 更正式地说,它是产品类型的类型总和。 在Rust中,使用enum关键字定义此类型:


 enum Message { Quit, ChangeColor(i32, i32, i32), Move { x: i32, y: i32 }, Write(String), } 

Message类型的变量的特定值的类型只能是Message中列出的结构类型之一。 这要么是类似于单元的Quit无边界结构,要么是带有无名字段的ChangeColorWrite tuple结构之一,要么是通常的Move结构。 传统的枚举类型可以表示为代数数据类型的特例:


 enum Color { Red, Green, Blue, White, Black, Unknown, } 

使用模式匹配可以找出在特定情况下哪种类型实际上具有价值:


 let color: Color = get_color(); let text = match color { Color::Red => "Red", Color::Green => "Green", Color::Blue => "Blue", _ => "Other color", }; println!("{}", text); ... fn process_message(msg: Message) { match msg { Message::Quit => quit(), Message::ChangeColor(r, g, b) => change_color(r, g, b), Message::Move { x, y } => move_cursor(x, y), Message::Write(s) => println!("{}", s), }; } 


以代数数据类型的形式,Rust实现了诸如OptionResult这样的重要类型,它们分别用于表示缺失值和正确/错误的结果。 在标准库中定义Option方法如下:


 pub enum Option<T> { None, Some(T), } 

Rust没有空值,就像意外调用它的烦人的错误一样。 相反,在确实需要指示可能缺少值的地方,使用Option


 fn divide(numerator: f64, denominator: f64) -> Option<f64> { if denominator == 0.0 { None } else { Some(numerator / denominator) } } let result = divide(2.0, 3.0); match result { Some(x) => println!("Result: {}", x), None => println!("Cannot divide by 0"), } 


代数数据类型是功能强大的表达工具,为类型驱动开发打开了大门。 在该范例中,由合格的程序编写的程序会将其工作正确性的大部分检查分配给类型系统。 因此,如果您在日常工业编程中缺少Haskell,Rust可以作为您的出路。 :)


10.易于重构


Rust开发的严格的静态类型系统以及在编译过程中尝试执行尽可能多的检查的事实导致修改和重构代码变得非常简单和安全。 如果在更改之后对程序进行了编译,则意味着仅逻辑错误保留在其中,与验证已分配给编译器的功能无关。 结合将单元测试添加到测试逻辑的简便性,可以极大地保证程序的可靠性,并提高程序员对更改后代码正确操作的信心。




也许这就是我在本文中想要谈论的全部。 当然,Rust具有许多其他优点,以及许多缺点(语言有些笨拙,缺少熟悉的编程习惯和“非文学”语法),在此未提及。 如果您有话要说,请在评论中写。 通常,请在实践中尝试Rust。 就像我的情况一样,也许他对您的好处会胜过他的所有缺点。 最后,您将获得长时间所需的确切工具集。

Source: https://habr.com/ru/post/zh-CN430294/


All Articles