Java程序员眼中的Rust系列 — 1.初见

0-前言

我从未想过自己要写一个Rust入门系列的文章,因为知道要把这么复杂的一门语言想办法教会给初学者是多么困难,我实在没那个毅力。然而在我日常的编码工作中(主要是使用Java),总是会冒出一系列的念头:“这个bug如果换做用Rust实现,就不会出现”或者“这部分代码如果用Rust实现可读性会更好”,当然也存在“这种功能如果用Rust写我会吐血”的时刻。可以说掌握一定Rust经验后,返回头来,我对Java的理解更深刻了,获得了更多的灵感。最重要的是,这种对比很好玩。

我想从一个Javaer的角度出发,带大家看看最近几年很火的Rust是一门什么样的语言——实际上不止于Rust,当今新生代的编程语言,具有愈来愈多的类似特性,这些特性在Java上看不到,但Rust有。熟悉了Rust,以后去学其他语言也会很快。

Rust的学习曲线陡峭是出了名的,如果费了很多精力学而不用则毫无意义。所以我的终极目标是“好玩”,“激发灵感”,而不是“掌握全貌”,我甚至不求读者要自己敲一遍代码。对于一些有难度又无法避开的问题,我尽量做到内容简洁易懂。

如果真的在学习过程中产生了兴趣,想要深入学习,我强烈推荐看一下这个免费教程《Rust语言圣经》关于本书 - Rust语言圣经(Rust Course)

当然,我并不拒绝深入讨论Rust相关问题,欢迎留言。

1-依旧从Hello, World开始

为了不吓跑读者,我们暂时不去谈如何安装一个完整的运行环境。Rust官方提供了一个网页可以执行简单的Rust代码——Rust Playground : Rust Playground。在不知道如何在本地跑一个Rust程序前,可以先把文中的代码贴到Playground中。

fn main(){

    println!("hello, world");

}

点击左上角的RUN按钮,可以看到执行结果:

这就是最简单的一个HelloWorld程序,我们逐行来说明。

首先, fn main()即是定一个main函数,等价于Java的:public static void main(String[] args)。fn是Rust中的关键字,用于定义函数或方法。main函数不可以有入参。

我相信打印出"hello, world"这行你能大致上明白,不过等等println!是什么?这是一个内置的函数么?结尾的叹号是什么鬼?

实际上这不是一个函数,而是Rust中的宏(macro)。Java或Golang程序员可能对宏的概念比较陌生,但C/C++程序员都应该很熟悉了。宏在Rust是一种元编程的工具,用来提高可读性,减少重复。详细内容我们未来再说。

所以println!其实是Rust内置的用于打印文字到标准输出的宏。为什么Rust要将打印输出这一编程语言中最常用的功能设计成宏而不是函数呢?看下面这个例子:

fn main() {

    println!("my name is {}, number is {} ", "Tom", 5);

}

如你所想,打印出的结果是:"my name is Tom, number is 5"。println!宏支持以这种方式拼接字符串,像极了Java中一些日志框架进行输出的写法:

logger.info("my name is {}, number is {} ", "Tom", 5);

方便且易读,新时代语言的标配。在java中,这种效果的实现依赖于“可变参数方法”这一语法。但Rust本身的语法并不支持可变参数,所以要靠宏来曲线救国。简单说,Rust中的宏会在编译阶段被替换成另外一个面目全非的样子。对应到println!宏,就是在编辑阶段产生一段代码,内容是把所有参数组成一个参数,然后调用某个底层标准输出的函数。再次强调,这个过程与运行时无关。

最后,Rust“绝大多数”语句都是要以分号结尾的,未来你会看到例外的情况。

2-声明一个变量

在Rust中,“声明”一个变量,使用let关键字:

let x = 1;

我们并没有指定x的类型,但它能工作,是因为Rust支持自动类型推导。1在此会被编译器认定为一个i32类型的字面量,当然也可以显示写明类型:

let x: i32 = 1;

与Java不同,Rust的世界,类型是“后置”的。

结合上一节,你可以在打印的参数中加入变量:

fn main() {
    let name = "Tome";
    let number = 5;
    println!("my name is {}, number is {} ", name, number);
}

甚至可以这样写:

fn main() {
    let name = "Tome";
    let number = 5;
    println!("my name is {name}, number is {number}");
}

哈哈,有python内味儿了。

实际上let x = 1这种语法并不等于Java中的变量声明,严格的定义应该称为“绑定”(binding)。了解一些js,ts,scala等语言的同学会比较熟悉,这是函数式语言中常见的概念。至于到底“声明”与“绑定”有什么区别,还是要等到未来有机会再说。

2-数值类型

正如Java一样,Rust也针对不同长度的数值设置了对应的类型,区别是每种类型多一个无符号的版本:

// 整数类型
let a: i8 = 1_i8; // Java: byte
let b: u8 = 1_u8;
let c: i16 = 1_i16; // Java: short
let d: u16 = 1_u16;
let e: i32 = 1_i32; // Java: int
let f: u32 = 1_u32;
let g: i64 = 1_i64; // Java: long
let h: u64 = 1_u64;
let i: i128 = 1_i128;
let j: u128 = 1_u128;
let k: isize = 1_isize;
let l: usize = 1_usize;

// 浮点类型
let m: f32 = 1.0_f32; // Java: float
let n: f64 = 1.0_f64; // Java: double

容易看懂,i8代表8位有符号,u8代表8位无符号。isize和usize占用大小和运行的平台相关,64位环境占用64位。

在Java中,可以直接将一个表示范围更小的值赋值给范围更大的变量,反之则不行:

int a = 1;
long b = a;
long c = 1L;
int d = ((int) c);

但Rust就会更严格一些,即使是从小向大的转换,也需要通过as关键字: 

let a: i32 = 1;
let b: i64 = a as i64;
let c: i64 = 1;
let d: i32 = c as i32;

3-编译告警

如果你把上一节的代码直接拿来运行,而没有添加其他内容,则编译时应该出现以下信息:

这里的意思是,b和d两个变量并没有被使用。Rust的编译器针会对未使用的变量,方法,导入等进行告警。近些年新兴的编程语言越来越重视检查“用不上的”内容,以提高可读性维护性。比如golang,对于未使用的变量和导入都会在编译阶段报错。

Rust更温和一些,仅仅是告警,但告警多了也很烦人。这个问题编译器给出的方案是,将bd定义改为_b,_d。其实你也可以直接写一个下划线:

告警一样会消失。这种用法接近python和golang。

下划线在Rust中有各种特殊的作用,未来还能看到,不过都可以概括为一个意思:“I don't care”。

4-修改一个变量

直到现在,Rust看起来还和Java没本质的区别,别忙,第一个让Javaer别扭的特性要来了:

fn main() {
    let x = 1;
    println!("{x}");
    x = 2;
    println!("{x}");
}

我要将x的值修改为2,看起来很正常是不是?运行一下,编译报错了:

Compiling playground v0.0.1 (/playground)
error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:4:5
  |
2 |     let x = 1;
  |         -
  |         |
  |         first assignment to `x`
  |         help: consider making this binding mutable: `mut x`
3 |     println!("{x}");
4 |     x = 2;
  |     ^^^^^ cannot assign twice to immutable variable


For more information about this error, try `rustc --explain E0384`.
error: could not compile `playground` (bin "playground") due to 1 previous error

顺带一提,Rust的编译器是非常智能和体贴的,每种类型的错误都有一个编号,比如这里的E0384,你可以在错误码页面Rust error codes index - Error codes index找到对应的解释。除了错误原因,很多时候编译器还会给你建议,看这句话:“help: consider making this binding mutable: `mut x`”,编译器建议我们把x变量前面加上mut。试一试:

fn main() {
    let mut x = 1;
    println!("{x}");
    x = 2;
    println!("{x}");
}

可以了,为什么?

在Rust中,一个变量默认是不可修改其值的,如果要可变,必须加上mut关键字,包括我们以后会讲到的函数也是如此。这种对可变性做限制的理念,源自于纯函数式语言,又被诸多混合式的语言借鉴,我想Rust如此设计大抵有两个原因:

  1. 提高可读性,避免难排查的bug

做过复杂业务系统的码农,一定有过类似这样的经历:你有一个实体对象,会作为一些操作的核心参数,在不同的方法之间传递,这些方法是不同的人在不同的时间“堆积”出来的,每个方法里的代码背后都代表着一个不可理解的用户需求,你不懂,也不想懂。在调用方法传入参数的那一刻,你只是根据方法签名的含义(甚至还有注释),想当然的认为,这个参数在执行周期内不会发生改变,然而你错了。

public class Main {
    static class Foo{
        // 忽略一些字段
    }

    static void function_modify_foo(Foo foo){
        // 调用其他方法,最终会修改foo的内容
    }

    static void function_use_foo(Foo foo) {
        // 使用foo的内容
    }

    public static void main(String[] args){
        final Foo foo = new Foo();
        function_modify_foo(foo);
        // 你觉得foo不会改变
        function_use_foo(foo);
    }
}

解决这类问题的办法有两类,要么你把所有涉及到代码读一遍,要么你一层一层debug调试。这还仅仅是一些应用层的业务代码,你都不敢想象在C/C++开发的领域内,要如何查找问题根源。

Rust中的默认不可变机制并不能杜绝数据修改,mut关键字的作用更多的是让你清楚正在做什么,清楚某个变量或者参数是可能被修改的,一旦发生问题,也能在一定程度上缩小排查范围。

至于上面举例的这种典型问题,得益于Rust的所有权,引用,Clone一系列机制,会被扼杀在摇篮中。

    2.  便于编译器优化

这部分内容略有超纲,不深入讲解了,有兴趣的同学可以自行查找资料。我只简单的讲解一下。

编译器的工作并不只是将程序员写的代码原封不动的翻译成目标代码,而是会进行“一定程度”的优化,可以说比起词法语法解析,这些优化才是一个编译器工作的难点。当编译器知道一个变量或者参数,在其生命周期中不会改变时,可以做非常多的事情,比如内联,消除多余代码,直接分配到寄存器中,调整执行顺序,并行计算等等。

总结

这是系列第一篇,希望你看到mut关键字的时候还没有“心生厌恶”。下一篇将介绍函数,结构体,流程控制这些内容,让我们看一看哪些语法特点能让你眼前一亮。同样,我努力做去到侧重于给大家一个直观的感受,而规避复杂烧脑的部分,至少是暂时规避。

  • 9
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值