学习Rust零零碎碎已经有两周的时间了,老实说最开始其实是本着学习好C++的态度打算好好学习C++的;但是个人对自己的代码能力并没有什么自信,因为C++实在太太太太太容易写出Memory-Leak的代码了!
最后,就打算试一试Rust这门语言。用过之后不得不说,Rust应该是神级的Program Language了,编译检查简直严格到变态!
本文主要想谈一谈我在学习了Rust两周后的一些感受;
源代码:
学习Rust两周后我的一些感想
GC掩盖了什么?
关于内存管理的一些常见的问题有:
- 没有及时释放分配的内存导致内存泄漏;
- 释放了某一个内存区域两次;
- 引用了一个被释放的内存空间;
目前大部分现代编程语言,比如:Java、Golang、Python等等,基本上都实现了自己的垃圾回收器GC,这让我们忽略了在写代码时,我们肆意在堆上分配的各种内存;
可能很少会有人考虑,编程语言的GC到底帮我们做了什么?如果没有了GC,我们应该怎么办?
对于C++、Rust这种无GC的编程语言,一个很重要的特点就是要自己控制堆内存,而控制堆内存一个很重要的工具就是:指针;
在这里我不打算谈论如何使用智能指针实现内存的开辟和释放,并且避免内存泄漏;
首先我们来看看下面这个Rust中的例子:
1 | fn main() { |
上面是Rust官方提供的一个很经典的例子;
在一个新的作用域中创建的新的变量x的引用被赋值给了引用r,随后退出新的作用域后,使用引用r访问变量x;
如果你尝试编译这个文件,将会产生一个编译错误:值x在退出作用域后就被自动释放了,因此此时引用r指向的是一个堆中已经不属于x的空间;
类似的这种操作是非常危险的,但是在C++中,这些操作是无法被编译器捕获的;
不妨来看一下在C++中这段相同逻辑代码的表现:
注意:在C++中这是被允许的操作!
1 |
|
代码会正常输出5;
你可能会说,这是因为在C++中,并不会自动释放x的内存;
下面我们使用string和智能指针unique_ptr来实现:
1 |
|
上面的代码通过unique_ptr保证了string数组x在退出作用域后内存空间被自动释放,但是我们的引用r仍然指向了这个内存空间的开头位置!
尝试运行这个例子,可以得到下面的结果:
1 | hello |
即内存被释放前,引用r的确指向了数组开头,而退出作用域后,由于空间被释放掉了因此此时引用r指向的空间是空的!
但是引用r还是成功的指向了我们的内存,并且可以肆无忌惮的访问!
没什么大惊小怪的,C++甚至都不会检查数组越界!
这些危险的行为按道理应当在编译器就被发现,并且被解决!
Rust正是做到了这一点:
1 | fn main() { |
运行程序,正常输出:
1 | r: hello |
但是如果取消注释最后一行println!("r: {}", r);,将无法编译:
1 | error[E0597]: `x[_]` does not live long enough |
因为Rust中的生命期检查将会发现当退出作用域后引用r将会指向一个被释放的内存空间;
关于垂悬引用(Dangling Refer)
除了上述很明显的引用了一个被释放的内存空间之外,还有另外一类也会产生这种错误的例子,就是在函数返回时产生了垂悬引用(Dangling Refer);
下面是一个Rust中的例子:
1 | fn main() { |
例子中,get_str()函数创建了一个关于字符串"hello"的引用并返回;
尝试编译会报一个错:
1 | error[E0106]: missing lifetime specifier |
报错的原因主要是因为:我们是在函数的内部创建的字符串"hello",但是在返回时,我们将这个字符串的引用返回到了函数体外;之后字符串"hello"被回收,因此返回的引用是一个已经被释放的区域;这就是一个垂悬引用(Dangling Refer);
其实我们是可以将这个返回值高性能的返回的,只要声明返回值是一个右值引用即可!
这就相对于函数将这个变量的所有权转移到了函数体的外部;
因此在Rust中,只需要返回含有所有权的String类型即可:
1 | fn main() { |
可以看到,通过所有权的方式,可以很好的理解垂悬引用这个概念,而Rust也是这么做的!
再谈函数入参和引用
从上面我们可以看到:如果需要返回函数内部在堆上创建的变量,需要将变量的所有权也一并交出;
但是如果返回值是一个引用呢?
那就必须要和入参有关系了!
因此,在Rust中添加了生命期泛型,用于标注入参和出参之间的生命期关系;
下面是一个Rust中的例子:
1 | fn main() { |
例子中,通过遍历vector中的各个str引用,返回一个新的、将字符串大写的数组;
需要注意的是,在调用x.to_uppercase()函数时会创建一个新的String,因此依然犯了上面的Dangling Refer的错误:在函数内部返回了引用;
我们可以通过将返回值修改为String:
1 | fn main() { |
这是合理的,因为函数执行完成后需要将所有权转接;
如果函数必须要返回引用类型,由上面的分析可知,出参是必须要从入参来的(否则就会引用函数中创建的变量,从而造成Dangling Refer错误!)
如,下面的函数返回了长度大于指定值的引用:
1 | fn main() { |
上面的代码可以被正确的执行:
1 | before: ["hello", "hi, there", "ok"] |
那么为什么我们这里没有声明生命期泛型呢?
这是因为:对于Rust的编译器而言,目前入参和出参的结构已经可以判断出生命期了;
因此Rust会自动加上生命期:
1 | fn longer_than<'a>(str: Vec<&'a str>, len: usize) -> Vec<&'a str> { |
那么什么时候需要显式声明生命期呢?
下面是一个经典的例子:
1 | fn main() { |
此时,函数无法推断是返回X还是Y,因此需要显式声明!
总结
本文草草总结了关于学习Rust时的一些感想;
最后想说的是:如果是老的项目难以迁移,则可以继续使用C++;否则为什么不试试更加安全的Rust呢?
附录
源代码: