关于本文
本文通过确认 .NET 的 Encoding API 与 UTF-8 规范,并在 PowerShell 中观察字符数与字节数的区别来进行整理。验证状态:📘 已确认官方规范・PowerShell 实机未验证
由于 Shift_JIS 系列 code page 的可用性在不同的 PowerShell/.NET 环境中存在差异,文中也包含了无法获取时的处理方式。
“因为是 10 个字符,所以是 10 个字节”这并不总是成立。字符串在保存或通信之前会通过 Encoding 转换成字节序列,而字节数是由字符与 Encoding 的组合决定的。
这种差异在处理固定长数据、CSV、API 以及与旧业务系统进行对接时非常重要。
字符串与字节序列是两码事
flowchart LR
A[文字列] --> B{Encoding}
B -->|UTF-8| C[UTF-8 byte列]
B -->|code page 932| D[Shift_JIS系 byte列]
C --> E[byte数]
D --> F[byte数]
在 PowerShell/.NET 中,可以使用 GetByteCount 来统计该 Encoding 所生成的字节数。
通过 UTF-8 进行确认
$texts = @(
'ABC',
'日本',
'A日本',
'😀'
)
$utf8 = [System.Text.Encoding]::UTF8
foreach ($text in $texts) {
[pscustomobject]@{
Text = $text
StringSize = $text.Length
UTF8Bytes = $utf8.GetByteCount($text)
}
}
对于 ASCII 范围内的字符、日语以及表情符号,即使是外观上相同的“1 个字符”,其 UTF-8 字节数也不尽相同。
String.Length 也不一定等于“人眼看到的字符数”
.NET 的 String 被处理为 UTF-16 code unit 序列。因此,对于补充平面(Supplementary Planes)等字符,外观上的 1 个字符与 String.Length 可能会不一致。
flowchart TB
A[見た目の文字] --> B[.NET String]
B --> C[UTF-16 code units]
B --> D[Encodingでbyte列へ]
C --> E[String.Length]
D --> F[GetByteCount]
E -. 同じ概念ではない .- F
重要的是不要将其过度简化为“字符数”和“字节数”这两种简单的对应关系。
与 Shift_JIS 系列 code page 932 进行比较
在 PowerShell 7 / .NET 中,某些环境需要注册 CodePagesEncodingProvider 才能使用 legacy code page。
try {
$sjis = [System.Text.Encoding]::GetEncoding(932)
}
catch {
[System.Text.Encoding]::RegisterProvider(
[System.Text.CodePagesEncodingProvider]::Instance
)
$sjis = [System.Text.Encoding]::GetEncoding(932)
}
$utf8 = [System.Text.Encoding]::UTF8
foreach ($text in @('ABC', '日本', 'A日本')) {
[pscustomobject]@{
Text = $text
UTF8Bytes = $utf8.GetByteCount($text)
SJISBytes = $sjis.GetByteCount($text)
}
}
即使是相同的字符串,Encoding 发生变化,字节序列也会随之改变。
切勿断言“如果是 Shift_JIS,日语就是 2 个字节”
有些字符不包含在 code page 中,也有一些字符是用单字节(single-byte)表示的。此外,如果将 Windows 的 code page 932 与“Shift_JIS”这一名称当作完全相同的规范来草率对待,会在细节上产生误解。 本文将其作为在 PowerShell/.NET 中获取并比较 code page 932 的实验来进行探讨。
为什么在固定长数据中会出现问题
在旧系统集成中,字段宽度有时不是按“字符”定义的,而是按字节单位定义的。
项目A: 10 bytes 项目B: 20 bytes 项目C: 8 bytes
如果简单地用 Substring 截取字符串后再保存,有时可能会超出预期的字节数。
flowchart LR
A[入力文字列] --> B[文字数で切る]
B --> C[Encoding]
C --> D{指定byte幅以内?}
D -->|No| E[固定長レコードがずれる]
D -->|Yes| F[次の項目へ]
反过来,如果在中途粗暴地截断字节序列,可能会将多字节字符(multi-byte character)从中间切断。在处理固定长数据时,必须将“使用哪种 Encoding 以及有多少字节”作为规范来进行确认。
需要将 BOM 和换行符分开考虑
Encoding.GetByteCount(string) 计算的是该字符串进行 Encoding 时的字节数。
另一方面,实际的文件大小可能还会包含以下内容:
BOM
CRLF / LF 等换行符
分隔符
整个文件的 header
请不要将“字符串本身的字节数”与“保存后的文件大小”混为一谈。
通过实际文件进行确认
保存字符串后,顺便比较一下实际的文件大小会更容易理解。
$text = '日本'
$path = '.utf8-sample.txt'
[System.IO.File]::WriteAllText($path, $text, [System.Text.UTF8Encoding]::new($false))
$bytes = [System.IO.File]::ReadAllBytes($path)
[pscustomobject]@{
TextByteCount = [System.Text.Encoding]::UTF8.GetByteCount($text)
FileByteCount = $bytes.Length
}
这里显式指定了无 BOM 的 UTF-8,因此可以减少多余条件的干扰来进行比较。
总结
字符串会根据 Encoding 被转换为字节序列
String.Length、外观上的字符数以及字节数并不是同一个概念
即使是相同的字符串,UTF-8 和 code page 932 的字节数也可能不同
固定长数据不应看字符数,而应看规范中的 Encoding 和字节宽度
包含 BOM 或换行符的文件大小,应与字符串本身的 GetByteCount 分开考虑
官方资料与第一手信息
Microsoft Learn — Encoding.GetByteCount
https://learn.microsoft.com/en-us/dotnet/api/system.text.encoding.getbytecount
Microsoft Learn — Encoding.GetEncoding
https://learn.microsoft.com/en-us/dotnet/api/system.text.encoding.getencoding
Microsoft Learn — CodePagesEncodingProvider
https://learn.microsoft.com/en-us/dotnet/api/system.text.codepagesencodingprovider
RFC 3629 — UTF-8
https://www.rfc-editor.org/rfc/rfc3629.html

