Rust基础学习–(二) 四、所有权与借用 1、所有权原则
Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
当所有者(变量)离开作用域(rust的作用域与c相同)范围时,这个值将被丢弃(drop)
2、引用(借用)与解引用 &
符号即是引用,它们允许你使用值,但是不获取所有权,因此,不可通过解引用来修改变量值。
1 2 3 4 5 6 7 fn main () { let x = 5 ; let y = &x; assert_eq! (5 , x); assert_eq! (5 , *y); }
这就是不可变引用,我们可以通过加上mut关键字的方式改为可变引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 fn main () { let s = String ::from ("hello" ); change (&s); }fn change (some_string: &String ) { some_string.push_str (", world" ); }fn main () { let mut s = String ::from ("hello" ); change (&mut s); }fn change (some_string: &mut String ) { some_string.push_str (", world" ); }
不过可变引用并不是随心所欲、想用就用的,它有一个很大的限制: 同一作用域,特定数据只能有一个可变引用 。
另外,可变引用与不可变引用不能同时存在 。
【Non-Lexical Lifetimes(NLL) :新版rust的一个特性,用于找到某个引用在作用域(}
)结束前就不再被使用的代码位置,见下例。
1 2 3 4 5 6 7 8 9 10 11 12 fn main () { let mut s = String ::from ("hello" ); let r1 = &s; let r2 = &s; println! ("{} and {}" , r1, r2); let r3 = &mut s; println! ("{}" , r3); }
】
【垂悬引用导致的报错:
1 2 3 4 5 6 7 8 9 10 11 12 fn main () { let reference_to_nothing = dangle (); }fn dangle () -> &String { let s = String ::from ("hello" ); &s }
】
总的来说,借用规则 如下:
同一时刻,你只能拥有要么一个可变引用,要么任意多个不可变引用
引用必须总是有效的
五、复合类型 1、字符串与切片 切片:
它允许你引用集合中部分连续的元素序列,而不是引用整个集合。
对于字符串而言,切片就是对 String
类型中某一部分的引用,是使用方括号包括的一个序列:[开始索引..终止索引],它看起来像这样:
1 2 3 4 5 let s = String ::from ("hello world" );let hello = &s[0 ..5 ];let world = &s[6 ..11 ];
【在对字符串使用切片语法时需要格外小心,切片的索引必须落在字符之间的边界位置,也就是 UTF-8 字符的边界,例如中文在 UTF-8 中占用三个字节,下面的代码就会崩溃:
1 2 3 let s = "中国人" ;let a = &s[0 ..2 ];println! ("{}" ,a);
因为我们只取 s
字符串的前两个字节,但是本例中每个汉字占用三个字节,因此没有落在边界处,也就是连 中
字都取不完整,此时程序会直接崩溃退出,如果改成 &s[0..3]
,则可以正常通过编译。 因此,当你需要对字符串做切片索引操作时,需要格外小心这一点。】
字符串:(String)
<1>简介:Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4),这样有助于大幅降低字符串所占用的内存空间。
Rust 在语言级别,只有一种字符串类型: str
,它通常是以引用类型出现 &str
,也就是上文提到的字符串切片。虽然语言级别只有上述的 str
类型,但是在标准库里,还有多种不同用途的字符串类型,其中使用最广的即是 String
类型。
str
类型是硬编码进可执行文件,也无法被修改,但是 String
则是一个可增长、可改变且具有所有权 的 UTF-8 编码字符串,当 Rust 用户提到字符串时,往往指的就是 String
类型和 &str
字符串切片类型,这两个类型都是 UTF-8 编码。
除了 String
类型的字符串,Rust 的标准库还提供了其他类型的字符串,例如 OsString
, OsStr
, CsString
和 CsStr
等,注意到这些名字都以 String
或者 Str
结尾,分别对应的是具有所有权和被借用的变量。
<2>String与&str的转化:
1 2 3 4 5 6 7 8 9 10 11 12 String ::from ("hello,world" )"hello,world" .to_string ()fn main () { let s = String ::from ("hello,world!" ); say_hello (&s); say_hello (&s[..]); say_hello (s.as_str ()); }fn say_hello (s: &str ) { println! ("{}" ,s); }
<3>rust不允许通过索引访问字符串某一个元素,因为Rust 提供了不同的字符串展现方式,不同语言符号占用的空间不同。但是允许字符串切片,但仅在切片恰好有意义的时候允许,否则会崩溃。
<4>字符串操作:修改,添加,删除
追加:在字符串尾部可以使用 push()
方法追加字符 char
,也可以使用 push_str()
方法追加字符串字面量。这两个方法都是在原有的字符串上追加,并不会返回新的字符串 。由于字符串追加操作要修改原来的字符串,则该字符串必须是可变的,即字符串变量必须由 mut
关键字修饰 。eg:
1 2 3 4 5 6 7 8 9 fn main () { let mut s = String ::from ("Hello " ); s.push_str ("rust" ); println! ("追加字符串 push_str() -> {}" , s); s.push ('!' ); println! ("追加字符 push() -> {}" , s); }
插入:可以使用 insert()
方法插入单个字符 char
,也可以使用 insert_str()
方法插入字符串字面量,与 push()
方法不同,这俩方法需要传入两个参数,第一个参数是字符(串)插入位置的索引,第二个参数是要插入的字符(串),索引从 0 开始计数,如果越界则会发生错误。由于字符串插入操作要修改原来的字符串 ,则该字符串必须是可变的,即字符串变量必须由 mut
关键字修饰 。
1 2 3 4 5 6 7 fn main () { let mut s = String ::from ("Hello rust!" ); s.insert (5 , ',' ); println! ("插入字符 insert() -> {}" , s); s.insert_str (6 , " I like" ); println! ("插入字符串 insert_str() -> {}" , s); }
替换:共三个方法
1)replace
该方法可适用于 String
和 &str
类型。replace()
方法接收两个参数,第一个参数是要被替换的字符串,第二个参数是新的字符串。该方法会替换所有匹配到的字符串。该方法是返回一个新的字符串,而不是操作原来的字符串 。
1 2 3 4 5 fn main () { let string_replace = String ::from ("I like rust. Learning rust is my favorite!" ); let new_string_replace = string_replace.replace ("rust" , "RUST" ); dbg!(new_string_replace); }
2)replacen
该方法可适用于 String
和 &str
类型。replacen()
方法接收三个参数,前两个参数与 replace()
方法一样,第三个参数则表示替换的个数。该方法是返回一个新的字符串,而不是操作原来的字符串 。
1 2 3 4 5 fn main () { let string_replace = "I like rust. Learning rust is my favorite!" ; let new_string_replacen = string_replace.replacen ("rust" , "RUST" , 1 ); dbg!(new_string_replacen); }
3)replace_range
该方法仅适用于 String
类型。replace_range
接收两个参数,第一个参数是要替换字符串的范围(Range),第二个参数是新的字符串。该方法是直接操作原来的字符串,不会返回新的字符串。该方法需要使用 mut
关键字修饰 。
1 2 3 4 5 fn main () { let mut string_replace_range = String ::from ("I like rust!" ); string_replace_range.replace_range (7 ..8 , "R" ); dbg!(string_replace_range); }
删除:共四个方法
1)pop
—— 删除并返回字符串的最后一个字符
该方法是直接操作原来的字符串 。但是存在返回值,其返回值是一个 Option
类型,如果字符串为空,则返回 None
。
1 2 3 4 5 6 7 8 fn main () { let mut string_pop = String ::from ("rust pop 中文!" ); let p1 = string_pop.pop (); let p2 = string_pop.pop (); dbg!(p1); dbg!(p2); dbg!(string_pop); }
2)remove
—— 删除并返回字符串中指定位置的字符
该方法是直接操作原来的字符串 。但是存在返回值,其返回值是删除位置的字符串,只接收一个参数,表示该字符起始索引位置。remove()
方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 fn main () { let mut string_remove = String ::from ("测试remove方法" ); println! ( "string_remove 占 {} 个字节" , std::mem::size_of_val (string_remove.as_str ()) ); string_remove.remove (0 ); dbg!(string_remove); }
3)truncate
—— 删除字符串中从指定位置开始到结尾的全部字符
该方法是直接操作原来的字符串 。无返回值。该方法 truncate()
方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。
1 2 3 4 5 fn main () { let mut string_truncate = String ::from ("测试truncate" ); string_truncate.truncate (3 ); dbg!(string_truncate); }
4)clear
—— 清空字符串
该方法是直接操作原来的字符串 。调用后,删除字符串中的所有字符,相当于 truncate()
方法参数为 0 的时候。
1 2 3 4 5 fn main () { let mut string_clear = String ::from ("string clear" ); string_clear.clear (); dbg!(string_clear); }
连接:共三个方法
1)使用 +
或者 +=
连接字符串
使用 +
或者 +=
连接字符串,要求右边的参数必须为字符串的切片引用(Slice)类型。其实当调用 +
的操作符时,相当于调用了 std::string
标准库中的 add()
方法,这里 add()
方法的第二个参数是一个引用的类型。因此我们在使用 +
时, 必须传递切片引用类型。不能直接传递 String
类型。+
是返回一个新的字符串,所以变量声明可以不需要 mut
关键字修饰 。
1 2 3 4 5 6 7 8 9 10 fn main () { let string_append = String ::from ("hello " ); let string_rust = String ::from ("rust" ); let result = string_append + &string_rust; let mut result = result + "!" ; result += "!!!" ; println! ("连接字符串 + -> {}" , result); }
2)add()
以下是add方法的定义:
1 fn add (self , s: &str ) -> String
1 2 3 4 5 6 7 8 9 10 fn main () { let s1 = String ::from ("hello," ); let s2 = String ::from ("world!" ); let s3 = s1 + &s2; assert_eq! (s3,"hello,world!" ); }
3)使用 format!
连接字符串
format!
这种方式适用于 String
和 &str
。format!
的用法与 print!
的用法类似。
1 2 3 4 5 6 7 fn main () { let s1 = "hello" ; let s2 = String ::from ("rust" ); let s = format! ("{} {}!" , s1, s2); println! ("{}" , s); }
<5>字符串转义:
我们可以通过转义的方式 \
输出 ASCII 和 Unicode 字符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 fn main () { let byte_escape = "I'm writing \x52\x75\x73\x74!" ; println! ("What are you doing\x3F (\\x3F means ?) {}" , byte_escape); let unicode_codepoint = "\u{211D}" ; let character_name = "\"DOUBLE-STRUCK CAPITAL R\"" ; println! ( "Unicode character {} (U+211D) is called {}" , unicode_codepoint, character_name ); let long_string = "String literals can span multiple lines. The linebreak and indentation here ->\ <- can be escaped too!" ; println! ("{}" , long_string); }fn main () { println! ("{}" , "hello \\x52\\x75\\x73\\x74" ); let raw_str = r"Escapes don't work here: \x3F \u{211D}" ; println! ("{}" , raw_str); let quotes = r#"And then I said: "There is no escape!""# ; println! ("{}" , quotes); let longer_delimiter = r###"A string with "# in it. And even "##!"### ; println! ("{}" , longer_delimiter); }
<6>操作UTF-8字符
char()
1 2 3 4 5 6 7 for c in "中国人" .chars () { println! ("{}" , c); }
bytes()
1 2 3 4 5 6 7 8 9 10 11 12 13 for b in "中国人" .bytes () { println! ("{}" , b); }
2、元组 元组是由多种类型组合到一起形成的,因此它是复合类型,元组的长度是固定的,元组中元素的顺序也是固定的。
创建:
1 2 3 fn main () { let tup : (i32 , f64 , u8 ) = (500 , 6.4 , 1 ); }
可以使用模式匹配或者 .
操作符来获取元组中的值。
1 2 3 4 5 6 7 8 9 10 11 fn main () { let tup = (500 , 6.4 , 1 ); let (x, y, z) = tup; println! ("The value of y is: {}" , y); }fn main () { let x : (i32 , f64 , u8 ) = (500 , 6.4 , 1 ); let five_hundred = x.0 ; let six_point_four = x.1 ; let one = x.2 ; }
3、结构体 <1>和c语言语法基本相同
1 2 3 4 5 6 7 8 9 10 11 12 13 struct User { active: bool , username: String , email: String , sign_in_count: u64 , } let mut user1 = User { email: String ::from ("someone@example.com" ), username: String ::from ("someusername123" ), active: true , sign_in_count: 1 , }; user1.email = String ::from ("anotheremail@example.com" );
(简化:构建结构体的时候,当函数参数和结构体字段同名时,可以直接使用缩略的方式进行初始化。
eg:
1 2 3 4 5 6 7 8 9 fn build_user (email: String , username: String ) -> User { User { email, username, active: true , sign_in_count: 1 , } }
更新结构体时,与原结构体相同的部分可以的通过结构体更新语法 ..user1(原结构体名)
即可完成。
1 2 3 4 5 let user2 = User { email: String ::from ("another@example.com" ), ..user1 };
)
<2>结构体的内存排列与访问
1 2 3 4 5 6 7 8 9 10 11 12 fn main () { let f1 = File { name: String ::from ("f1.txt" ), data: Vec ::new (), }; let name = f1.name; println! ("{}" , name); println! ("data length: {}" , f1.data.len ()); }
<3>元组结构体
结构体必须要有名称,但是结构体的字段可以没有名称,这种结构体长得很像元组,因此被称为元组结构体,这种方式在希望有一个整体名称,但是又不关心里面字段的名称时将非常有用。
1 2 3 4 struct Color (i32 , i32 , i32 );struct Point (i32 , i32 , i32 );let black = Color (0 , 0 , 0 );let origin = Point (0 , 0 , 0 );
<4>单元结构体
如果你定义一个类型,但是不关心该类型的内容,只关心它的行为时,就可以使用 单元结构体
,即没有内含变量的结构体。
1 2 3 4 5 struct AlwaysEqual ;let subject = AlwaysEqual;impl SomeTrait for AlwaysEqual { }
<5>结构体数据所有权
结构体要么使用具有自身所有权的变量,要么在从其他对象借用数据时加上生命周期符(见后续生命周期部分)
<6>使用#[derive(Debug)]来打印结构体的信息
结构体没有实现Display特征,因此不可以直接使用{}打印
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #[derive(Debug)] struct Rectangle { width: u32 , height: u32 , }fn main () { let rect1 = Rectangle { width: 30 , height: 50 , }; println! ("rect1 is {:?}" , rect1); }
除了使用println!()外,还可以使用dbg!宏进行输出。
(dbg!
输出到标准错误输出 stderr
,而 println!
输出到标准输出 stdout
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #[derive(Debug)] struct Rectangle { width: u32 , height: u32 , }fn main () { let scale = 2 ; let rect1 = Rectangle { width: dbg!(30 * scale), height: 50 , }; dbg!(&rect1); }
4、枚举 eg:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 enum PokerSuit { Clubs, Spades, Diamonds, Hearts, }fn main () { let heart = PokerSuit::Hearts; let diamond = PokerSuit::Diamonds; print_suit (heart); print_suit (diamond); }fn print_suit (card: PokerSuit) { println! ("{:?}" ,card); }
枚举成员可以包含各种数据,如:
1 2 3 4 5 6 7 8 9 10 11 12 enum Message { Quit, Move { x: i32 , y: i32 }, Write (String ), ChangeColor (i32 , i32 , i32 ), }fn main () { let m1 = Message::Quit; let m2 = Message::Move{x:1 ,y:1 }; let m3 = Message::ChangeColor (255 ,255 ,0 ); }
Quit
没有任何关联数据
Move
包含一个匿名结构体
Write
包含一个 String
字符串
ChangeColor
包含三个 i32
【用Option枚举处理空值:
(代替其他语言中的NULL)
定义:
1 2 3 4 enum Option <T> { Some (T), None , }
其中 T
是泛型参数,Some(T)
表示该枚举成员的数据类型是 T
,换句话说,Some
可以包含任何类型的数据。
Option<T>
枚举是如此有用以至于它被包含在了prelude(prelude 属于 Rust 标准库,Rust 会将最常用的类型、函数等提前引入其中,省得我们再手动引入)之中,你不需要将其显式引入作用域。另外,它的成员 Some
和 None
也是如此,无需使用 Option::
前缀就可直接使用 Some
和 None
option的匹配:
1 2 3 4 5 6 7 8 9 10 11 fn plus_one (x: Option <i32 >) -> Option <i32 > { match x { None => None , Some (i) => Some (i + 1 ), } }let five = Some (5 );let six = plus_one (five);let none = plus_one (None );
plus_one
接受一个 Option<i32>
类型的参数,同时返回一个 Option<i32>
类型的值(这种形式的函数在标准库内随处所见),在该函数的内部处理中,如果传入的是一个 None
,则返回一个 None
且不做任何处理;如果传入的是一个 Some(i32)
,则通过模式绑定,把其中的值绑定到变量 i
上,然后返回 i+1
的值,同时用 Some
进行包裹。
】
5、数组 在日常开发中,使用最广的数据结构之一就是数组,在 Rust 中,最常用的数组有两种,第一种是速度很快但是长度固定的 array
,第二种是可动态增长的但是有性能损耗的 Vector
,在本书中,我们称 array
为数组,Vector
为动态数组。
<1>array:
三要素:
创建:
1 2 3 4 5 6 7 fn main () { let a = [1 , 2 , 3 , 4 , 5 ]; let a : [i32 ; 5 ] = [1 , 2 , 3 , 4 , 5 ]; let a = [3 ; 5 ]; let array : [String ; 8 ] = std::array::from_fn (|_i| String ::from ("rust is good!" ));println! ("{:#?}" , array); }
访问:
1 2 3 4 5 6 7 8 fn main () { let a = [9 , 8 , 7 , 6 , 5 ]; let first = a[0 ]; let second = a[1 ]; }
数组切片:
1 2 3 let a : [i32 ; 5 ] = [1 , 2 , 3 , 4 , 5 ];let slice : &[i32 ] = &a[1 ..3 ];assert_eq! (slice, &[2 , 3 ]);
切片的长度可以与数组不同,并不是固定的,而是取决于你使用时指定的起始和结束位置
创建切片的代价非常小,因为切片只是针对底层数组的一个引用
切片类型 [T] 拥有不固定的大小,而切片引用类型 &[T] 则具有固定的大小,因为 Rust 很多时候都需要固定大小数据类型,因此 &[T] 更有用,&str
字符串切片也同理
<2>vector:
动态数组(具体见集合类型,须先学习其他知识)
To Be Continue…