10、初识Rust – 结构体

目录 编程

Rust中的结构体和元组类似,它们都可以声明许多相关的值,每一部分也可以是不同的类型。但是,和元组不一样的是,结构体需要命名每个部分数据以便能清楚表明这个值的意义。由于这些值有名字,所以结构体比元组更加灵活,不需要依赖顺序或者索引去访问实例中的值。类似固定类型的JSON

struct User {
    name: String,
    passwd: String,
    status: bool
}

定义一个节奏体需要使用 struct 关键字,并为结构体提供一个名字。接着在大括号中定义每一部分数据的名字和类型,称为 字段( field )。如上代码就展示了一个存储用户账号的结构体。

定义结构体之后,为了使用它,通过为每个字段定义的具体值来创建这个结构体的实例,创建一个实例需要以结构体的名字开头,接着在 : 使用指定类型的key 来为实例附值。

#[derive(Debug)]
struct User {
     name: String,
     passwd: String,
     status: bool
}

fn main() {
    let a = User {
        name: String::from("fcy"),
        passwd: String::from("xxxxxx"),
        status: true
    };
    println!("{:?}",a);
}

如上,为User 结构体赋予不同类型的值,并打印到终端。

struct User {
    name: String,
    passwd: String,
    status: bool
}

fn main() {
    let a = User {
        name: String::from("fcy"),
        passwd: String::from("xxxxxx"),
        status: true
    };
   println!("{},{},{}",a.name,a.passwd,a.status);
}

像json一样使用结构体变量 . 字段名称来获取值,但是需要注意,如果该字段不存在,就会抛出错误。

struct User {
    name: String,
    passwd: String,
    status: bool
}

fn main() {
    let mut a = User {
        name: String::from("fcy"),
        passwd: String::from("xxxxxx"),
        status: true
    };
    a.name = String::from("newName");
   println!("{},{},{}",a.name,a.passwd,a.status);
}

如果需要修改其中的值,同样也需要在变量名称中加入mut,否则就会报错。

struct User {
    name: String,
    passwd: String,
    status: bool
}

fn main() {
    let a = test_function(String::from("fcy"),String::from("xxxxxx"));
   println!("{},{},{}",a.name,a.passwd,a.status);
}


fn test_function(name: String, passwd: String) -> User{
    User {
        name,
        passwd,
        status: true
    }
}

如上是一个字段初始化的简写语法,我们可以将User的初始化写在一个函数中,并直接将值传入函数中,注意函数传入值的名称与结构体中字段的名称一样,这样我们就可以不必去填写 字段名称: 字段值这样的写法,编译器会自动处理。

struct User {
    name: String,
    passwd: String,
    status: bool
}

fn main() {
    let a = User {
        name: String::from("fcy"),
        passwd: String::from("xxxxxx"),
        status: true
    };
    let b = User {
        status: false,
        passwd: String::from("zzzzz"),
        ..a
    };
   println!("{},{},{}",b.name,b.passwd,b.status);
}

如上是一种结构体更新语法,它可以从其旧实例创建新实例并更新部分值。需要注意的是,..a必须写在结构体尾部,这样就能自动赋予剩余未显示设置字段的值到新实例对应的值,同时,该操作也是一个移动操作,所以在附值后,原实例的值会失效,只有部分没有赋予的值才会依旧存在。

除了有字段的结构体外,也可以创建没有命名字段的结构体。

#[derive(Debug)]
struct xyz(i8,i8,i8);
   
fn main() {
    let a = xyz(2,3,4);
   println!("{:?},{},{}",a,a.0,a.2);
}

如上代码,我们声明一个xyz的位置结构体,并在main代码块中附值并绑定变量a然后打印。从打印的语句中我们可以发现,没有字段的结构体可以使用 a.0 , a.1这样的方式来访问其中的值。

除了无字段的结构体外,我们也可以创建没有任何字段的类单元结构体。

struct xyz;
   
fn main() {
    let a = xyz;
}

不过,由于部分内容这里未学习到,所以不在此讲此结构体的作用。并且目前我们声明的结构体都使用了自身拥有所有权的string类型,并不是&str类型。因为目前使用引用的话会报错并提示需要生命周期标识符,这涉及到后续的 生命周期 功能,所以这里也不具体说明,只使用String这类类型暂替。

那么,我们为什么需要使用到结构体呢?因为结构体相对于元组和一般变量来说,更加容易被人看明白。如下代码,我们将用三种方式来书写

//使用变量
fn main() {
    let width = 20;
    let height = 20;

    println!("{}",area(width,height));
}

fn area(width: i32, height: i32) -> i32 {
    width * height
}
//使用元组

fn main() {
    let rect = (20,20);

    println!("{}",area(rect));
}

fn area(rect: (i32,i32)) -> i32 {
    rect.0 * rect.1
}
// 使用结构体

struct Rect {
    width: i32,
    height: i32
}

fn main() {
    let rect = Rect {
        width: 20,
        height: 20
    };

    println!("{}",area(rect));
}

fn area(rect: Rect) -> i32 {
    rect.width * rect.height
}

结构体相对于变量的优势是,它很好的关联了数据,使代码更加易懂也更加容易处理。而相对于元组,可读性的优势将更加明显。同时派生 trait之后,可以像JSON一样直接打印出整个结构。

同时,我们也可以在结构体内定义方法。

struct Rect {
    width: i32,
    height: i32
}

impl Rect {
    fn area(&self) -> i32 {
        self.width * self.height
    }
}

fn main() {
    let rect = Rect {
        width: 20,
        height: 20
    };

    println!("{}",rect.area());
}

为了使 area函数定义在Rect上下文中,需要开始一个impl块(impl 是 implementation 的缩写),这个impl 块中的所有内容都将和Rect类型相关联。接着将area函数移动到impl大括号中,并将签名中的第一个参数,也是唯一一个参数 &self,self参数用来代替rect: Rect,&self 实际上是 self: &Self 的缩写。接着就可以在main块中声明Rect的宽高,并调用定义的area函数直接返回面积。

-> 运算符到哪去了?

在 C/C++ 语言中,有两个不同的运算符来调用方法:. 直接在对象上调用方法,而 -> 在一个对象的指针上调用方法,这时需要先解引用(dereference)指针。换句话说,如果 object 是一个指针,那么 object->something() 就像 (*object).something() 一样。

Rust 并没有一个与 -> 等效的运算符;相反,Rust 有一个叫 自动引用和解引用automatic referencing and dereferencing)的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。

它是这样工作的:当使用 object.something() 调用方法时,Rust 会自动为 object 添加 &&mut 或 * 以便使 object 与方法签名匹配。也就是说,这些代码是等价的:


p1.distance(&p2);
(&p1).distance(&p2);

第一行看起来简洁的多。这种自动引用的行为之所以有效,是因为方法有一个明确的接收者———— self 的类型。在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取(&self),做出修改(&mut self)或者是获取所有权(self)。事实上,Rust 对方法接收者的隐式借用让所有权在实践中更友好。

除了单&self参数外,也可以进行多参数函数。

struct Rect {
    width: i32,
    height: i32
}

impl Rect {
    fn area(&self) -> i32 {
        self.width * self.height
    }
    fn contrast(&self,other: &Rect) -> bool {
        let rect = &self.width * &self.height;
        let other_rect = &other.width * &other.height;
        rect < other_rect
    }
}

fn main() {
    let rect1 = Rect {
        width: 20,
        height: 20
    };
    let rect2 = Rect {
        width: 22,
        height: 18
    };
    let rect3 = Rect {
        width: 30,
        height: 30
    };
    println!("{}",rect1.contrast(&rect2));
    println!("{}",rect1.contrast(&rect3));
    println!("{}",rect1.area());
}

如上就增加了一个对比面积函数 contrast 通过传入不同的Rect结构体来对比任意结构体的大小并返回bool类型。

同时,我们也可以去掉&self参数来定义一个关联函数,它接收一个参数并同时作为宽高,这样可以更好的创建一个正方形。

#[derive(Debug)]
struct Rect {
    width: i32,
    height: i32
}

impl Rect {
    fn area(other: &Rect) -> i32 {
        other.width * other.height
    }
    fn square(size: i32) -> Rect {
        Rect {
            width: size,
            height: size
        }
    }
}

fn main() {
    let rect = Rect::square(10);
    let a = Rect::area(&rect);
    println!("{:?}",&rect);
    println!("{}",a);
}

所有在 impl 块中定义的函数被称为 关联函数associated functions),因为它们与 impl 后面命名的类型相关。我们可以定义不以 self 为第一参数的关联函数(因此不是方法),因为它们并不作用于一个结构体的实例。我们已经使用了一个这样的函数:在 String 类型上定义的 String::from 函数。使用结构体名和 :: 语法来调用这个关联函数:比如 let rect = Rect::square(10);。这个函数位于结构体的命名空间中::: 语法用于关联函数和模块创建的命名空间。

#[derive(Debug)]
struct Rect {
    width: i32,
    height: i32
}

impl Rect {
    fn area(other: &Rect) -> i32 {
        other.width * other.height
    }
}

impl Rect {
    fn square(size: i32) -> Rect {
        Rect {
            width: size,
            height: size
        }
    }
}

fn main() {
    let rect = Rect::square(10);
    let a = Rect::area(&rect);
    println!("{:?}",&rect);
    println!("{}",a);
}

并且每个结构体impl块允许拥有多个,哪怕写成如上那样没有什么意义的多impl,它也是有效的。结构体让你可以创建出在你的领域中有意义的自定义类型。通过结构体,我们可以将相关联的数据片段联系起来并命名它们,这样可以使得代码更加清晰。在 impl 块中,你可以定义与你的类型相关联的函数,而方法是一种相关联的函数,让你指定结构体的实例所具有的行为。