如何在Rust中索引一个字符串

142

我试图在Rust中索引一个字符串,但编译器报错。我的代码(Project Euler问题4,playground):

fn is_palindrome(num: u64) -> bool {
    let num_string = num.to_string();
    let num_length = num_string.len();

    for i in 0 .. num_length / 2 {
        if num_string[i] != num_string[(num_length - 1) - i] {
            return false;
        }
    }
    
    true
}

错误:

error[E0277]: the trait bound `std::string::String: std::ops::Index<usize>` is not satisfied
 --> <anon>:7:12
  |
7 |         if num_string[i] != num_string[(num_length - 1) - i] {
  |            ^^^^^^^^^^^^^
  |
  = note: the type `std::string::String` cannot be indexed by `usize`

为什么无法索引String?那我如何访问数据呢?


1
这个答案可能会有帮助:https://dev59.com/OmEh5IYBdhLWcg3wtFRE - Austin Mullins
8个回答

179

在 Rust 中,不能像其他语言一样通过字符串索引来访问特定位置的字符。这是因为 Rust 的字符串在内部使用 UTF-8 编码,所以直接进行索引操作会导致二义性,而且人们容易误用:字节索引虽然快速,但几乎总是错误的(当文本包含非 ASCII 字符时,字节索引可能会让你停留在一个字符的中间,如果需要进行文本处理,这将非常糟糕);而字符索引不是免费的,因为 UTF-8 是一种可变长度编码,所以你必须遍历整个字符串才能找到所需的代码点。

如果你确定你的字符串仅包含 ASCII 字符,可以在 &str 上使用 as_bytes() 方法返回一个字节切片(byte slice),然后再对该切片进行索引。

let num_string = num.to_string();

// ...

let b: u8 = num_string.as_bytes()[i];
let c: char = b as char;  // if you need to get the character as a unicode code point

如果你需要索引代码点,你必须使用char()迭代器:

num_string.chars().nth(i).unwrap()

如我所述,这将需要遍历整个迭代器直到第i个代码元素。

最后,在许多文本处理的情况下,实际上需要使用图形群集(grapheme clusters)而不是代码点或字节。通过unicode-segmentation crate的帮助,您也可以索引到图形群集:

use unicode_segmentation::UnicodeSegmentation

let string: String = ...;
UnicodeSegmentation::graphemes(&string, true).nth(i).unwrap()

自然地,图形簇索引与索引到码点一样需要遍历整个字符串。


13
就翻译而言,FWIW,“String” 无法被索引。索引的移除仅适用于“&str”。 - huon
9
我认为现在,“char_at()”也已被移除...(rustc 1.23.0-nightly (79cfce3d3 2017-11-12)) - BitTickler
请注意,chars().nth(i) 是一个迭代器,因此该操作的时间复杂度为 O(n),而不是像使用向量索引一样的 O(1)。 - undefined

48

在 Rust 中处理这种情况的正确方法不是使用索引,而是使用迭代。主要问题在于 Rust 的字符串采用 UTF-8 编码,这是一种变长编码用于 Unicode 字符。由于长度可变,第 n 个字符的内存位置不能确定,需要查看整个字符串。这也意味着访问第 n 个字符的运行时间为 O(n)!

在这种特殊情况下,您可以迭代字节,因为已知字符串仅包含字符 0-9(迭代字符是更通用的解决方案,但效率略低些)。

这里有一些惯用代码来实现这个功能(playground):

fn is_palindrome(num: u64) -> bool {
    let num_string = num.to_string();
    let half = num_string.len() / 2;

    num_string.bytes().take(half).eq(num_string.bytes().rev().take(half))
}

我们同时正向(num_string.bytes().take(half))和反向(num_string.bytes().rev().take(half))遍历字符串中的字节; .take(half)部分用于减少工作量。然后,我们只需将一个迭代器与另一个迭代器进行比较,以确保在每一步中第n个和倒数第n个字节相等;如果相等,则返回true;否则返回false。


3
顺便提一下,String有一个直接的as_bytes方法。此外,你可以使用std::iter::order::equals,而不是allequals(iter.take(n), iter.rev().take(n)) - huon
2
顺便提一句,惯例是导入 std::iter::order 并调用 order::equals(..., ...)(我之所以在评论中没有这样做,是因为会很嘈杂)。 - huon

37

如果您正在寻找类似索引的东西,您可以在字符串上使用.chars().nth()


.chars() -> 返回一个字符串切片中char值的迭代器。

.nth() -> 返回迭代器中第n个元素作为一个Option


现在您可以以多种方式使用上述方法,例如:

let s: String = String::from("abc");
//If you are sure
println!("{}", s.chars().nth(x).unwrap());
//or if not
println!("{}", s.chars().nth(x).expect("message"));

9
需要注意的是,Chars::nth(n)会消耗n个字符,而不仅仅是简单的索引。正如文档所述,在同一迭代器上多次调用nth(0)将返回不同的元素。 - D. Scott Boggs
1
如果您确实不确定第N个字符是否存在,则使用 expect()unwrap() 并不能防止恐慌。代码仍将发生 panic,但 expect 将提供自定义的 panic 消息。请参见:https://dev59.com/21IH5IYBdhLWcg3wFYx1 - A248

22
你可以将一个String&str转换为一个字符的vec,然后索引该vec

例如:

fn main() {
    let s = "Hello world!";
    let my_vec: Vec<char> = s.chars().collect();
    println!("my_vec[0]: {}", my_vec[0]);
    println!("my_vec[1]: {}", my_vec[1]);
}

这里有一个实时的示例


3
性能如何?我认为字符串字节被复制了。 - prehistoricpenguin

4

由于以下原因,字符串索引不被允许(请查看本书):

  • 不清楚索引值应该是什么:一个字节、一个字符或者一个语言符号簇(在常识中称为字母
  • 字符串是使用UTF-8编码的字节向量(u8),而UTF-8是一种可变长度编码,即每个字符可以占用不同数量的字节 - 从1到4个。因此,通过索引获取字符或语言符号簇需要对整个字符串进行遍历(平均和最坏情况下的时间复杂度均为O(n)),以确定字符或语言符号簇的有效字节边界。

因此,如果输入不包含变音符号(被视为单独的字符)并且可以将字母近似为字符,则可以使用chars()迭代器和DoubleEndedIterator特征进行双指针处理:

    fn is_palindrome(num: u64) -> bool {
        let s = num.to_string();
        let mut iterator = s.chars();
        loop  {
            let ch = iterator.next();
            let ch_end = iterator.next_back();
            
            if ch.is_none() || ch_end.is_none() {
                break;
            }
            if ch.unwrap() != ch_end.unwrap() {
                return false
            }
        }
        true
    }

1

在Rust中索引不起作用的原因有两个:

在 Rust 中,字符串存储为一组 UTF-8 编码的字节。在内存中,字符串只是一组 1 和 0。程序需要能够解释这些 1 和 0,并打印出正确的字符。这就是编码的作用所在。
fn main(){
    let sample:String=String::from("2bytesPerChar")
    // 在其他高级编程语言中,我们可以这样做。但在Rust中会报错,无法通过整数索引获取字符。
    let c:char=sample[0]
}

字符串是字节的集合。那么“2bytesPerChar”字符串的长度是多少呢?因为某些字符可能由 1 到 4 个字节组成。假设第一个字符有 2 个字节。如果您想要通过索引获得字符串中的第一个字符,则可以使用 hello[0],这将指定第一个字符串的唯一一半。

  • 另一个原因是单词在 Unicode 中有三种相关方式表示: 字节标量值图形群集。如果我们使用索引,Rust 就不知道我们将会收到什么。字节、标量值还是图形群集。因此,我们必须使用更具体的方法。

如何访问字符串中的字符

  • 返回字节

       for b in "dsfsd".bytes(){
           // bytes方法返回一个字节集合,这里我们正在迭代每个字节并将其打印出来
           println!("{}",b)
       }
    
  • 返回标量值:

   // we could iterate over scalar values using char methods
   for c in "kjdskj".chars(){
       println!("{}",c)
   }
  • 返回字形值:

为了保持 Rust 标准库的精简,迭代字形簇的能力不是默认包含在内的。我们需要导入一个 crate。

// in cargo.toml
   [dependencies]
   unicode-segmentation="1.7.1"

然后:

   use unicode_segmentation::UnicodeSegmentation;
   // we pass true to get extended grapheme clusters
   for g in "dada"graphemes(true){
       println!("{}",g)
   }


1

这并不适用于所有情况,但如果你只需要引用前一个字符(或者经过一些修改,下一个字符),那么可以在不迭代整个字符串的情况下实现。

场景是有一个字符串片段(str slice),字符串中出现了模式(pattern)。我想知道模式之前的字符是什么。

调用 prev_char 方法,像这样 prev_char(string.as_bytes(), pattern_index),其中 pattern_index 是模式在字符串中第一个字节的索引。

UTF-8 编码是明确定义的,通过向后移动直到找到起始字节之一(高位为0或位11),然后将1-4字节的 [u8] 切片转换为字符串。

此代码简单地解包它,因为模式最初就存在于有效的 UTF-8 字符串中,所以不会出现错误。如果你的数据还没有经过验证,最好返回一个结果而不是一个 Option。

enum PrevCharStates {
    Start,
    InEncoding,
}

fn prev_char(bytes: &[u8], starting_index: usize) -> Option<&str> {
    let mut ix = starting_index;
    let mut state = PrevCharStates::Start;

    while ix > 0 {
        ix -= 1;
        let byte = bytes[ix];
        match state {
            PrevCharStates::Start => {
                if byte & 0b10000000 == 0 {
                    return Some(std::str::from_utf8(&bytes[ix..starting_index]).unwrap());
                } else if byte & 0b11000000 == 0b10000000 {
                    state = PrevCharStates::InEncoding;
                }
            },
            PrevCharStates::InEncoding => {
                if byte & 0b11000000 == 0b11000000 {
                    return Some(std::str::from_utf8(&bytes[ix..starting_index]).unwrap());
                } else if byte & 0b11000000 != 0b10000000 {
                    return None;
                }
            }
        }
    }
    None
}

1
这个函数可以以稍微不同的签名编写,如 string[..index].chars().next_back() (playground)。 - kmdreko
谢谢。我对Rust还比较新手,每天都学到新东西。 - bmacnaughton

1
以下代码运行良好,但性能和O复杂度尚不确定,希望有人能提供更多关于此解决方案的信息。
fn is_palindrome(num: u64) -> bool {
    let num_string = String::from(num.to_string());
    let num_length = num_string.len();
    for i in 0..num_length / 2 {
        let left = &num_string[i..i + 1];
        let right = &num_string[((num_length - 1) - i)..num_length - i];
        if left != right {
            return false;
        }
    }
    true
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接