<-- Home |--rust |--rfe

RFE 005: 工程师(粗)Rust入门之值编程能走多远?

Rust极限条件能走多远?

在前面的文章中,我们介绍了Rust的变量使用方式,强调了变量名的重复使用和不可变性。这种方式实际上体现了一种编程范式,称为值编程范式(Value Programming Paradigm)。在这种范式下,程序主要关注值的计算和传递,而不是变量的状态变化。

那么,以Rust目前的语法、特性和标准库,不涉及可变变量,不主动使用堆内存分配(这个还需要考虑所有权和借用的问题),我们究竟能走多远?如果用非标准库的话,实际上可以想象得到,可以很容易满足工程师的各种需求。但是,使用这些特性和库,实际上也会给工程师带来额外的认知负担和复杂性。

我们在哪里

首先,还是可以回顾一下已经掌握的内容:

  • 变量的定义和使用:使用let关键字定义不可变变量,变量名可以重复使用。
  • 基本数据类型:整数、浮点数。
  • 基本的算术运算。
  • 条件表达式:使用if语句进行条件判断。
  • 模式匹配:使用match语句进行模式匹配。
  • 数组:存储一组同类型的值。
  • 循环:for循环用于遍历数组等集合,或者for i in 0..n用于重复执行代码块。
  • 函数调用:通过传递值作为参数,获取返回值,支持递归调用。
  • 元组:将多个不同类型的值组合在一起。
  • 输出操作:使用println!宏进行输出。

稍微复杂一点的操作,比如字符串处理、日期时间计算、文件操作等,目前还没有涉及。

在进行这些之前,我们还有几个重要的工具可以掌握一下。

更多的工具

结构体(Struct)

结构体是一种自定义数据类型,可以将多个相关的值组合在一起。通过结构体,我们可以更好地组织和管理数据。默认的结构体字段是不可变的,这符合我们的值编程范式。

1struct Point {
2    x: f64,
3    y: f64,
4}
5let p = Point { x: 1.0, y: 2.0 };
6println!("Point: ({}, {})", p.x, p.y);

从工程师的角度来看,结构体允许我们将相关的数据封装在一起,形成一个新的值。这使得程序更具可读性和可维护性。这就像我们在工程设计中,将相关的参数和属性封装在一个组件或者模块中一样,便于理解和管理。

从已经有的结构体值出发,构造一个新的结构体值,也是非常自然的操作:

1let p1 = Point { x: 1.0, y: 2.0 };
2let p2 = Point { x: 3.0, y: 4.0 };
3let p3 = Point { x: p1.x + p2.x, y: p1.y + p2.y };
4let p4 = Point { x: 4.0, ..p1 }; // 使用结构体更新语法
5println!("Point 3: ({}, {})", p3.x, p3.y);

当我们的结构体的内部都是不变数据时,结构体本身也是不可变的值。这使得我们可以放心地传递和使用结构体,而不必担心其状态会被修改。也就是说,结构体的Copy trait约束,只要其所有字段都实现了Copy trait,可以非常简单的定义结构体本身的Copy trait。

1#[derive(Copy, Clone)]
2struct Point {
3    x: f64,
4    y: f64,
5}

从ADT的角度,结构体可以看作是一个乘积类型(Product Type),表示多个值的组合。这使得我们可以更好地表达和处理复杂的数据结构。

实际上,还有一个中间态,叫做元组结构体(Tuple Struct),它介于元组和结构体之间。元组结构体允许我们定义一个具有命名类型但没有命名字段的结构体。

1struct Color(u8, u8, u8); // RGB颜色
2let red = Color(255, 0, 0);
3println!("Red Color: ({}, {}, {})", red.0, red.1, red.2);

当然,命名的字段结构体更符合我们工程师的习惯,并且更易读。

枚举

实际上,enum(枚举类型)也是一个非常重要的工具,可以帮助我们处理有限状态的问题。枚举类型允许我们定义一组命名的常量,这些常量可以表示不同的状态或者选项。

枚举,从ADT的角度来看,是一个和结构体相对的概念,表示多个可能值中的一个。因此,枚举可以看作是一个和类型(Sum Type)。

1enum Shape {
2    Circle { radius: f64 },
3    Rectangle { width: f64, height: f64 },
4}
5let shape1 = Shape::Circle { radius: 5.0 };
6let shape2 = Shape::Rectangle { width: 4.0, height: 6.0 };

通过模式匹配,我们可以非常方便地处理不同的枚举变体:

1match shape1 {
2    Shape::Circle { radius } => println!("Circle with radius: {}", radius),
3    Shape::Rectangle { width, height } => println!("Rectangle with width: {} and height: {}", width, height),
4}

ADT

有了结构体和枚举,我们实际上就掌握了构建代数数据类型(Algebraic Data Types, ADT)的基本工具。ADT允许我们通过组合基本类型来构建复杂的数据结构。当然,ADT的概念对于我们工程师来说,可能有点抽象。但是,从实际应用的角度来看,结构体和枚举已经足够我们处理大部分的工程问题了。

文件的处理

std::fs模块

实际上,Rust有一个非常强大的serde生态系统,可以帮助我们进行数据的序列化和反序列化操作。通过这些工具,我们可以非常方便地将数据保存到文件中,或者从文件中读取数据。

但是,目前我们考虑尽量不适用外部库,所以我们可以使用标准库中的std::fs模块来进行文件操作。

 1use std::fs::File;
 2use std::io::{self, Read, Write};
 3
 4fn main() -> io::Result<()> {
 5
 6    // 读取文件
 7    let content = match std::fs::read_to_string("input.txt") {
 8        Ok(c) => c,
 9        Err(e) => {
10            println!("Failed to read file: {}", e);
11            String::new()
12        }
13    };
14
15
16    // 写入文件
17    std::fs::write("output.txt", content)?;
18
19    Ok(())
20}

虽然这样有点烦人,但是至少我们可以进行基本的文件读写操作。不使用mut变量,我们可以通过重新赋值的方式来处理文件内容。

string

说真的,字符串处理在哪里都是一个麻烦事儿。Rust的字符串类型String和字符串str,实际上是比较复杂的类型,涉及到堆内存分配和所有权的问题。单独这个就可以写一个系列文章了。

不过,我们可以通过一些简单的操作来处理字符串,而不涉及复杂的内存管理。

1    let s1 = String::from("Hello, ");
2    let s2 = String::from("world!");
3    let s3 = s1.chars().chain(s2.chars()).collect::<String>();
4    println!("{}", s3);
5    s3.split_whitespace().for_each(|word| println!("{}", word));

实际上,真的讨厌字符串……

总结

通过前面的介绍,我们已经掌握了Rust中值编程范式的基本工具,包括结构体、枚举、文件操作和字符串处理。虽然这些工具在某些方面可能不如使用可变变量和堆内存分配那样方便,但是它们提供了一种更加安全和可预测的编程方式(希望吧)。

可能只是没苦硬吃,实在太难的地方,我会快速抛弃目前的探索……


文章标签

|-->rust |-->engineering |-->tutorial |-->value |-->programming paradigm


GitHub