rust学习 -- 第六章 数组和字符串

382 阅读10分钟

第六章 数组和字符串

数组

数组是一个容器,他在块连续空间内存中存储了一系列同样类型的数据. 数组中的元素的占用空间大小必须是编译器确定的, 数组本身所容纳元素的个数也必须是编译期确定的.执行阶段不可变.变长容器使用 Vec/LinkedList.

[T; n] // T是类型, n是数量fn main() {
    let xs: [i32; 5] = [1,2,3,4,5];
    let ys: [i32; 500] = [0; 500]; // 所有元素相同时 的简化写法
    println!("some {}", ys[100]);
}

在Rust中,对于两个数组类型,只有元素类型和元素个数都完全相同,这两个数组才是同类型的.数组与指针之间不能隐式转换.同类型的数组之间可以互相赋值.把数组xs作为参数传给一个函数,这个数组并不会退化成一个指针.而是会将这个数组完整复制进这个函数.函数体内对数组的改动不会影响到外面的数组.

多维数组

let v: [[i32; 3]; 3] = [[0,0], [0,0], [0,0]];

数组切片

对数组取借用borrow操作,可以生成一个“数组切片”(Slice).数组切片对数组没有“所有权”,我们可以把数组切片看作专门用于指向数组的指针.比如,我们有一个数组[T;n],它的借用指针的类型就是&[T;n].它可以通过编译器内部魔法转换为数组切片类型&[T]

fn main() {
    fn mut_array(a: &mut [i32]) {
        a[2] = 5;
    }
​
    println!("size of &[i32; 3]: {:?}", std::mem::size_of::<&[i32; 3]>());
    println!("size of &[i32]  :{:?}", std::mem::size_of::<&[i32]>());//和指针大小相同.
​
    let mut v: [i32; 3] = [1,2,3];
    { 
        let s: &mut [i32; 3] = &mut v;
        mut_array(s);
    }
    println!("{:?}", v);
}

DST和胖指针

切片比单纯的指针多一个长度信息. 切片有一个别名叫胖指针.与这个概念相对应的概念是“动态大小类型”(Dynamic Sized Type,DST). 所谓的DST指的是编译阶段无法确定占用空间大小的类型.为了安全性,指向DST的指针一般是胖指针.

&[T] 类型占用了两个指针大小的内存空间, 一个是数组地址,一个是数组长度大小.

对于DST类型,Rust有如下限制:

  • 只能通过指针来间接创建和操作DST类型,&[T]Box<[T]>可以, [T]不可以;
  • 局部变量和函数参数的类型不能是DST类型,因为局部变量和函数参数必须在编译阶段知道它的大小因为目前unsized rvalue功能还没有实现;
  • enum中不能包含DST类型,struct中只有最后一个元素可以是 DST,其他地方不行,如果包含有DST类型,那么这个结构体也就成了 DST类型

Range

代表一个区间, 一个范围. 内置支持 start..end(左闭右开)

fn main() {
   let r = 1..10;
   for i in r {
       print!("{:?}\t", i);
   }
}

..生成的是个std::ops::Range<_>类型的变量.标准库定义为(居然是个结构体)

pub struct Range<Idx> {
    pub start: Idx,
    pub end: Idx
}

这个类型是实现了 Iterator trait.因此可以直接用到循环中.

100 递减到 10

fn main() {
    use std::iter::Iterator;
    // 先用rev方法反转, 然后用map 乘以10.  =>> 100 90 80 70
    let r = (1i32..11).rev().map(|i| i * 10);
​
    for i in r {
        print!("{:?}\t", i);
    }
}

其他的Range 类型(std::ops - Rust (rust-lang.org)):

std::ops::RangeFrom 只有起始值, 没有结束值. start.. ==> [start ....

std::ops::RangeTo 只有结束值, 没有起始值. ..end ==> ....end)

std::ops::RangeFull 使用全部值. .. ==> [start..end]

如果我们希望产生一个i32类型的从0到 i32::MAX的范围,为按语法,我们应该写0.. (i32::MAX+1),然而(i32::MAX+1)已经溢出了. 此时就需要一个闭区间, 使用..=.

对应的类型是

std::ops::RangeInclusive 语法为 start..=end

std::ops::RangeToInclusive语法为 ..=end

let arr = [0, 1, 2, 3, 4];
assert_eq!(arr[ ..  ], [0, 1, 2, 3, 4]); // This is the `RangeFull`     $$$
assert_eq!(arr[ .. 3], [0, 1, 2      ]); // This is a `RangeTo`         $$$
assert_eq!(arr[ ..=3], [0, 1, 2, 3   ]); // This is a `RangeToInclusive` 
assert_eq!(arr[1..  ], [   1, 2, 3, 4]); // This is a `RangeFrom`
assert_eq!(arr[1.. 3], [   1, 2      ]); // This is a `Range`
assert_eq!(arr[1..=3], [   1, 2, 3   ]); // This is a `RangeInclusive`  $$$

上面几个标记$$$的类型不能作为迭代器使用.

字符串

rust字符串使人困扰的原因

  • rust倾向于暴露可能的错误
  • 字符串数据结构复杂
  • rust中的字符串是utf-8

什么是字符串

byte类型的集合, 并且提供一些方法,能将byte解析为文本.

在rust的核心语言层面,只有一个字符串类型:字符串切片str(或&str)

字符串切片: 对存储在其他地方, utf-8编码的字符串的引用. 字符串字面值: 存储在二进制文件中, 也是字符串切片.

这里要讲的是String 类型:

此类型来自标准库而不是核心语言层面.存储在堆上可以增长、可修改、可拥有.使用utf-8编码.

其他类型的字符串

rust标准库中包含了很多其他的字符串类型,例如: OsString、OsStr、CString、CStr

  • 有String 和Str 后缀的,分别代表String拥有所有权Str借用的变体
  • 可以存储不同编码的文本或在内存中以不同的形式展现.

library crate针对存储字符串可提供更多的选项

字符串字面量是切片

let s: &str = "Hello, world!";

&str

字符串有两种类型, 一种是&str 一种是 String, String拥有所有权.

str是Rust的内置类型. &str是对str的借用.Rust的字符串内部默认是使用utf-8编码格式的. 字符串底层是一个用u8类型的数组.实际上str类型有一种方法: fn as_ptr(&self)-> *const u8. 它内部无须做任何计算,只需做一个强制 类型转换即可

self as *const str as *const u8

Rust 中的 字符 (注意是字符)是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4) ,这样有助于大幅降低字符串所占用的内存空间.

rust 这种设计, 在查找字符串中的一个字符的时间是 O(n)的.并不是 O(1)的时间复杂度.他需要从头开始遍历找到所需的字符串.

&str 也是一个胖指针.

fn main() {
    println!("Size of pointer:{}", std::mem::size_of::<*const ()>());
    println!("Size of &str   :{}", std::mem::size_of::<&str>());
}

String

String 的特点是它具有字符串的所有权, 有管理内存空间的权力.很多Vec<T>的操作都可用于String.创建方法

String::new()  // 函数

使用初始值来创建String:

to_string() 方法 ,可用于实现了Display trait 的类型,包括字符串字面值.

fn main() {
    let data = "initial contents";
    let s = data.to_string();
    let s1 = "initial contents".to_string();
}

String::from()函数,从字面值创建String

fn main() {
    let s = String::from("initial contents")
}

&str的问题是无法直接修改字符串

let greeting: &str = "hello"  // 无法添加字符串, 无法扩展他的存储空间.
fn main() {
    let mut s = String::from("hello");
    s.push(' ');  // 可以添加字符
    s.push_str("World.");
    println!("{}", s);
}
fn capitalize(substr: &mut str) {
    substr.make_ascii_uppercase();  // 可以改变字符串中的值
}
​
fn main() {
    let mut s = String::from("Hello World");
    capitalize(&mut s);  // 将&mut String  转成了 &mut str
    println!("{}", s);
}

String 与 &str的转换

&str ==> String

String::from("hello, world")
"hello, world".to_string()

String ==> &str

let s = String::from("hello, world!");
say_hello(&s);
say_hello(&s[..]);
say_hello(s.as_str());

字符串索引

就如上面说的, 对字符串的索引是对底层[u8] 数组的索引, 要十分注意每个字符的字节大小是不同的, 如果从中间切分一个字符, 会导致无法正确显示, 程序报错. 比如一个中文占三个字节, 取两个字节就会报错, (中英文混合就有问题...)

let s = "一个字符串";
let s2 = &s[..2]; // 报错!

字符串操作

追加

在字符串尾部可以使用 push() 方法追加字符 char,也可以使用 push_str() 方法追加字符串字面量. 这两个方法都是在原有字符串上追加, 不会返回新的字符串. 原字符串需要是mut.

fn main() {
    let mut s = String::from("hello ");
    s.push('r');
    s.push_str("ust!");
}

插入

insert() 方法插入单个字符 char, insert_str() 方法插入字符串字面量. 需要输入两个参数, 第一个参数是插入的位置.

fn main() {
    let mut s = String::from("hello rust!");
    s.insert(5, ',');
    s.insert_str(6, " I like");
}

替换

  1. replace

    适用于String 和 &str, 接收两个参数, 第一个是要被替换的字符串, 第二个是新的字符串. 该方法会替换所有匹配到的字符串, 返回新的字符串, 而不是修改原有的字符串.

    fn main() {
        let string_replace = String::from("I like rust. Learning rust is my favorite!");
        let new_string_replace = string_replace.replace("rust", "RUST");
    }
    
  2. replacen

    适用于String 和 &str, 接收三个参数, 第一个是要被替换的字符串, 第二个是新的字符串. 第三个是替换的个数.返回一个新的字符串,而不是操作原来的字符串.

    fn main() {
        let string_replace = "I like rust. Learning rust is my favorite!";
        let new_string_replacen = string_replace.replacen("rust", "RUST", 1);
    }
    
  3. replace_range

    该方法仅适用于 String 类型. replace_range 接收两个参数,第一个参数是要替换字符串的范围(Range),第二个参数是新的字符串. 该方法是直接操作原来的字符串,不会返回新的字符串. 该方法需要使用 mut 关键字修饰.

删除

删除的方法有4个, pop(),remove(),truncate(),clear(). 这四个方法仅适用于 String 类型.

  1. pop 删除并返回字符串的最后一个字符

    返回值是一个Option, 字符串为空时为None

  2. remove 删除并返回字符串中指定位置的字符(注意是字符)

    指定的是一个字符的起始位置,比如一个中文字符串

    let s = String::from("一个字符串");
    s.remove(0);  // 第一个字符的起始位置
    s.remove(2);  // 这里会报错, 因为2不是一个字符的起始位置
    s.remove(3);  // 第二个字符的起始位置
    
  3. truncate 删除字符串中从指定位置开始到结尾的全部字符.

    无返回值. 直接操作原字符串.

  4. clear 清空字符串.

    直接操作原字符串

拼接

使用 + 或者 += 连接字符串,要求右边的参数必须为字符串的切片引用(Slice)类型.其实当调用 + 的操作符时,相当于调用了 std::string 标准库中的 add() 方法,这里 add() 方法的第二个参数是一个引用的类型.因此我们在使用 +, 必须传递切片引用类型.不能直接传递 String 类型. ++= 都是返回一个新的字符串.所以变量声明可以不需要 mut 关键字修饰.

fn main() {
    let string_append = String::from("hello ");
    let string_rust = String::from("rust");
    let result = string_append + &string_rust;  // string_append的所有权被转移到result
    let mut result = result + "!";
    result += "!!!";
}

使用format!

适用于 String&str

fn main() {
    let s1 = "hello";
    let s2 = String::from("rust");
    let s = format!("{} {}!", s1, s2);
}

字符串转义

我们可以通过转义的方式 `` 输出 ASCII 和 Unicode 字符.

fn main() {
    // 通过 \ + 字符的十六进制表示,转义输出一个字符
    let byte_escape = "I'm writing \x52\x75\x73\x74!";
    println!("What are you doing\x3F (\x3F means ?) {}", byte_escape);
​
    // \u 可以输出一个 unicode 字符
    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);
}

操作 UTF-8 字符串

如果你想要以 Unicode 字符的方式遍历字符串,最好的办法是使用 chars 方法,例如:

for c in "中国人".chars() {
    println!("{}", c);
}

以字节的方式返回字符串

for b in "中国人".bytes() {
    println!("{}", b);
}

想要准确的从 UTF-8 字符串中获取子串是较为复杂的事情,例如想要从 holla中国人नमस्ते 这种变长的字符串中取出某一个子串,使用标准库你是做不到的. 你需要在 crates.io 上搜索 utf8 来寻找想要的功能.

对String按索引的形式进行访问

按索引的语法访问String的某部分,会报错!!!!

&s1[0]

rust的字符串不支持索引语法的访问

String的内部表示: String是对Vec<u8>的包装,并且他有一个len方法,返回String的字节数.

fn main() {
    let len = String::from("something").len()
​
    println!("{}", len)
}

rust中看待字符串有三种方式 字节(bytes)、标量值(scalar values)、字形簇(grapheme clusters)

字节:查看的是字符串每一个字节存储的整数值.

标量值: 这是unicode中的概念,描述的是一个unicode字符的表示

字形簇:最接近我们看到的字符,但是rust 的标准库是不支持的.

fn main() {
    let w = "一个字符串";
    println!("{}", w.len()); // 15个字节
​
    for b in w.bytes() {
        println!("{}", b); // 228 184 128 228 184 170 229 173 151 231 172 166 228 184 178
        // 每个中文字符占用3个字节
    }
}
fn main() {
    let w = "一个字符串";
​
    for b in w.chars() {
        println!("{}", b);
    }
}

rust 不允许对String进行索引的最后一个原因是:

  • 索引操作应该消耗常量的时间(O(1))
  • 而String无法保证这一点,它需要遍历所有内容,来确定有多少合法的字符.