136 lines
4.0 KiB
Markdown
136 lines
4.0 KiB
Markdown
|
|
---
|
|||
|
|
tags: ["Go", "类型系统", "算法技巧", "避坑"]
|
|||
|
|
create time: 2026-05-15 16:00
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 03-StringByteRuneConversion - string 索引返回 byte 而非 rune
|
|||
|
|
|
|||
|
|
## 核心问题
|
|||
|
|
|
|||
|
|
在 Go 中,字符串的**两种访问方式**返回完全不同的类型:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
s := "ABC"
|
|||
|
|
|
|||
|
|
// ── 方式 1:range 迭代 → 返回 rune ──
|
|||
|
|
for _, char := range s {
|
|||
|
|
// char 的类型是 rune(int32)
|
|||
|
|
_ = char // rune ✅
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 方式 2:索引访问 → 返回 byte ──
|
|||
|
|
c := s[0]
|
|||
|
|
// c 的类型是 byte(uint8)!不是 rune
|
|||
|
|
_ = c // byte ✅ ← 这就是陷阱所在
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 为什么这个细节容易踩坑?
|
|||
|
|
|
|||
|
|
当你的哈希表用 `map[rune]int` 做 key 时:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
need := make(map[rune]int)
|
|||
|
|
have := make(map[rune]int)
|
|||
|
|
|
|||
|
|
for right, char := range s { // char 是 rune,✅ 可以直接当 key
|
|||
|
|
have[char]++
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
leftChar := s[left] // s[left] 是 byte ❌
|
|||
|
|
have[leftChar]-- // 编译错误:byte vs rune
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!danger] ⛔ 没有隐式转换
|
|||
|
|
>
|
|||
|
|
> Go 和 Python/JavaScript 不同,**byte 不能隐式转为 rune**。即使 byte 的值域完全包含在 rune 内,也必须显式转换:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// ❌ 编译错误:cannot use leftChar (variable of type byte) as rune value in map index
|
|||
|
|
have[s[left]]--
|
|||
|
|
|
|||
|
|
// ✅ 正确:显式转换
|
|||
|
|
leftChar := rune(s[left])
|
|||
|
|
have[leftChar]--
|
|||
|
|
|
|||
|
|
// ✅ 替代方案:统一用 byte 做 key(仅限 ASCII 场景)
|
|||
|
|
have := make(map[byte]int)
|
|||
|
|
have[s[left]]-- // byte ↔ byte,匹配 ✅
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 典型出错场景
|
|||
|
|
|
|||
|
|
### 滑动窗口中使用 `range` + `[index]` 混合访问
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func minWindow(s string, t string) string {
|
|||
|
|
need := make(map[rune]int) // key 是 rune
|
|||
|
|
have := make(map[rune]int) // key 是 rune
|
|||
|
|
|
|||
|
|
for right, char := range s { // range → rune,与 map key 一致
|
|||
|
|
have[char]++ // ✅ rune 作为 key
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 但收缩阶段需要按索引访问左边界字符:
|
|||
|
|
leftChar := rune(s[left]) // ← 必须加 rune() 转换!
|
|||
|
|
have[leftChar]-- // ✅
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**根源原因**:`range s` 给出的是完整 Unicode 码点(rune),而 `s[i]` 直接访问的是 Go 字符串的底层字节数组([]byte)。前者一次可能跳过多个字节(对于 UTF-8 多字节字符),后者永远只取一个字节。
|
|||
|
|
|
|||
|
|
## byte key vs rune key 如何选择?
|
|||
|
|
|
|||
|
|
| 场景 | 推荐 key 类型 | 原因 |
|
|||
|
|
|------|-------------|------|
|
|||
|
|
| **仅英文字母 / ASCII** | `map[byte]int` | 少一层转换,性能略优 |
|
|||
|
|
| **含中文 / Emoji / 多字节 Unicode** | `map[rune]int` | 能正确处理多字节字符 |
|
|||
|
|
| **通用安全写法** | `map[rune]int` | 无论输入什么都能工作 |
|
|||
|
|
|
|||
|
|
> [!tip] 🔑 选择建议
|
|||
|
|
>
|
|||
|
|
> LeetCode 题目几乎全部只涉及英文字母,所以两种都可以。但养成使用 `rune` 的习惯更安全——代码复用时不会因为遇到非 ASCII 字符而出错。
|
|||
|
|
|
|||
|
|
## 常见变体代码对比
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// ── 错误写法(漏了转换)──
|
|||
|
|
func buggy(s string) {
|
|||
|
|
m := make(map[rune]int)
|
|||
|
|
m[s[0]]++ // ❌ compile error: byte used as rune key
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 修正写法 A:加转换 ──
|
|||
|
|
func fixA(s string) {
|
|||
|
|
m := make(map[rune]int)
|
|||
|
|
m[rune(s[0])]++ // ✅ byte → rune 显式转换
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 修正写法 B:改用 byte key ──
|
|||
|
|
func fixB(s string) {
|
|||
|
|
m := make(map[byte]int)
|
|||
|
|
m[s[0]]++ // ✅ byte 对 byte,天然匹配
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 修正写法 C:全用 range,不索引 ──
|
|||
|
|
func fixC(s string) {
|
|||
|
|
m := make(map[rune]int)
|
|||
|
|
for _, c := range s {
|
|||
|
|
m[c]++ // ✅ range 返回 rune,直接用
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 检查清单
|
|||
|
|
|
|||
|
|
写涉及 `string` 索引的算法时自问:
|
|||
|
|
|
|||
|
|
- [ ] 我的 map key 是 `rune` 还是 `byte`?
|
|||
|
|
- [ ] `s[index]` 返回的是 `byte`
|
|||
|
|
- [ ] 如果 key 是 `rune`,是否加了 `rune(s[index])` 转换?
|
|||
|
|
- [ ] 如果使用 `range` 遍历,`char` 本身就是 `rune`,不需要额外转换
|
|||
|
|
|
|||
|
|
## 记忆口诀
|
|||
|
|
|
|||
|
|
> **`range s` 拿的是 rune,`s[i]` 抠出来是 byte。**
|
|||
|
|
> **rune 地图塞不进 byte,要转就要明说。**
|