.NET如何將字符串分隔為字符
如果這是一道面試題,答案也許非常簡單:.ToCharArray()
,這基本正確……
我們以“AB吉𠮷😁👨👩👧👦”作為輸入參數,首先如果按照“正常”處理的思路,用.ToCharArray()
,然后轉換為JSON
(以便方便查看)返回結果如下:
[
"A",
"B",
"吉",
"�",
"�",
"�",
"�",
"�",
"�",
"",
"�",
"�",
"",
"�",
"�",
"",
"�",
"�"
]
不出所料,出現了大量亂碼。
正常一個字符(Unicode
基平面)應該是占用一個char
(2字節)沒錯,但如果涉及4
字節Unicode
或Emoji
,這個問題就不簡單了。
- 首先,
32
位Unicode
占用兩個char
,如:𠮷; - 其次,某些
emoji
可能占用超過兩個char
,可能多達11
個,如:👨👩👧👦;
代碼演示如圖:
下面我將一一演示我的解決過程。
32
位Unicode
我知道在.NET
中,如果一個char
無法容納一個字符,char.IsHighSurrogate()
方法傳入這個char
就會返回true
,這時即可做處理。按照這個思路,解決方法如下:
IEnumerable<string> SplitToCharacters(string input)
{
for (var i = 0; i < input.Length; ++i)
{
if (char.IsHighSurrogate(input[i]))
{
yield return input.Substring(i, 2);
++i;
}
else
{
yield return input[i].ToString();
}
}
}
我將“AB吉𠮷😁👨👩👧👦”作為輸入參數,運行結果如下:
[
"A",
"B",
"吉",
"𠮷",
"😁",
"👨",
"",
"👩",
"",
"👧",
"",
"👦"
]
可見,它成功“破解”了32
位Unicode
,“𠮷”顯示正常,部分表情如😁,也顯示正常。但👨👩👧👦還是被“暴力”拆成了4
個表情“👨👩👧👦”和三個空白。我稍后聊這個Emoji
,因為這些代碼有簡化空間。
后來我將這個“字符串分隔為字符”問題在長沙.NET技術社區發問,有大佬就指出有簡單的辦法,通過系統內置的StringInfo
類,即可一步到位解決:
IEnumerable<string> SplitToCharacters(string input)
{
var si = new StringInfo(input);
for (var i = 0; i < si.LengthInTextElements; ++i)
{
yield return si.SubstringByTextElements(i, 1);
}
}
返回值完全一樣,更有大佬祭出了“騷操作”,通過UTF32
來解決,實在是暗暗佩服:
string[] SplitToCharacters(string input)
{
byte[] bytes = Encoding.UTF32.GetBytes(input);
Span<int> span = MemoryMarshal.Cast<byte, int>(bytes);
var strings = new string[span.Length];
for (var i = 0; i < span.Length; ++i)
{
strings[i] = char.ConvertFromUtf32(span[i]);
}
return strings;
}
返回值也完全一樣。
然而這些辦法都解決不了Emoji
的問題,那么Emoji
到底要如何才能解決呢?
Emoji
在一次偶然的機會,看UWP
的Win2D Gallery
時,我看到了這個demo
:
我心想,DirectWrite
既然知道每個字符的邊界,顯然也必然知道如何將字符串分隔為字符。果然,經過一陣探索,我找到了解決辦法:
// 安裝NuGet包:SharpDX.Direct2D1
using SharpDX.DirectWrite;
IEnumerable<string> SplitToCharacters(string text)
{
using var dwrite = new Factory();
using var format = new TextFormat(dwrite, "Arial", 14.0f); // 字體字號無所謂
using var layout = new TextLayout(dwrite, text, format, int.MaxValue, int.MaxValue);
var pos = 0;
foreach (ClusterMetrics cm in layout.GetClusterMetrics())
{
yield return text.Substring(pos, cm.Length);
pos += cm.Length;
}
}
運行效果如下:
[
"A",
"B",
"吉",
"𠮷",
"😁",
"👨👩👧👦"
]
終於……完全正常!但這是基於Windows Only
的DirectWrite
技術,有沒有平台無關的方法呢?
經常我4個多小時的翻閱文檔、編寫代碼,終於找到了眉目。文檔如下:https://en.wikipedia.org/wiki/Zero-width_joiner
原來有一個“零寬度連接符”(Zero-width joiner
/ZWJ
)的概念,值為0x200D
。如果發現char
為該值,則說明它是一個零寬度連接符,此時后面的emoji
應該與前面的emoji
連接。可以使用如下代碼分析“👨👩👧👦”這個emoji
:
IEnumerable<string> SplitToCharacters(string input)
{
for (var i = 0; i < input.Length; ++i)
{
if (char.IsHighSurrogate(input[i]))
{
yield return input.Substring(i, 2);
++i;
}
else
{
yield return input[i].ToString();
}
}
}
SplitToCharacters("👨👩👧👦").Select(x => new
{
Text = x,
Code = String.Join("", x.Select(x => ((short)x).ToString("X4"))),
}).Dump();
運行結果如下——果然它包含了三個零寬度連接符:
因此我們可以利用這個0x200D
,然后加幾個if/else
,即可將問題解決:
IEnumerable<string> SplitToCharacters(string input)
{
for (var i = 0; i < input.Length; ++i)
{
if (char.IsHighSurrogate(input[i]))
{
int length = 0;
while (true)
{
length += 2;
if (i + length < input.Length && input[i + length] == 0x200D)
{
length += 1;
}
else
{
break;
}
}
yield return input.Substring(i, length);
i += length - 1;
}
else
{
yield return input[i].ToString();
}
}
}
效果與DirectWrite
完全一樣,完美!
結語
說來話長,這其實是客戶真正遇到的問題。事情起源於一次客戶與我的微信聊天,客戶遇到了一個問題:
客戶是想從簡體中文轉換為繁體中文,正使用Microsoft.VisualBasic.dll
提供的Strings.StrConv(text, VbStrConv.TraditionalChinese)
方法,遇到了這個問題。客戶的代碼如下:
Strings.StrConv("飛龍騎臉怎么輸!😁", VbStrConv.TraditionalChinese)
在.NET Framework
下輸出結果是:飛龍騎臉怎么輸!??
。注意,最后的emoji
表情"😁"被顯示成了兩個問號“??”。
在
.NET Core
下,該代碼運行報異常,提示需要操作系統支持(可能需要安裝語言包),具體報錯內容是:“ArgumentException: This system does not contain support for the Traditional Chinese locale.
”。這個區別說明,該函數最好別在
.NET Core
上使用。
后來我找到了一個好辦法,安裝NuGet
包CHTCHSConv
,然后使用類似代碼即可,結果為飛龍騎臉怎么輸!😁
,完全正確。
ChineseConverter.Convert("飛龍騎臉怎么輸!😁", ChineseConversionDirection.SimplifiedToTraditional)
但我在尋求這個問題的過程中誤入了另一條路,我想將字符串分隔開來,然后單獨判斷是不是一個char
能包含整個字符。雖然我后來知道解決這個問題不需要,也不應該這樣做。但我在這條錯誤的路上越陷越深,然后出現了本篇文章😂。
喜歡的朋友 請關注我的微信公眾號:【DotNet騷操作】