Unicode、ASCII、UTF-8区别与联系

发布时间 2023-07-25 13:06:48作者: weikang_zeng

概念梳理

ASCII 字符集

ASCII:ASCII 是美国标准信息交换码的缩写,是一种基于拉丁字母的字符编码标准。ASCII 使用 7 位二进制数(也就是 0-127)来表示每个字符,因此它最多可以表示 128 个不同的字符。ASCII 是所有现代字符编码方案的基础。

ASCII 字符编码其实只使用了 7 位,取值范围是 0-127。原因在于,ASCII 是在很早的计算机系统中设计的编码系统,那时的计算机系统资源非常有限。使用 7 位可以表示 128 个不同的字符,这已经足够表示所有的英文字母(大小写)、数字、常见的标点符号以及一些控制字符了。

然而,计算机中的数据通常以字节(8 位)为最小单位进行处理,所以每个 ASCII 字符通常还会有一个额外的 0 位,使得它们的实际编码在二进制表示下有 8 位。这个额外的位在 ASCII 中并没有被使用,所以有些系统会使用这个额外的位来存储额外的信息(比如校验位)。

在 ASCII 之后,有很多扩展的字符编码标准被开发出来,这些标准通常会使用这个额外的位来增加可表示的字符数量。例如,ISO 8859-1 (也被称为 Latin-1)就是一种这样的编码,它使用全部的 8 位,因此可以表示 256 个不同的字符。同样,UTF-8 也是一种可以表示超过 ASCII 范围的字符的编码。

总的来说,ASCII 只使用 7 位是因为历史原因,当时的计算机资源有限,而 7 位已经足够表示所有的基本字符。随着计算机技术的发展,现在的编码标准已经可以表示更多的字符了。

Unicode 字符集

Unicode:Unicode 并不是一种编码,而是一种字符集(Character Set)。它旨在包含全世界所有的字符。在 Unicode 中,每个字符都对应一个唯一的数字,这个数字称为这个字符的 Unicode 码点(Code Point)。Unicode 码点的范围是 0-1,114,111(0x10FFFF)。

因为Unicode兼容ASCII,所以 0 ~ 127 这些 ASCII 编码也可以称为是Unicode码点。例如“Hello”对应的ASCII编码为“72 101 108 108 111”,这些数字既可以被看作是 ASCII 编码,也可以被看作是 Unicode 码点。只不过,在 ASCII 编码中,它们表示的是字节序列,在 Unicode 中,它们表示的是字符。

字符和字节序列的区别

"字符"和"字节序列"这两个概念描述的是数据的两种不同的视角。

  1. 字符:字符是书写系统中的最小单位,代表了一个语义上的单位,如字母、数字、标点符号和其他符号等。例如,英文字母 'a',汉字 '中',标点符号 ',' 都是字符。字符的表述和语言、文化等有关。

  2. 字节序列:字节序列是计算机用于存储和操作数据的方式。计算机内部并不直接理解字符的概念,它们只能处理数字。因此,为了在计算机中使用字符,我们需要某种方式将字符映射到数字上,这就是字符编码的任务。一个字符在计算机中可能被编码为一个或者多个字节,这个或者这些字节的组合就构成了该字符的字节序列。

    • Unicode码点对应的字符可以被UTF-8编码为字节序列。例如:字符 "世界" 对应的Unicode码点是 “19990” 和 “30028” ,它们在 UTF-8 编码下的字节序列为 "228 184 150 231 149 140"。

    • 当我们在计算机中处理字符时,通常需要在字符(和它们对应的 Unicode 码点)与它们的字节序列之间进行转换。

对比这两个概念,我们可以看到他们描述的是同一件事情的不同侧面。字符是面向人类的,关注的是语义;而字节序列则是面向计算机的,关注的是如何存储和传输数据。

举一个例子,假设我们要在计算机中处理字符 'A'。在 ASCII 编码中,'A' 对应的是数字 65,因此,它在计算机中就被存储为一个字节,这个字节的值就是 65。因此,我们可以说,字符 'A' 在计算机中的字节序列是 65。

但如果我们要处理的是字符 '中',情况就变得复杂了。'中' 不在 ASCII 的范围内,我们需要使用能够表示更多字符的编码,比如 UTF-8。在 UTF-8 编码中,'中' 对应的字节序列是 "228 184 173"。这说明,虽然 '中' 只是一个字符,但在计算机中,我们需要三个字节来表示它。

UTF-8 编码方案

UTF-8:UTF-8 是 Unicode 的一种具体实现(即一种编码方式),它是一种变长的编码方式,每个字符可以使用 1 到 4 个字节来表示。UTF-8 的好处是它向后兼容 ASCII:也就是说,所有的 ASCII 字符在 UTF-8 中的表示和 ASCII 中完全一样,这使得许多只能处理 ASCII 的软件无需修改就可以处理 UTF-8。

UTF-8 是一种用于编码 Unicode 字符的方式。它的特点是变长,也就是说每个字符可能由一个、两个、三个甚至四个字节组成。它是以字节为单位进行编码的,因此每个字节的取值范围是 0-255。ASCII 编码的字符在 UTF-8 中仍然保持不变(也就是说,0-127 的值在 UTF-8 中有同样的意义),但 UTF-8 可以编码更多的字符。

例如 "世界" 这两个汉字对应的Unicode码点(字符)是 "19990" 和 "30028" ,然后经过UTF-8转换后得到字节序列 "228 184 150 231 149 140" 。因此,UTF-8 编码实际上是一种将字符(在这里,具体来说就是 Unicode 码点)转换为字节序列的方法。

UTF-8 编码最为常见,但还有其他的编码实现,比如 UTF-16 和 UTF-32。

总结

ASCII 和 Unicode 是两种字符集,它们定义了字符与数字之间的对应关系;而 UTF-8 是一种编码方案,它定义了如何把 Unicode 码点转化为一串字节,并从字节串中恢复出原来的 Unicode 码点。

字符编码的基本原理

当我们说 "Unicode 码点" 的时候,我们是在谈论一个抽象的概念:在 Unicode 字符集中,每个字符都对应一个唯一的数字。这个数字就是这个字符的 Unicode 码点。

然而,当我们把这些字符存储在计算机中,或者在网络上进行传输时,我们不能直接使用这些码点,因为这些码点可能会占用很多的空间(Unicode 码点的范围是 0 到 1,114,111),并且不利于处理(例如,如何表示一个 Unicode 码点的结束?)。所以我们需要一种方式来"编码"这些 Unicode 码点,这就是 UTF-8 的作用。

UTF-8 是一种把 Unicode 码点编码为字节序列的方式,这种方式具有一些优良的特性,比如向后兼容 ASCII,以及具有自同步的能力(也就是说,如果我们在一个 UTF-8 编码的字节流中任意一点开始解码,我们都可以正确地找到字符的边界)。

比如,"世" 这个字符的 Unicode 码点是 19990,但是在 UTF-8 编码中,它被编码为三个字节:228,184,150。这三个字节才是我们在计算机中实际存储和处理的内容。同样,"界" 这个字符的 Unicode 码点是 30028,但是在 UTF-8 编码中,它被编码为三个字节:231,149,140。

Unicode 码点的存在主要是为了给每个字符提供一个唯一的标识,不依赖于任何具体的编码方式,可以被人类理解,并且方便在不同的编码方式之间进行转换。

"19990 30028" 这两个 Unicode 码点是 "世界" 这两个字符的唯一标识。它们不依赖于任何具体的编码方式,不管是 UTF-8、UTF-16 还是其他的编码方式,这两个字符的 Unicode 码点都是 "19990 30028"。

然而,当我们需要在计算机中存储这两个字符,或者在网络上进行传输的时候,我们需要选择一种具体的编码方式,比如 UTF-8,来把这两个 Unicode 码点转化为一串字节。在 UTF-8 编码下,"19990 30028" 就会被转化为 "228 184 150 231 149 140"。

所以,"19990 30028" 这两个 Unicode 码点的存在主要是为了方便我们理解和操作字符,而 "228 184 150 231 149 140" 这串字节则是实际存储和传输数据时使用的编码。

Go语言中的byte类型和rune类型

在 Go 语言中,byterune 是两种特殊的类型,它们在处理字符时有一些关键的区别:

  1. byte:这是一个别名,本质上是 uint8 类型。byte 类型用于处理 ASCII 字符,因为 ASCII 字符的编码 (0~127) 都是在 0-255 之间,这恰好一个 byte 能够表示。这样,每一个 byte 就对应一个 ASCII 字符。

  2. runeruneint32 的别名。rune 类型用于处理 Unicode 字符。Unicode 是一种可以表示全世界所有字符的编码方案,包括 ASCII 在内的各种语言的字符。由于 Unicode 字符的编码范围比 ASCII 字符的编码范围要大得多,因此需要更多的位来表示,这就是为什么 rune 是基于 int32 的。

在处理字符时,这两者的区别主要体现在它们能够处理的字符范围上。如果只需要处理 ASCII 字符,那么使用 byte 就足够了。但如果需要处理包括各种国家语言在内的 Unicode 字符,那么就需要使用 rune

此外,在 Go 语言中使用 for range 遍历字符串时,Go 语言会自动对 UTF-8 编码的字符串进行解码,然后给我们提供每个字符的 Unicode 码点,得到的实际上是 rune 类型的字符。这是因为 Go 语言内置对 Unicode 的支持,使我们可以非常容易地处理 Unicode 字符。如果我们用普通的 for 循环遍历字符串,得到的就是原始的 UTF-8 编码的字节,实际上是 byte 类型的字符。

以下是一个简单的例子来说明这两者的区别:

package main

import "fmt"

func main() {
    s := "Hello, 世界"

    fmt.Println(len(s))

    for i := 0; i < len(s); i++ {
        fmt.Printf("%v ", s[i])  // 这里得到的是 byte 类型的字符
    }

    fmt.Println()

    for _, r := range s {
        fmt.Printf("%v ", r)  // 这里得到的是 rune 类型的字符
    }
}

// 输出结果
13                                                // len方法显示的是字节长度
72 101 108 108 111 44 32 228 184 150 231 149 140  // 显示的都是 UTF-8 编码的字节序列
72 101 108 108 111 44 32 19990 30028              // 前面一部分显示的是字节序列,后面显示的是Unicode码点,便于我们理解,计算机处理时还是会将其转换为字节序列

可以看到 for range 循环能够正确地处理 Unicode 字符(比如 "世" 和 "界"),而普通的 for 循环则不能正确处理。

总的来说,byterune 类型都是 Go 语言中处理字符的重要工具,它们之间的区别主要取决于你需要处理的字符的编码范围。