Skip to content

对比 Go、Python 和 Rust 三种语言中字符串索引操作

March 23, 2022 | 11:01 AM

字符串有很多实现细节,如果详细介绍每个细节,一篇文章的篇幅显然是不够的。 这篇文章不会面面俱到地讲述细节,而是聚焦于三种不同的语言对字符串遍历和索引的处理逻辑。

假设我们现在有这样的一个字符串 "Hello, 世界",我们首先在三种语言中做遍历和求字符串长度操作。

# Python
st = "Hello, 世界"
for item in st:
    print(item)
print(len(st))
# Output:
# H
# e
# l
# l
# o
# ,
#
# 世
# 界
# 9

Python 的操作逻辑符合我们直觉,"Hello, 世界" 一共有 9 个字符。

// Go
package main

import "fmt"

func main() {
    st := "Hello, 世界"
    for _, item := range st {
        fmt.Println(item)
    }
    fmt.Println(len(st))
    fmt.Println("-----")

    // 把字符串转换为字节数组
    for _, item := range []byte(st) {
        fmt.Println(item)
    }
}

/* Output:
72
101
108
108
111
44
32
19990
30028
13
-----
72
101
108
108
111
44
32
228
184
150
231
149
140
*/

Go 这边和 Python 差距比较大,输出的不是字符而是数字,而且字符串的长度竟然是 13,这是什么情况?首先把问题放到一边,继续看 Rust 的情况。

// Rust
fn main() {
    let st = "Hello, 世界";
    for item in st.chars() {
        println!("{}", item);
    }
    println!("{}", st.len());

    println!("----------");

    for item in st.bytes() {
        println!("{}", item);
    }
    println!("{}", st.len());
}
/* Output:
H
e
l
l
o
,



13
----------
72
101
108
108
111
44
32
228
184
150
231
149
140
13
*/

Rust 不同于上面的两种语言,遍历字符串有两种方式:chars()bytes()。 输出的结果分别与 Python 和 Go 相似,但是长度是 13。从方法名称和实际的输出效果可以推断出,Python 遍历字符串默认输出的是字符串中的字符,而 Go 默认输出的是字符串中每个字符对应的字节编码。 从结果推断,可以得到下面的对应关系:

字符字节
H72
e101
l108
l108
o111
,44
空格32
228,184,150 (19990)
231, 149,140 (30028)

如果你足够熟悉 ASCII 码的话,你会发现空格之前的所有字符对应的字节其实就是 ASCII。 而中文“世界” 比较特殊,每个字竟然对应三个字节。这是什么情况呢?

提到这个问题,就不得不提一下字节编码。计算机只能通过 0 1 二进制串的方式来存储数据,字符串也同样如此。 为了存储字符,一个直观的方式就是通过映射,一个字符对应一个键,该键用数字表示(参考 ASCII 码)。但是,现实世界存在多种语言:英语、中文、日语和韩语等等。 所有的语言包含的单词数量要远远超过一个字节所能表达的范围(0-255),为了准确存储字符,UTF-8 编码标准应运而生。与 ASCII 不同,UTF-8 采用四个字节来存储字符, 为了节省空间和确保与 ASCII 兼容,采用一种可变长的方式,存储英文单词用 1 个字节,而中文用 3 个字符。 Python、Rust 和 Go 默认使用 UTF-8 编码来存储字符串。

在 Python 中可以通过 encode() 查看每个字符的 UTF-8 编码,以我们的“世界”为例:

print("世".encode())
print("界".encode())
# b'\xe4\xb8\x96' -> 十进制为:228,184,150 = 19990
# b'\xe7\x95\x8c' -> 十进制为:231,149,140 = 30028

果然”世界“每个字对应三个字节,这样一来,长度是 13 也说得通了。

那为什么 228,184,150 可以转换为 19990 呢? 这就涉及到了 UTF-8 的编码细节,可以参考延伸阅读的第二篇文章。

从遍历字符串的结果可以看出,Python 把字节编码细节封装起来,默认每个字符串的单位是字(无论实际存储该字需要多个字节)。 而 Go 和 Rust 将编码细节暴露给用户。

既然这样,如果我想访问字符串中的某个元素,比如 bt[8],会发生什么呢?

# Python
st = "Hello, 世界"
print(st[8])
# Output:
# 界

Python 直接返回了第 8 个字符“界”。

// go
func main() {
    st := "Hello, 世界"
    fmt.Println(st[8])
}
// Output:
// 184

Go 返回了存储字符串的字节数组的第 8 个字节 184

// Rust
fn main() {
    let st = "Hello, 世界";
    println!("{}", st[8usize]);  
}

// Output
// error[E0277]: the type `str` cannot be indexed by `{usize}`

而 Rust 直接报错,表示不能直接通过下标索引的方式来访问字符串元素。

Rust 为了避免出现像 Go 中索引字符串却得到一个数字的“奇怪”现象,杜绝了下标索引访问字符串的可能。

结论

同样的索引字符串,不同语言采取了不同的逻辑来处理:


延伸阅读

Strings and Character Data in Python

一文看懂ASCII,UNICODE,UTF8编码规则