老前端人学Rust - 第五课(结构体)

208 阅读7分钟

第五课来了兄弟们!™差点就断更了,还好我意志力惊人,躲懒了俩礼拜还是站出来了,哈哈哈。这个系列不会断的,虽然一篇文章比一篇文章阅读量少,心塞。麻烦大家用摸鱼的小手,点个赞呗~

好了,这次咱话不多说直接开始进入主题!

举个栗子

用我们前面学过的知识来设计一个计算长方形面积的例子:

fn main() {
    let width1 = 30;
    let height1 = 50;
    
    println!("The area of the rectangle is {} square pixels.", area(width1, height1));
}

fn area(width: u32, height: u32) -> u32 {
    width*height
}

执行这段代码会打印出The area of the rectangle is 1500 square pixels.,这个例子中我们使用area函数来计算一个长方形的面积,这个函数有两个参数widthheight,但是这两个参数其实是有关联的语义在,比如他们是长方形的宽高,但是从代码角度看,他们其实只是两个毫无关联的u32类型的变量。

那我们用元组改造一下:

fn main() {
    let rect1 = (30, 50);
    
    println!("The area of the rectangle is {} square pixels.", area(rect1));
}

fn area(dimensions: (u32, u32)) -> u32 {
    dimensions.0 * dimensions.1
}

这样改造完后,函数的数据结构变了,只需要一个参数就可以传参了,但是他跟我们写js用数组传参一样,简单了,却缺少了语义化,我们不知道dimensions.0是不是代表了宽,dimensions.1是不是代表了高。

我们在js中,一般定义函数的参数时,如果是强关联的属性,一般会放到一个对象中去,如:

type TDimensions {
    width: number;
    height: number;
}

function area(dimensions: TDimensions) {
    const {width, height} = dimensions;
    return width*height;
}

这样在语义和代码层面都能表现出需要的参数就是尺寸,尺寸包含宽和高。那我们之前学的rust还没有任何一个数据结构能表明像js的对象一样的集合,这节课我们就来认识一下与Object类似的数据结构 --- 结构体(strcut)

结构体

结构体是由其他的基础类型或复合类型组成的,当它所有字段同时实例化后,就生成了这个结构体的实例,他由关键字struct定义。这与我们的Object十分类似,都是可以有基础类型,可以有引用类型组成。

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

这个结构体就是一个很典型的例子,该User结构体由4个属性构成,包括了基础类型的bool和u64,还有引用类型的String。 那如何给结构体赋值呢?

fn main() {
    let user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("someone@example.com"),
        sign_in_count: 1,
    };
}

是不是跟js一毛一样。

结构体的形式

结构体有三种构造形式,包括命名结构体、元组结构体和单元结构体。

  • 命名结构体

我们上面定义的User就是命名结构体,他是指每个字段都有名字的构造体。 我们还可以像js一样,定义结构体的时候缩写,如:

fn main() {
    let username = String::from("someusername123");
    let email = String::from("someone@example.com");
    let sign_in_count = 1;
    
    let user2 = User {
        active: true,
        username,
        email,
        sign_in_count
    };
}

还可以类似扩展操作符...一样,对结构体进行解构,不过要注意,rust中只有两个点..,而且是要放在已改的属性后面,表示将剩下的不需要修改的结构体的属性复制过来,而不是像js一样我们先...,将整个对象解构后对需要修改的属性进行覆盖操作:

fn main() {
 let username = String::from("someusername123");
    let email = String::from("someone@example.com");
    let sign_in_count = 1;
    
    let user2 = User {
        active: true,
        username,
        email,
        sign_in_count
    };
    
    let user3 = User {
        active: false,
        ..user2    // 注意这里的写法,两个点,并且需要放在需要修改的属性之后
    };
    
    println!("{}", user3.email)
}
  • 元组结构体

元组结构体就是元组和结构体的结合体。

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

这个例子就是一个元组结构体的例子,他使用关键字struct定义了两个结构体,但是后面不是使用花括号{},而是用小括号()像定义元组一样定义了Color和Point,这样的写法省略了结构体的命名,是一个匿名的结构体。

这种定义方式在某些情况下有用,比如上面的Color,我们定义RGB颜色,自然知道这个结构体的三个参数代表红绿蓝,也就不需要特意指定属性名是什么了。

  • 单元结构体

单元结构体也要空结构体。顾名思义,这是一个没有任何字段的结构体类型。当某个类型需要实现一个trait却不需要在该类型存储任何数据结构的时候,空结构体就可以发挥作用。具体的我们后面的课再讲这个结构体的妙用。

struct Empty;
fn main() {
    let e = Empty;
}

面向对象特性

现在我们用结构体来改造下开头的例子:

struct Rectangle {
    width: u32,
    height: u32
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50
    };
    
    println!("The area of the rectangle is {} square pixels.", area(&rect1));
}

fn area(dimensions: &Rectangle) -> u32 {
    dimensions.width * dimensions.height
}

这个例子中我们使用了结构体代替了元组,很像ts的写法了。既有语义化的声明,又有代码层面的关联。

但是我们还可以继续改造,因为结构体还有面向对象的特性!虽然rust不是一个面向对象的语言,但是他确实在结构体和枚举类型、trait对象中可以有一些面向对象的特性。

rust中有一个impl关键字。使用他可以对结构体进行方法的定义,并在结构体的实例进行方法的调用。

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32
}

impl Rectangle {
    fn area(&self) -> u32{
        self.widhth * self.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50
    };
    
    println!("The area of the rectangle is {} square pixels.", rect1.area());
}

使用Impl关键字,在Rectangle的上下文环境中定义了函数,参数唯一,就是&self,表示自身的不可变借用,如果需要可变借用,跟之前学的一样,使用&mut self。然后就可以在Rectangle的实例上使用定义的方法。

这里的self就像js中class的this指针,只不过js中是隐式的,而rust不会有隐式参数,全部会显示出来。area(&self)其实是area(self: &Self)的简写,Self是一个特定的类型,表示正在被实现的那个类型,比如当前的Rectangle。

注意:方法和函数一样,都使用fn关键字定义,但是方法和函数不是一个东西。

多次定义&多个传参

impl可以多次定义,方法也可以传递多个参数:

impl Rectangle {
    fn area(&self) -> u32 {
        self.widhth * self.height
    }
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height 
    }
}

fn main() {
     let rect1 = Rectangle {
        width: 30,
        height: 50
    };
    
    let rect2 = Rectangle {
        width: 20,
        height: 30
    };
    
    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
}

关联函数

impl还允许我们不用接收self作为参数,不过这个时候,他就不是方法了,而是一个关联函数。表示这个函数与结构体互相关联。比如我们经常使用的String::from就是关联函数的一种。


impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle {width: size, height: size}
    }
}

使用方法就是Rectangle::square(20)。

end

结构体可以帮我们创建一些具有特定意义的领域,可以将多个不同类型的数据结构组合起来。方法相当于结构体的行为,也就是类中的方法的概念。关联函数是将不需要实例的但是属于这个结构体的特定的功能,从而放置到结构体的命名空间的一种操作。

好了,这节课就到这里了。最近确实有点忙的不可开交,学的也比较散。大家见谅~~

希望看到最后的同学能给点个赞就行。Thanks♪(・ω・)ノ