quinnwencn / blog

Apache License 2.0
0 stars 0 forks source link

[RustLearning] Slice的使用 #32

Open quinnwencn opened 1 month ago

quinnwencn commented 1 month ago

Slice是如何定義的,又該如何使用?Slice有哪些常用的方法?

quinnwencn commented 1 month ago

定義

Cpp中沒有切片的概念,在python等語言中則較為常見。我們假設s是一個數組(字符串數組也可以),以s為例子說明在Rust中切片的定義方式:

常用方法

Rust中,切片有很多方法支持,但是我這裡只列舉常用的幾個,後續用到的再補充:

pub fn sublist(_first_list: &[T], _second_list: &[T]) -> Comparison { let first_len = _first_list.len(); let second_len = _second_list.len(); match first_len.cmp(&second_len) { std::cmp::Ordering::Less => { if _first_list.len() == 0 || _second_list.windows(_first_list.len()).any(|window| window == _first_list) { Comparison::Sublist } else { Comparison::Unequal } }, std::cmp::Ordering::Equal => { if _first_list == _second_list { Comparison::Equal } else { Comparison::Unequal } }, std::cmp::Ordering::Greater => { if _second_list.len() == 0 || _first_list.windows(_second_list.len()).any(|window| window == _second_list) { Comparison::Superlist } else { Comparison::Unequal } }, } }


題解中,通過長度較長的切片使用windows方法,以短的切片長度為輸入,生成多個windows切片,然後通過any裡匹配,是否有其中一個切片window和短的切片匹配。
quinnwencn commented 1 month ago

普通的數組或者vec的切片,我們都可以用切片來訪問切片中的元素。但是String類型的切片,我們只能用chars()去訪問,但是有一個例外,如果一個String的所有元素都是ASCII碼,那麼我們可以將這個切片轉換成bytes,然後就可以用下標的方式去訪問了。 我們來看Exercism上的這一道掃雷題目: Minesweeper,如果不使用切片去實現,那麼我們就要先拷貝一份切片, 存儲成vec,然後再分別掃描每一個位置是否是雷,並計算地雷的數目,這也是我的解法:

pub fn annotate(strings: &[&str]) -> Vec<String> {
    let mut result = Vec::new();
    let rows = strings.len();
    if rows == 0 {
        return result;
    }

    let cols = strings[0].len();
    if cols == 0 {
        result.push("".to_string());
        return result;
    }

    let grid: Vec<Vec<char>> = strings
        .iter()
        .map(|s| s.chars().collect())
        .collect();

    for i in 0..rows {
        let mut line = String::new();
        for j in 0..cols {
            if grid[i][j] == '*' {
                line.push('*');
                continue;
            }

            let mut count = 0;
            for di in -1..=1 {
                for dj in -1..=1 {
                    let (x, y) = (i as isize + di, j as isize + dj);
                    if x >= 0 && x < rows as isize && y >=0 && y < cols as isize {
                        if grid[x as usize][y as usize] == '*' {
                            count += 1;
                        }
                    }
                }
            }

            line.push(if count == 0 {' '} else {char::from_digit(count, 10).unwrap()});
        }
        result.push(line);
    }

    result
}

[package]
edition = "2021"
name = "minesweeper"
version = "1.1.0"

但是,社區中,其他人的優秀解法都是使用了切片轉bytes的方式,並結合match機制非常優雅地解決了這個問題,我們看其中一個:

static NEIGBOURHOOD_OFFSETS: &'static [(i32, i32)] = &[
    (-1, -1), (0, -1), (1, -1),
    (-1,  0),          (1,  0),
    (-1,  1), (0,  1), (1,  1),
];

pub fn annotate(field: &[&str]) -> Vec<String> {
    let height = field.len() as i32;
    (0..height).map(|y| {
        let width = field[y as usize].len() as i32;
        (0..width).map(|x| {
            if field[y as usize].as_bytes()[x as usize] == b'*' {
                '*'
            } else {
                match NEIGBOURHOOD_OFFSETS.iter()
                    .map(|&(ox, oy)| (x + ox, y + oy))
                    .filter(|&(x, y)| (0 <= x && x < width) && (0 <= y && y < height))
                    .filter(|&(x, y)| field[y as usize].as_bytes()[x as usize] == b'*')
                    .count() {
                        0 => ' ',
                        n => (n as u8 + '0' as u8) as char
                    }
            }
        }).collect()
    }).collect()
}

這可以說是match的完美理解了!Solution by kstep

quinnwencn commented 1 month ago

當然,這個優秀的解法也有一些需要注意的地方:

  1. 閉包默認是引用捕捉,因此filter中的pair都是引用,為了在使用中直接使用,而不通過*號解引用,我們直接在參數加上引用符號
  2. 但是,閉包中的參數如果是Copy trait類型,就會轉換成複製,比如u8等類型或者自己實現/繼承了Copy trait
  3. char::from_digit參數是u32,而count等結果是一個usize,因此需要轉換類型
quinnwencn commented 1 month ago

slice还有一个有用的方法:first,first方法返回一個Option,如果是空則是None,否則就是Some(list),這對於使用match來處理切片非常方便。比如Exercism上的這道Proverb,一開始我使用for循環來冩,其實還是C++的味道:

pub fn build_proverb(list: &[&str]) -> String {
    let mut proverb = String::new();
    if list.len() == 0 {
        return proverb;
    }
    let tail = list.len() - 1;
    for i in 0..list.len() {
        match i {
            t if t == tail  => proverb.push_str(&format!("And all for the want of a {}.", list[0])),
            _ => proverb.push_str(&format!("For want of a {} the {} was lost.\n", list[i], list[i + 1])),
        }
    }

    proverb
}

[package]
edition = "2021"
name = "proverb"
version = "1.1.0"

[dependencies]

但是如果使用first和match來完成,就可以使用迭代器來完成:

use std::iter::once;

pub fn build_proverb(list: &[&str]) -> String {
    match list.first() {
        None => String::new(),
        Some(word) => list.windows(2)
            .map(|w| format!("For want of a {} the {} was lost.\n", w[0], w[1]))
            .chain(once(format!("And all for the want of a {}.", word)))
            .collect(),
    }
}

這裡首先match list.first(),如果切片爲空,則直接返回空String,否則則將切片劃分windows,由於打印是打印兩個,因此我們劃分爲兩個一組的windows,然後將這些輸出使用chain將兩個迭代器組合,也就是將String拼接。

quinnwencn commented 3 weeks ago

windws这个迭代器方法,虽然可以直接用于slice,但是并不能用于&str上,原因应该是&str不能按下标索引,不过&str可以转化为bytes,如果确认都是ascii的话,然后再使用windows。 可以参考Exercism的这道题Series,我的解法如下:

pub fn series(digits: &str, len: usize) -> Vec<String> {
    if len > digits.len() {
        return vec![];
    }
    digits.as_bytes()
        .windows(len)
        .map(|w| std::str::from_utf8(w).unwrap().to_string())
        .collect()
}

可以看到,&str转为bytes后,如果结果仍然是&str,那么可以使用std::str::from_utf8将bytes转为&str,题中结果是String,因此还需要再使用str的to_string进行一次转换。 需要注意的是,as_bytes以及std::str::from_utf8都不会申请内存,都是在原有的slice的地址进行转换和检查,但是to_string则会申请一次内存。