中文字元在預設編碼 (GB2312/Big5) 佔 2 位元組,英文佔 1 位元組。直接用 Substring 依字元數截取會導致顯示寬度不一致。本文提供依**位元組長度**截取的完整實作,支援指定起始位元組位移與截取長度。
0x4E00~0x9FA5)startIdx 開始記錄,達到 startIdx + length 停止
using System;
using System.Text;
public static class MixedStringHelper
{
/// 判斷是否為 CJK 統一表意文字 (基本區)
public static bool IsChinese(char c) => c >= 0x4E00 && c <= 0x9FA5;
/// 取得字串在預設編碼下的位元組長度
public static int GetByteLength(string str) => Encoding.Default.GetByteCount(str);
///
/// 依位元組長度截取中英混合字串
///
/// 來源字串
/// 起始位元組索引 (0-based)
/// 欲截取的位元組長度
/// 截取結果
public static string SubstringByByteLength(string str, int startByteIndex, int byteLength)
{
if (string.IsNullOrEmpty(str)) return string.Empty;
if (startByteIndex < 0) startByteIndex = 0;
if (byteLength <= 0) return string.Empty;
int totalBytes = GetByteLength(str);
if (startByteIndex >= totalBytes) return string.Empty;
var sb = new StringBuilder();
int currentBytePos = 0;
int takenBytes = 0;
bool started = false;
foreach (char c in str)
{
int charWidth = IsChinese(c) ? 2 : 1;
// 檢查是否進入截取範圍
if (!started && currentBytePos + charWidth > startByteIndex)
{
started = true;
}
if (started)
{
// 檢查加入此字元是否超過長度限制
if (takenBytes + charWidth > byteLength)
{
// 若是中文字元且只剩 1 位元組空間,無法完整放入,直接結束
break;
}
sb.Append(c);
takenBytes += charWidth;
}
currentBytePos += charWidth;
// 優化:已超出範圍且已開始截取,可提早結束
if (started && currentBytePos > startByteIndex + byteLength)
break;
}
return sb.ToString();
}
}
var test = "gisoracle歡迎你gisoracle"; // 英文 10 + 中文 3*2 + 英文 10 = 26 bytes
Console.WriteLine($"原始: [{test}] ({MixedStringHelper.GetByteLength(test)} bytes)");
// 從位元組 0 開始取 10 bytes → "gisoracle" (10 英文)
Console.WriteLine($"[0,10]: [{MixedStringHelper.SubstringByByteLength(test, 0, 10)}]");
// 從位元組 1 開始取 10 bytes → "isoracle歡" (9 英文 + 1 中文 = 11 bytes? 實際邏輯見下)
Console.WriteLine($"[1,10]: [{MixedStringHelper.SubstringByByteLength(test, 1, 10)}]");
// 從位元組 2 開始取 10 bytes
Console.WriteLine($"[2,10]: [{MixedStringHelper.SubstringByByteLength(test, 2, 10)}]");
| 參數 | 位元組範圍 | 結果 | 說明 |
|---|---|---|---|
| (0, 10) | 0-9 | gisoracle | 前 10 個英文字元 |
| (1, 10) | 1-10 | isoracle歡 | 跳過 'g',取 9 英文 + 1 中文 (2 bytes) → 實際 11 bytes,但邏輯在「超過長度前」加入完整字元 |
| (2, 10) | 2-11 | isoracle歡 | 同理 |
若需依 UTF-8 位元組長度截取 (中文 3 bytes,英文 1 byte):
public static string SubstringByUtf8ByteLength(string str, int startByteIndex, int byteLength)
{
if (string.IsNullOrEmpty(str)) return string.Empty;
var bytes = Encoding.UTF8.GetBytes(str);
if (startByteIndex >= bytes.Length) return string.Empty;
int end = Math.Min(startByteIndex + byteLength, bytes.Length);
// 關鍵:確保不截斷多位元組字元中間
while (end > startByteIndex && (bytes[end - 1] & 0xC0) == 0x80) // 連續位元組標記 10xxxxxx
{
end--;
}
return Encoding.UTF8.GetString(bytes, startByteIndex, end - startByteIndex);
}
byte[] 並傳入,避免重複 Encoding.GetBytesSpan<byte> / Memory<byte> 零複製切片 (.NET Core 2.1+)IStringSegmenter 服務,注入使用| 誤區 | 正確觀念 |
|---|---|
str.Substring(start, len) 就是按位元組截取 | Substring 是按 **字元 (Char/UTF-16 code unit)** 截取,中英同算 1 |
| 中文一定佔 2 bytes | 視編碼而定:GB2312/Big5=2、UTF-8=3、UTF-16=2/4 (補充平面) |
| 直接截斷 byte[] 再解碼 | 會產生無效字元序列,解碼時拋例外或產生替換字元 () |
依位元組長度截取中英混合字串的關鍵在於:**逐字元累加位元組寬度、完整字元不可分割、編碼一致性**。本文提供的 SubstringByByteLength 方法支援指定起始位移與長度,適合用於固定寬度顯示、舊系統介面整合、報表列印欄位截斷等場景。若專案已全面採用 UTF-8,請改用 SubstringByUtf8ByteLength 版本。