C++工程师的Rust迁移之路(2)- 类与结构体

在上文 zhuanlan.zhihu.com/p/75 中,我从一个Hello World的示例开始,简要介绍了一下C++和Rust在变量和基础数据类型之前的异同。

在本文中,我将在C++类和Rust的结构体之间做一个对比,顺便介绍Rust中的核心概念Trait。

类vs结构体

C++

class Rectangle {
public:
    Rectangle(float width, float height)
        : width_(width), height_(height)
    {}

public:
    float area() const {
        return width_ * height_;
    }

    void resize(width, height) {
        width_ = width;
        height_ = height;
    }

private:
    float width_, height_;
};

Rust

struct Rectangle {
    width: f32,
    height: f32
}

impl Rectangle {
    pub fn new(width: f32, height: f32) -> Rectangle {
        Rectangle {
            width: width,
            height: height
        }
    }
    
    pub fn area(&self) -> f32 {
        self.width * self.height
    }
    
    pub fn resize(&mut self, width: f32, height: f32) {
        self.width = width;
        self.height = height;
    }
}

对于C++开发者来说,有一件事情是众所周知的:struct和class本质上是一样的,唯一的区别就是struct的成员默认是public的,而class的成员默认是private的。

而对于Rust来说,它没有class的概念,只有struct的概念。而Rust中的struct的成员默认都是private的,除非加上pub关键词做修饰。

从语法上来说,可以看到Rust跟C++有一个很大的不同。在C++里面,往往我们的方法声明(对于模版类来说,甚至定义也是如此)是包含在class body这个大的语句块内的,而且对于顺序没有明确的要求。而在Rust里面,结构体的声明仅包含了它内部的数据结构,相关的实现是放在额外的impl语句块中的。这个变化看似稀松平常,但内含着巨大的好处,特别是类的逻辑比较复杂的时候。作为C++程序员,我们一定碰到过那种在一个类的头上和一个类的尾端,甚至是某些函数之间都声明了成员变量的情况。这就引入了一个风险,就是当你修改代码的时候,有可能会忘记给其中的某些变量赋值,这非常危险。而在Rust中,由于结构体的成员变量是与函数分开,并且集中在一起的,就大大降低了出现这种情况的概率。

在C++中,我们写一个类时,第一个要做的事情,就是定义它的构造函数;而反观Rust,它是没有构造函数这个概念的。上述代码中的new函数,如果对比C++的概念的话,相当于Rectangle类的一个静态方法,它的名字叫做new,接受2个f32类型的参数,返回值是一个Rectangle对象。这里有三点需要注意的:

  1. 构造Rectangle的语法。可以看到,构造Rectangle的时候,直接通过指定它的私有变量的值来实现了构造。而Rust的静态检查器是非常严格的,如果你忘记了构造其中的某个成员变量,它会直接在编译阶段报错,并阻止编译。所以,当你修改了结构体的结构以后,永远不用担心会在某处代码出现未初始化成员变量的bug,因为这样的情况通不过编译。
  2. 可以看到new函数中,并没有return语句。这是一个非常关键的点。在C++中代码块是一个语句(statement),它是不能作为右值的;而在Rust中,代码块是一个表达式(expression)。所以,在C++中常用的三目运算符([condition] ? [true_exp] : [false_exp])在Rust中并不存在,取而代之的是if表达式(if [condition] { [true_exp] } else { [false_exp] })。
  3. 另外,我们可以看到,在Rectangle {}的后面是没有分号的,这表示了它是一个表达式,而不是语句。当它不加分号时,这个代码块的类型是Rectangle,而加上了分号以后,它的类型就变成了(),也就是unit type,与函数声明的返回值Rectangle不符,编译出错。关于unit type的更多细节,在以后的文章中,我可以再做进一步的阐述。

我们再接着看area和resize两个方法,可以看到它们的第一个参数分别是&self, 和&mut self。这里其实也有两点需要注意的:

  1. 与C++一样,&表示引用,这里的&self其实是一个语法糖,相当于self: &Rectangle,而&mut self相当于self: &mut Rectangle
  2. 就像我在前文中所说,在Rust中,默认的行为是不可变的,除非加上mut关键词。那么在这里的&self,就相当于C++中的const方法,而&mut self相当于非const方法。

Playground

Rust语言的官网提供了一个Playground工具,可以供大家在无需安装Rust环境的前提下,试用Rust。

本文中的例子我也发布到了Rust Playground上,链接如下:

Rust Playgroundplay.rust-lang.org

欢迎大家使用。

延伸阅读

上一篇:

黄珏珅:C++工程师的Rust迁移之路(1)- 起步zhuanlan.zhihu.com图标

下一篇:

黄珏珅:C++工程师的Rust迁移之路(3)- 继承与组合 - 上zhuanlan.zhihu.com图标

编辑于 2019-08-05