综上所述,笔者以《面向C++程序员的Rust教程》进行开题,作为本身就是C++程序员兼Rust学习者的身份,相信能给读者带来切身体会和不一样的理解感悟。
从Hello World说起
按照世界级惯例,程序语言教程都由一个Hello World开始。一来我们可以以最少的篇幅来编写一段可运行的程序,让我们建立对这门语言的主观印象;二来也可以从这个最简单的程序里看到很多端倪,这有助于我们接下来的学习和研究。
fn main() {
println!("Hello, World!");
}
算上大括号也就只有短短的3行,但其实能看出不少东西的,我们一一来说。
首先,整体上来说,Rust也采用了大括号和分号的代码风格,这一点跟C++一样,而不同于Python和Go。函数体是用大括号包裹的,并且每行语句都以分号结尾,那么这种风格通常来说都是对缩进和空格、空行不敏感的,因此这里的缩进、空行等都是一种让代码可读性更高的编码规范,而不是语法本身要求。
其次,这里出现了我们熟悉的main
函数,作为程序的入口。函数是可以直接单独存在的,不需要强行包裹在类中,这一点与Java不同,说明Rust至少是「可以」做面向过程编程的。
最后,我们可以看到main
函数是空参数空返回值的,这一点与C/C++不同,也就是说当Rust程序作为应用程序时,其与OS或父进程之间的交流应当有其他专有的方式,而不是通过传参。这种限制其实有他自己的独特优势的,这点我们在后面章节再详细介绍。
其他呈现的细节还有比如说双引号表示字符串,而单引号表示字符,这与C++表现一致,不同于js、Python、shell等。这些都等后面章节我们再一一诉说。
这里希望读者可以有一个主观印象,Rust程序大致就是长这个样子哒~。
类型说明符
在C语言以及其衍生语言(如C++、Java、Objective-C等)中,有一个非常大的特点(甚至说是缺陷),就是「行为」被隐含在了「类型」说明符之中。
举个例子来说:
int a;
这里的int
首先是表达了「创建变量」的含义,其次才是表示「变量类型是整型」。「创建变量」这种表示动作的语义是隐藏在里面的,根据类型说明符的位置、上下文、组合等不同来区分。比如上面单独一个类型符号表示创建变量,而如果加上小括号就表示「函数声明」的动作:
int f();
这里的int
和()
共同表示「函数声明」,而int
又承担了「函数返回值类型」的类型说明的含义。
而在一些「仅需要表示动作,不需要类型」的场合,C++的这种语法就显得很奇怪和诡异,比如说:
template <typename T1, typename T2>
auto f(T1 a, T2 b) -> decltype(a + b) {}
这里的auto
并不表示任何类型,因为返回值类型在箭头后面的表达式,因此这里只是需要一个占位符,跟小括号一起表达「函数定义」这样的动作语义,所以强行塞了一个auto
。
而对于Rust来说,这个问题就得到了非常好的解决,那就是把「动作」和「类型」这两件事分来,比如说:
let a: i32;
我们看到这里有两个关键字,其中let
表示「定义变量」这个动作,而i32
表示类型,也就是32位有符号整型。
那么,当遇到只需要动作描述,不需要类型描述的场景,这个语法就很自然,只要去掉类型描述符就好了,比如说:
let a = 5;
这时a
的类型就会由其初始化的值类型来推导,整体语法上会比C++的auto
占位符自然得多。
再比如说定义函数,也是用fn
关键字,与类型的符号分开表示。
另一点就是,在C/C++当中,我们推荐使用int
、long
这种视架构而定的类型,比如说long
在32位环境下是4字节,在64位环境下是8字节。原因是在CPU的寄存器原理上。匹配寄存器长度的数据读写是效率最高的。但这种缺点也很明显,因为你不能确定它的具体长度,自然也就不能确定它的值域范围。所以我们看到其实更多情况下int32_t
、int64_t
使用得更广泛。这其实就是一个编程思维问题,如果你是底层思维,那你考虑更多的是硬件的位宽,但如果你是上层思维,你更多考虑的是类型的值域。
Rust当中虽然也有匹配架构的isize
和usize
类型,但是显然更被推荐的是i32
,u16
等这种指定长度的类型,其实这也说明了Rust设计之初,是希望你更加聚焦到程序逻辑上,而不是底层框架上的。
引用/指针
在C++当中,引用和指针是独立的两套语法和语义,但底层实现上又非常相似,大多数场景下用指针或是引用都不会有太大差别,但这也就造成了另一个问题,就是我们可能难以区分「值传递」和「引用传递」。
举个例子来说:
int a = 5;
f(a); // 但从这里是看不出来a是值传递还是引用传递的
C++引入的这种「引用传递」的语义,原本的目的也是为了屏蔽「函数调用栈之间的地址传递」这样的信息,让你感觉就是真的把变量本身传进去了一样。但与之带来的就是它无法与真正的值传递进行区分,对于上例来说void f(int);
和void f(int &)
的函数类型就会有不同的表现。
有写团队为了区分这种情况,会有规定类似于「出参必须使用指针」这样的方法,希望能在调用时对出入参进行区分,比如说:
void f(int in, int \*out);
void Demo() {
int a = 5;
int b = 0;
f(a, &b); // 加了&的是出参
}
但这样做局限性也很大,首先我们还是区分不了入参是值传递和引用传递,比如说:
void f1(Test t);
void f2(const Test &t);
void Demo() {
Test t;
f1(t); // 入参,但会触发拷贝构造
f2(t); // 入参,但是单纯引用传递
}
另一方面就是说,对于「指针类型的入参」,可能还是会添加取地址符,但本质上却作为了入参,比如说:
// 这个函数是为了打印指针的值,因此p并不是出参
void PrintPtr(const void \*p) {
printf("%p", p);
}
void Demo() {
int a = 5;
PrintPtr(&a); // 其实并不是把a当做出参,而是把「&a」当做了入参
}
当然,像C++这种同时保留指针和引用语义的语言也是屈指可数的,大多数语言都只会保留其一,比如说Go当中,仅保留指针语义而没有引用语义。
// 只有指针语法
func f(a \*int) {
\*a = 5 // 解指针后操作出参
}
func main() {
var a = 3
f(&a) // 显式取地址
fmt.Println(a)
}
另一种就是像Java这种的,完全取消了引用和指针,只是会根据实际传参时的数据类型来决定使用值传递还是引用传递:
先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前在阿里
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Linux运维全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上运维知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上运维知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新