跳到主要内容
预计阅读 22 分钟

Rust基础语法 —— 像搭积木一样学编程

语法是编程语言的”字母表”。在你能写出优美的Rust程序之前,得先认识这些基础的”积木块”。好消息是,如果你有其他编程语言经验,很多概念你已经见过了——只是Rust给它们加了一些独特的”规矩”。

📋 开篇自测:你已经知道多少?

  1. Rust中变量默认是不可变的,你知道为什么这样设计吗?
  2. i32u64f64——你能猜出这些类型名称的含义吗?
  3. Rust的if表达式能返回值,你知道这意味着什么吗?

一、变量与可变性——Rust的”保守主义”

1.1 用let声明变量

在Rust中,声明变量用let关键字:

fn main() {
    let city = "杭州";
    let temperature = 28;
    println!("{}今天气温{}度", city, temperature);
}

到这里一切都很正常。但如果你尝试修改变量的值:

fn main() {
    let score = 85;
    score = 90;  // 编译错误!
    println!("分数是:{}", score);
}

编译器会毫不留情地报错:cannot assign twice to immutable variable

这就是Rust的第一条”规矩”:变量默认是不可变的(immutable)

1.2 为什么默认不可变?

想象你在图书馆借了一本书。如果每个借阅者都能随意修改书的内容,等你还回来的时候,谁也不知道书里写的还是不是原来的东西。Rust的设计哲学是:数据默认是”只读”的,只有你明确表示”我需要修改它”时,才允许修改。

这不是给你添麻烦,而是帮你减少bug。在大型项目中,一个变量在你不知道的地方被偷偷改掉了——这是最难排查的bug之一。

1.3 用mut让变量可变

当你确实需要修改变量时,加上mut关键字:

fn main() {
    let mut score = 85;
    println!("期中成绩:{}", score);
    score = 92;
    println!("期末成绩:{}", score);
}

mut是mutable(可变的)的缩写。它像是一个明确的声明:“我知道这个变量以后会被改,这是有意为之的。“

1.4 变量遮蔽(Shadowing)

Rust有一个有趣的特性叫变量遮蔽——你可以用同名的新变量”盖住”旧变量:

fn main() {
    let x = 5;
    let x = x + 1;    // 新的x遮蔽了旧的x
    let x = x * 2;    // 再次遮蔽
    println!("x的值是:{}", x);  // 输出12
}

遮蔽和mut有本质区别:遮蔽是创建了一个全新的变量(甚至可以改变类型),而mut是修改同一个变量的值。

fn main() {
    let input = "42";         // 字符串类型
    let input: i32 = input.parse().unwrap();  // 遮蔽:变成了整数类型!
    println!("数值是:{}", input);
}

1.5 常量

如果一个值从头到尾都不会变,而且你想在编译期就确定它,可以用const

const MAX_PLAYERS: u32 = 100;
const PI: f64 = 3.14159265358979;

常量必须标注类型,名字用全大写加下划线的风格,而且值必须是编译期就能确定的——你不能把一个函数调用的结果赋给常量。

🤔 想一想 在你之前用的编程语言中,变量默认是可变还是不可变的?如果默认不可变,会对你的编程习惯产生什么影响?


二、数据类型——Rust是个”类型控”

Rust是静态类型语言——每个变量在编译时都有确定的类型。不过大多数时候,编译器能自动推断类型,你不需要手动标注。

2.1 整数类型

Rust的整数类型名称非常直观:

类型大小范围说明
i88位-128 ~ 127有符号(i=integer)
u88位0 ~ 255无符号(u=unsigned)
i1616位-32768 ~ 32767
u1616位0 ~ 65535
i3232位约±21亿默认整数类型
u3232位0 ~ 约42亿
i6464位约±9.2×10¹⁸
u6464位0 ~ 约1.8×10¹⁹
i128128位约±1.7×10³⁸
u128128位0 ~ 约3.4×10³⁸
isize平台相关和CPU位数一致
usize平台相关常用于索引和长度

命名规则:i代表有符号整数,u代表无符号整数,后面的数字代表占多少位。简单粗暴,一目了然。

fn main() {
    let age: u8 = 25;           // 年龄不会是负数,用u8足够
    let balance: i64 = -50000;  // 余额可能是负数
    let count = 42;             // 编译器自动推断为i32

    // 数字可以用下划线分隔,提高可读性
    let population: u64 = 1_400_000_000;
    println!("人口:{}", population);
}

2.2 浮点数类型

Rust有两种浮点数:f32(单精度)和f64(双精度,默认)。

fn main() {
    let pi = 3.14159;        // 默认f64
    let temp: f32 = 36.5;    // 明确指定f32
    println!("圆周率:{},体温:{}", pi, temp);
}

2.3 布尔类型

fn main() {
    let is_rust_fun: bool = true;
    let is_easy = false;  // 类型自动推断为bool
    println!("Rust好玩吗?{}", is_rust_fun);
}

2.4 字符类型

Rust的char类型使用单引号,而且它支持Unicode——意味着你可以存储中文字符甚至emoji:

fn main() {
    let letter = 'A';
    let chinese = '龙';
    let emoji = '🦀';   // Rust的吉祥物是螃蟹!
    println!("{} {} {}", letter, chinese, emoji);
}

注意:char占4个字节(32位),因为它需要能表示任何Unicode字符。

2.5 元组(Tuple)

元组可以把多个不同类型的值打包在一起:

fn main() {
    let person: (&str, u8, f64) = ("小明", 28, 175.5);

    // 用点号加索引访问
    println!("姓名:{},年龄:{},身高:{}", person.0, person.1, person.2);

    // 解构(destructuring)
    let (name, age, height) = person;
    println!("{}今年{}岁,身高{}cm", name, age, height);
}

元组就像一个”临时打包袋”——当你需要把几个相关的值捆在一起传来传去时,它很方便。但如果这组数据有固定的结构和含义,用结构体(struct)会更好——这个我们后面会讲。

2.6 数组(Array)

Rust的数组是固定长度的,所有元素类型必须相同:

fn main() {
    let weekdays = ["周一", "周二", "周三", "周四", "周五"];
    let scores: [i32; 5] = [88, 92, 76, 95, 83];

    println!("第一天:{}", weekdays[0]);
    println!("第三科成绩:{}", scores[2]);

    // 创建一个包含5个0的数组
    let zeros = [0; 5];  // [0, 0, 0, 0, 0]
    println!("零数组:{:?}", zeros);
}

如果你需要可变长度的数组,用Vec(向量),它是Rust中最常用的集合类型之一:

fn main() {
    let mut fruits = vec!["苹果", "香蕉", "橙子"];
    fruits.push("葡萄");
    println!("水果篮:{:?}", fruits);
}

🤔 想一想 为什么Rust要区分i32u32f32f64这么多类型?在Python中一个int就搞定了。想想在嵌入式设备或高性能计算场景下,精确控制数据大小有什么好处?


三、函数——代码的”工具箱”

3.1 定义和调用函数

fn greet(name: &str) {
    println!("你好,{}!欢迎学习Rust!", name);
}

fn add(a: i32, b: i32) -> i32 {
    a + b  // 注意:没有分号!这是返回值表达式
}

fn main() {
    greet("小红");
    let sum = add(3, 7);
    println!("3 + 7 = {}", sum);
}

几个要点:

  • 函数参数必须标注类型——Rust不会帮你猜参数类型
  • 返回类型用->箭头标注
  • 函数体最后一个表达式(没有分号)就是返回值
  • 如果加了分号,那就变成了语句,返回的是()(空元组,类似其他语言的void)

3.2 表达式 vs 语句

这是Rust中一个微妙但重要的概念:

  • 语句(statement):执行某个动作,不返回值。以分号结尾。
  • 表达式(expression):计算并产生一个值。不以分号结尾。
fn main() {
    // 这是语句
    let x = 5;

    // 花括号内部也可以是表达式
    let y = {
        let inner = 10;
        inner * 2   // 没有分号 → 这个值(20)就是整个代码块的值
    };

    println!("y = {}", y);  // y = 20
}

这就像做数学题:3 + 5是一个表达式,它”产出”了8这个值。而”把8写在纸上”是一个动作(语句)。

3.3 提前返回

虽然”最后一个表达式作为返回值”是惯用写法,但你也可以用return关键字提前返回:

fn check_age(age: u32) -> &'static str {
    if age < 18 {
        return "未成年";
    }
    "成年"  // 最后一个表达式,等同于 return "成年";
}

四、控制流——让程序学会”做决定”

4.1 if表达式

fn main() {
    let score = 85;

    if score >= 90 {
        println!("优秀!");
    } else if score >= 70 {
        println!("良好!");
    } else if score >= 60 {
        println!("及格");
    } else {
        println!("不及格,继续努力!");
    }
}

Rust的if有一个超能力——它是表达式,可以直接返回值:

fn main() {
    let age = 20;
    let category = if age >= 18 { "成年人" } else { "未成年人" };
    println!("你是{}", category);
}

这比三元运算符(condition ? a : b)更加清晰。注意:当if用作表达式时,两个分支的返回类型必须一致

4.2 循环——loop、while、for

Rust提供了三种循环,各有用武之地:

loop:无限循环

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;
        if counter == 10 {
            break counter * 2;  // break可以带返回值!
        }
    };

    println!("结果:{}", result);  // 输出20
}

loopwhile true的区别在于:loop能通过break返回值,编译器也能更好地推断类型。

while:条件循环

fn main() {
    let mut fuel = 100;

    while fuel > 0 {
        println!("剩余燃料:{}", fuel);
        fuel -= 25;
    }
    println!("燃料耗尽!");
}

for:遍历循环(最常用!)

fn main() {
    // 遍历数组
    let cities = ["北京", "上海", "广州", "深圳"];
    for city in cities {
        println!("欢迎来到{}", city);
    }

    // 遍历范围
    for i in 1..=5 {    // 1到5,包含5
        println!("第{}轮", i);
    }

    for i in 0..5 {     // 0到4,不包含5
        println!("索引:{}", i);
    }

    // 带索引的遍历
    let scores = [88, 92, 76, 95];
    for (index, score) in scores.iter().enumerate() {
        println!("第{}科成绩:{}", index + 1, score);
    }
}

for是Rust中最常用也最安全的循环——它不会出现数组越界的问题。

4.3 match——Rust的”超级switch”

match是Rust中最强大的控制流工具之一。它就像一个精密的分拣机器——把一个值和一系列模式进行匹配:

fn main() {
    let grade = 'B';

    let description = match grade {
        'A' => "太棒了!顶尖水平!",
        'B' => "不错!继续保持!",
        'C' => "还行,有提升空间",
        'D' => "危险了,加把劲",
        'F' => "挂科了...",
        _ => "无效的等级",    // _ 是通配符,匹配所有其他情况
    };

    println!("{}", description);
}

match的一个硬性要求:必须穷尽所有可能性。如果你漏了某种情况,编译器不会放过你。这就像一个严格的收件员——每一封信都必须有去处,不允许有”不知道该放哪”的情况。

我们在后面讲枚举和模式匹配时,会看到match真正大显身手的场景。

⚠️ 常见误区

  1. 忘了mut就尝试修改变量——这是新手最高频的编译错误。记住:Rust默认不可变,需要修改就加mut
  2. 搞混表达式和语句——函数最后一行如果加了分号,返回值就变成了(),不是你期望的值。这个分号经常导致新手困惑。
  3. 整数溢出与除零——在debug模式下,整数溢出会导致panic(程序崩溃)。比如let x: u8 = 255; let y = x + 1;就会出错。但注意:在release模式下(cargo build --release),Rust会进行二进制补码回绕(类似C),溢出不会panic而是静默回绕。如果需要在所有模式下都控制溢出行为,可以使用checked_addwrapping_addsaturating_add等方法。另外注意整数除以零在任何模式下都会panic,而浮点数除以零不会panic——1.0 / 0.0会得到Infinity0.0 / 0.0会得到NaN
  4. if当语句用却忘了else分支——当if用作表达式赋值时,必须有else分支,否则编译器不知道条件不满足时该返回什么。

📝 掌握度自测

  1. 变量let x = 5;let mut x = 5;有什么区别?变量遮蔽和mut又有什么不同?
  2. 类型:说出至少5种Rust的基本数据类型,并解释usize通常用在什么场景。
  3. 函数:写一个函数is_even(n: i32) -> bool,判断一个整数是否为偶数。注意函数体的最后一行不要加分号。
  4. 控制流:用for循环打印1到100中所有能被3整除的数。
  5. 综合:写一个函数,接收一个温度(摄氏度,f64类型),返回对应的华氏温度。公式:F = C * 9/5 + 32。在main函数中调用它,打印”25°C等于77°F”。

💡 自我评估

  • 答对5题:基础语法已经掌握,准备好迎接Rust最核心的概念——所有权!
  • 答对3-4题:再练习一下不熟悉的部分,特别注意表达式和语句的区别。
  • 答对0-2题:不要着急,语法需要反复练习。建议打开编辑器,把每个代码示例都敲一遍运行一下。

购买课程解锁全部内容

内存安全 + 零成本抽象:Rust 系统编程实战

¥29.90