­

一起学Rust-结构体

  • 2019 年 10 月 4 日
  • 筆記

最近一段时间没有坚持写学习记录,总是给自己加班很晚,工作很忙这些理由来自我安慰,回想来属实有些羞愧,总是暗示自己没有时间,但是真正空闲的时间却在我一边安慰自己一边做着一些没啥太大意义的事情的过程中流逝了。所以要想让自己能够有进步,不逼自己一把是不行的。

本节学习分为四部分:

  • 结构体定义
  • 结构体使用
  • 元组结构体
  • 结构体内存

一、结构体定义

学习过C语言的同学都不会陌生,结构体在C中的使用不亚于指针的使用,是非常好用的一种组织不同类型数据的结构,同时又可以集合一些结构体的方法,对于非面向对象的C语言来说,结构体就类似 类 的作用。

话说回来,Rust中的结构体的意义基本与C中大体相同。下面看一下Rust中如何定义一个普通的结构体:

struct Rect {      width: i32,      height: i32,  }    //也可以定义空的结构体,仅仅用于需要实现结构体方法且不需要成员变量时使用  struct EmptyStruct {}

结构体使用 struct 关键字定义,后面跟随结构体名称,如果使用IDE,会建议使用大驼峰来命名结构体名称,结构体内成员在定义时需要标明类型。

结构体既可以定义在方法之外(直接定义在最外层),默认情况下,可以允许当前文件内的所有方法使用:

#[derive(Debug)]  struct Rect {      width: i32,      height: i32,  }  //注意:这里不可以使用分号    fn main() {      //使用结构体  }    fn other_fn() {      //使用结构体  }

结构体定义时,如果是定义在方法体外,结尾是不可以加分号的,而定义在方法体内既可以加也可以不加分号。

fn main() {      struct Rect {          width: i32,          height: i32,      };        struct Rect1 {          width: i32,          height: i32,      }  }

以上这两种均可以,所以直接就都不需要加分号了。(方法体内定义结构体不加分号,不代表是会返回一个结构体,下面会说到)

二、结构体使用

使用结构体也比较简单,下面定义一个结构体变量:

let square = Rect{ width: 10, height: 10};    let square1 = Rect {      width: 100,      height: 100  };

如果需要修改结构体成员则需要使用mut模式:

let mut square = Rect {      width: 30,      height: 30  };    square.width = 40;  square.height = 40;

从函数返回结构体:有时我们会写一个公共的函数来专门用来初始化结构体,来简化代码,对于维护和阅读更友好:

fn create_rect() -> Rect {      Rect {          width: 100,          height: 100      }      //#这里不写分号才会返回一个结构体变量,也可以使用return  }

这里要求 create_rect 方法与结构体定义处于同一作用域中,否则就需要公有化结构体并通过 use 引入了,当前我们默认此方法可以直接访问结构体。

简化写法,定义结构体变量时Rust还提供了一些便利的写法,使用与成员名称相同的变量可以直接复制,省去成员名称的重复书写:

let (width, height) = (20, 20);    let square = Rect{      width,      height  };

上面的例子中存在一个解包的写法,这样可以一次性定义一串相关变量,让代码看起来比较简介,下面两种写法是相同效果的:

let (mut width, mut height) = (20, 20);      let mut width = 20;  let mut height = 20;

结构体也同样有一个类似解包的操作。

拆解赋值使用情景:当存在一个或多个相同类型的结构体变量,基于已存在的结构体变量内数据创建新的结构体数据时。对于一些初始值总是相同的成员数据则可以使用这个结构体的语法来简化赋值操作:

let square = Rect{      width:30,      height:30  };    let rect_new = Rect {      ..square  };

可以想上面这样全部使用已有的结构体的值,可以使用部分的值:

let rect_new = Rect {      width: 40,      ..square  };    let rect_new1 = Rect {      height: 3,      ..square  };    //下面这种写法无意义,**(编译器不会报警告)**  //这里..square没有起到作用,  //这里只是证明是允许这样写的  let rect_new2 = Rect {      width: 3,      height: 3,      ..square  };

需要注意的是这个 ..<struct expr> 的语法必须写在结构体尾部,且仅能在这里出现一次,下面是错误的:

let rect_new = Rect {      ..square,      width: 30,  };    let rect_new1 = Rect {      width:30,      ..square,      ..square  };

这里 .. 运算符后方的表达式既可以是一个结构体变量,也可以是一个返回结构体变量的方法:

let rect_new = Rect {      height: 30,      ..create_rect()  };

三、元组结构体

元组的定义和使用在前面就已经介绍过了,而元组结构体与元组的区别就在于它提供了一个名称,对元组有了一个名称的定义,语义化更友好。

看下元组结构体如何定义:

struct RGB (u8, u8, u8);  struct EmptyStruct();  //同样支持空的元组结构体

这里通过元组结构体非常清晰的定义了一个RGB颜色的结构体,相比与直接使用元组来定义要更加易于阅读,语义明确。

创建元组结构体变量的写法没有什么特殊之处:

let red = RGB(255, 0, 0);    let yellow = RGB(255, 255, 0);    //可以使用一般结构体的形式创建变量。需要标明下标。  let green = RGB{0:0, 1:255, 2:0};

同样,元组结构体也是支持结构体拆解赋值的语法,这里和一般结构体的写法略有不同,虽然在创建变量使用的是小括号,但是在使用拆解时,必须使用大括号,在对部分值主动赋值时需要使用元组的下标进行赋值:

let some_color = RGB{..red};    let some_color1 = RGB {1: 23, ..red};    let some_color2 = RGB {0: 100, 2: 255, ..yellow};

元组结构体的读写与元组无异:

let mut black = RGB(1,1,1);    black.0 = 0;  black.1 = 0;  black.2 = 0;    println!("{}", black.0);

关注:在上面的某些例子里面在结构体上面紧挨着有一个注解 #[derive(Debug)] ,这个注解作用是为结构体实现Debug这个trait,效果就是可以通过debug模式( {:?} )打印结构体的内容,可以在自己实验的时候去掉注解或者去掉格式化模式看一下会有什么错误提示。

四、结构体内存

在C语言中,结构体的内存分配是连续的,从结构体的起始地址也就是第一个结构体成员的起始位置,依次向下排列,并且这里还有一个内存对齐的问题:

struct test_st{      char address;      double name[5];      char a;  };

C中的对齐规则:

  • 成员起始位置必须在自身类型大小的最小整数倍的偏移量上开始,不满足则补齐至指定的偏移位置。
  • 最终的内存大小需要满足是成员中占用字节最多的类型的字节数的最小整数倍,不满足则在尾部补齐空字节。

所以上面的例子:test_st结构体占用56字节。计算如下

对于char address位于起始,偏移量为0,变量自身长度1字节。

double name[5],double在C中占用8字节,address仅占用1字节,所以从偏移位置8开始name[5]的存储,于是address和name[5]之间多了很多空余的内存空间,name[5]自身占用8 * 5 = 40字节,当前总长40+8 = 48。

char a,char类型占用1字节,起始位置满足整数倍,自身占用1字节,当前总长48+1 = 49 。

所有成员中double类型最长,大于49的8的最小整数倍是56,所以补齐内存至56字节,在这56个字节内的偏移地址都是test_st的势力范围。

说回到Rust里面的结构体,看下面的例子:

use std::mem;    #[derive(Debug)]  struct Rect {      name: char,      width: u8,      height: [i64; 4],  }    fn main() {        let rect = Rect{width: 1, height:[1,2,3,4], name:'a'};        println!("mem rect:{}", mem::size_of_val(&rect));      println!("mem name:{}", mem::size_of_val(&(rect.name)));      println!("mem width:{}", mem::size_of_val(&(rect.width)));      println!("mem height:{}", mem::size_of_val(&(rect.height)));        println!("rect start {:p}", &rect);      println!("name:{:p}", &(rect.name));      println!("width:{:p}", &(rect.width));      println!("height:{:p}", &(rect.height));  }    --输出--  mem rect:40  mem name:4  mem width:1  mem height:32  rect start 0x7ffee84f9218  name:0x7ffee84f9238  width:0x7ffee84f923c  height:0x7ffee84f9218

Rust中char占用4字节,u8占用1字节,i64占用8字节

通过输出可以发现,Rust内也是会有内存对齐的规则,而且与C中的对齐规则相同。

唯一有区别的地方是Rust中还做了一个处理,就是会自动把结构体成员按照类型占用的字节进行排序,长度最大的类型放到结构体起始的位置,占用最小的放到结尾,如果占用字节相同则按照成员在结构体中定义的顺序排列。

占用字节最多的类型放在最前面的好处就是,结构体成员是可以紧密排列的,后面的类型均可以落在自身大小的整数倍的位置上,类型安排合理是可以完全避免空余数据的对齐。

扩展:这里没有使用String或者Vec类型作为例子,是因为它们实际较为复杂,String本质是一个结构体,内部是一个Vec<u8>类型,而Vec<T>类型在64位的机器上占用24字节,内部结构:

pub struct Vec<T> {      buf: RawVec<T>,      len: usize,  //64位机器占用8字节。  }

RawVec<T>的结构如下:

#[allow(missing_debug_implementations)]  pub struct RawVec<T, A: Alloc = Global> {      ptr: Unique<T>,      cap: usize,      a: A,  }

这里Unique<T>内部是

pub struct Unique<T: ?Sized> {      pointer: *const T,      // NOTE: this marker has no consequences for variance, but is necessary      // for dropck to understand that we logically own a `T`.      //      // For details, see:      // https://github.com/rust-lang/rfcs/blob/master/text/0769-sound-generic-drop.md#phantom-data      _marker: PhantomData<T>,  }

Unique<T>内部pointer是一个裸指针,占用8字节。

可以使用 std::mem::size_of::<* const u8>() 输出查看。

PhantomData<T>是一个空结构体,占用0字节。所以Unique<T>占用8字节。

所以RawVec<T>中的ptr占用8字节,cap在64位机器中占用8字节,成员a是一个实现了Alloc trait的泛型,默认是Global,Global是一个空结构体,占用0字节。

所以RawVec<T>占用8+8=16字节。

回到Vec<T>中则得出16 + 8 = 24字节。

因为String内部使用的Vec<u8>,所以String在64位机器中占用24字节。

pub struct String {      vec: Vec<u8>,  }

最后,结构体的成员对于一些引用类型比如 &str 和指针类型比如 Box<T> 在这里没有提到,以及结构体方法的实现都没有提到,后面在学习到生命周期、智能指针和trait、泛型的再回过头来一起学习一下。

留一个小问题: &str 类型通过mem::size_of::<&str>()进行打印内存,始终为16字节,感兴趣的同学可以研究一下~~