第N次入门Rust - 4.结构体

文章目录

  • 前言
  • 4.1 定义
  • 4.2 实例化
    • 4.2.1 基本实例化语法
    • 4.2.2 字段初始化简写语法(field init shorthand)
    • 4.2.3 结构体更新语法(struct update syntax)
    • 4.2.4 元组结构体(tuple structs)
    • 4.2.5 类单元结构体(unit-like structs)
    • 4.2.6 结构体数据的所有权
    • 4.2.7 结构体例子
    • 4.2.8 derive简单介绍
  • 4.3 结构体方法
    • 4.3.1 基本语法
    • 4.3.2 关联函数


前言

这一篇介绍结构体——如何自定义数据结构~


4.1 定义

  • 结构体是个自定义数据类型,允许开发者命名和包装多个相关的值,从而形成一个有意义的组合。
  • 结构体定义需要使用关键字struct进行定义,且定义过程与json字符串类似:
    struct StructName {
        field_name1: type1,
        field_name2: type2,
        ...
        field_namen: typen,
    }
    
  • 例子:
     struct User {
         username: String,
         email: String,
         sign_in_count: u64,
         active: bool,
     }
    
  • 定义结构体的时候最后一个字段的声明后面可以跟逗号,这样能统一格式。
  • 定义结构体的时候无法顺带给字段赋予初始值。
  • 可以在函数内部进行结构体的定义。

4.2 实例化

4.2.1 基本实例化语法

  • 实例化结构体:创建一个实例需要以结构体的名字开头,接着在大括号中使用 key: value 键-值对的形式提供字段,其中 key 是字段的名字,value 是需要存储在字段中的数据值。实例中字段的顺序不需要和它们在结构体中声明的顺序一致。换句话说,结构体的定义就像一个类型的通用模板,而实例则会在这个模板中放入特定数据来创建这个类型的值。
    let var = StructName {
        field_name1 : value1,
        field_name2 : value2,
        // ...
        field_namen : valuen,
    };
    
  • 例子:
     let user1 = User {
         email: String::from("[email protected]"),
         username: String::from("someusername123"),
         active: true,
         sign_in_count: 1,
     };
    
  • 初始化结构体中的每一个字段的值不能只初始化某几个,需要全部都初始化。
  • 访问结构体实例中的字段:从结构体中获取某个特定的值,可以使用点号(ins.field_name=new_value)。
    user1.email = String::from("[email protected]");
    
  • 一旦struct的实例是可变的,那么该实例的所有字段都是可变的。rust不允许只将某个字段标记为可变的。 这一点按照前几章的内容说明不难理解。
  • struct可以作为函数的返回值。

4.2.2 字段初始化简写语法(field init shorthand)

  • 字段初始化简写语法:在初始化结构体中的字段时,对于没有赋予初值的字段,会使用上下文中同名字段进行初始化。
  • 具体语法是初始化结构体中的字段时,需要手动赋予初值的字段在字段后面给出初值,对于使用上下文同名字段初始化的字段,只要写字段名即可。
  • 例子:
    fn build_user(email: String, username: String) -> user {
        User {
            email,
            username,
            active: true,
            sign_in_count: 1,
        }
    }
    

4.2.3 结构体更新语法(struct update syntax)

  • 结构体更新语法为结构体实例进行初始化的时候,如果用到另一个给定的同类型结构体实例中的参数,可以先指定需要自定义字段初始化字段的值,然后通过..语法指定剩余未显示设置值的字段使用给定实例对应字段的值
  • 例子:
    let user2 = User {
        email: String::from("[email protected]"),
        username: String::from("x"),
        ..user1
    }
    

4.2.4 元组结构体(tuple structs)

  • 元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。
  • 当你想给整个元组取一个名字,并使元组成为与其他元组不同的类型时,元组结构体是很有用的,这时像常规结构体那样为每个字段命名就显得多余和形式化了。
  • 元组结构体定义语法:
    struct TupleName(type1, type2, ..., typen);
    
  • 实例化元组结构体语法:
    let var = TupleName(value1, value2, ... valuen);
    
  • 不同名字元组结构体之间哪怕每个字段下的属性完全相同,也是属于不同类型的元组结构体,它们之间的实例不能互通。
  • 元组结构体的使用与普通的元组大致相同,比如可以用模式匹配将其解构为单独的部分,也可以使用.下标的形式访问单独的值等。

4.2.5 类单元结构体(unit-like structs)

  • 类单元结构体没有任何的字段,与unit类型()类似。
  • 类单元结构体常常在开发者想要在某个类型上实现 trait 但不需要在类型中存储数据的时候发挥作用。
    • 即实现一种类型,但是这种类型没有任何字段,只有行为。

4.2.6 结构体数据的所有权

  • 结构体必须掌握字段值所有权,因为结构体失效的时候会释放所有字段。
  • 当一个结构体中所有字段都不是引用类型时,则该结构体持有结构体内所有字段的所有权。此时,只要结构体的实例是有效的,那么里面的字段数据也是有效的。
  • 结构体的字段也可以是引用类型,但是这需要用上生命周期的机制。
    • 生命周期的作用:保证只要结构体实例是有效的,那么里面的引用也是有效的。
    • 如果结构体里面存储引用,但是不使用生命周期,就会报错。
  • 无法单独设置结构体中每个字段的可读性,结构体中每个字段的可读性与结构体实例的可读性一致,即当一个结构体实例是可写的时候,其字段都是可写的。(个人观点,待确认)

4.2.7 结构体例子

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

fn main() {
    let rec = Rectangle {
        width: 30,
        length: 50,
    };
    println!("{}", area(&rec));
    println!("{:#?}", rec);
}

fn area(rec: &Rectangle) -> u32 {
    rec.width * rec.length
}
  • #[derive(Debug)]:派生,使Rectangle结构体的实例可以通过println!("{:?}")打印调试信息。
  • println!("{:?}")是打印结构体的信息,println!("{:#?}")是petty print结构体的信息。

4.2.8 derive简单介绍

关于#[derive]可以先暂时不用在意,后续会讲,这里先简单聊一下它的作用。

结构体要想拥有某种能力就需要实现对应的trait,比如上面例子中的“输出调试信息”能力,但是有很多的trait实际上其实现都是大同小异的(即可以按照一定的模板来编写该trait的实现代码),比如“输出调试信息”实际上就是把结构体的字段按键值对的形式输出(当然也可以是其他格式,只是一般输出调试信息的时候都比较关心值中每一个字段的取值)。

#[derive]的作用是为自动给结构体生成trait的实现代码,从而减少模板代码的编写。

就当然还有很多其他可以使用derive自动实现的trait,这里先不展开讲,等后续基本语法都过了一遍以后再聊这个话题。

4.3 结构体方法

  • 方法即结构体专属的函数。
  • 方法与函数的异同:
    • 相同点:它们使用 fn 关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。
    • 不同点:方法是在结构体的上下文中被定义(或者是枚举或 trait 对象的上下文),且方法的第一个参数总是self,代表调用该方法的结构体实例。

4.3.1 基本语法

  • 结构体StructName的结构体方法,需要在其同名的impl块中声明。

    struct StructName {
        // 各种字段声明
    }
    
    impl StructName {
        fn func1(&self, param1: type1) -> return_type{
            // 各种逻辑
        }
    }
    
  • 调用结构体方法语法:实例名.方法名()

  • 当方法定义在impl块中,方法的第一个参数self会被自动替换为类型为对应的结构体类型。

  • 在声明self的时候,根据实际需要可以考虑使用不同的修饰符控制可读性以及所有权转移还是借用(也就是说,调用方法的时候也会发生所有权转移或者借用)

    self        // 只读、调用方法时发生所有权转移
    mut self    // 可读可写、调用方法时发生所有权转移
    &self       // 只读借用
    &mut self   // 可变引用
    
  • 自动引用和解引用(automatic referencing and dereferencing):在调用某个值的方法时(比如a.method())发生这种行为,指的是在结构体实例调用它的方法时,根据方法的第一个参数(即self)是否有加引用等操作符,在调用方法的位置自动添加对应的修饰符(&&mut*)使其与方法名匹配。

    // 例子:
    struct DemoStruct { /***/ }
    impl DemoStruct {
        fn fun1(&self, args: &str) { /***/ }
        fn fun2(self, args: &str) {/***/}
    }
    
    let ds = DemoStruct{ /* 初始化 */ }
    ds.fun1("a");
    (&ds).fun1("a");
    
    • ds.fun1("a")(&ds).fun1("a")是等价的,因为在编译的时候,编译器检查方法fun1发现其第一个参数是一个只读借用,因此会将ds.fun1("a")补全为(&ds).fun1("a")
  • 一个结构体可以声明多个impl块,在同一个结构体的不同的impl块中声明方法与在同一个impl块中声明方法作用是一样的。

4.3.2 关联函数

  • impl 块的另一个有用的功能是:允许在 impl 块中定义不以 self 作为参数的函数。这被称为 关联函数(associated functions),因为它们与结构体相关联。它们仍是函数而不是方法,因为它们并不作用于一个结构体的实例。
  • 关联函数就是其他语言中的静态方法(函数)。
  • 使用结构体名和 :: 语法来调用这个关联函数:StructName::关联函数名(参数列表)

你可能感兴趣的:(第N次入门Rust,rust,开发语言,后端)