.NET如何将字符串分隔为字符
前言
如果這是一道面試題,答案也許非常簡單:.ToCharArray(),這基本正確……
我們以“AB吉??????”作為輸入?yún)?shù),首先如果按照“正常”處理的思路,用 .ToCharArray(),然后轉(zhuǎn)換為 JSON(以便方便查看)返回結(jié)果如下:
[ "A", "B", "吉", "�", "�", "�", "�", "�", "�", "?", "�", "�", "?", "�", "�", "?", "�", "�" ]不出所料,出現(xiàn)了大量亂碼。
正常一個字符( Unicode基平面)應(yīng)該是占用一個 char(2字節(jié))沒錯,但如果涉及 4字節(jié) Unicode或 Emoji,這個問題就不簡單了。
首先,?32位?Unicode占用兩個?char,如:?;
其次,某些?emoji可能占用超過兩個?char,可能多達(dá)?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吉??????”作為輸入?yún)?shù),運(yùn)行結(jié)果如下:
[ "A", "B", "吉", "?", "?", "?", "?", "?", "?", "?", "?", "?" ]可見,它成功“破解”了 32位 Unicode,“?”顯示正常,部分表情如?,也顯示正常。但???????還是被“暴力”拆成了 4個表情“????”和三個空白。我稍后聊這個 Emoji,因?yàn)檫@些代碼有簡化空間。
后來我將這個“字符串分隔為字符”問題在長沙.NET技術(shù)社區(qū)發(fā)問,有大佬就指出有簡單的辦法,通過系統(tǒng)內(nèi)置的 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來解決,實(shí)在是暗暗佩服:
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
在一次偶然的機(jī)會,看 UWP的 Win2DGallery時,我看到了這個 demo:
我心想, DirectWrite既然知道每個字符的邊界,顯然也必然知道如何將字符串分隔為字符。果然,經(jīng)過一陣探索,我找到了解決辦法:
// 安裝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; } }運(yùn)行效果如下:
[ "A", "B", "吉", "?", "?", "???????" ]終于……完全正常!但這是基于 WindowsOnly的 DirectWrite技術(shù),有沒有平臺無關(guān)的方法呢?
經(jīng)常我4個多小時的翻閱文檔、編寫代碼,終于找到了眉目。文檔如下:https://en.wikipedia.org/wiki/Zero-width_joiner
原來有一個“零寬度連接符”( Zero-width joiner/ ZWJ)的概念,值為 0x200D。如果發(fā)現(xiàn) char為該值,則說明它是一個零寬度連接符,此時后面的 emoji應(yīng)該與前面的 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();運(yùn)行結(jié)果如下——果然它包含了三個零寬度連接符:
因此我們可以利用這個 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完全一樣,完美!
結(jié)語
說來話長,這其實(shí)是客戶真正遇到的問題。事情起源于一次客戶與我的微信聊天,客戶遇到了一個問題:
客戶是想從簡體中文轉(zhuǎn)換為繁體中文,正使用 Microsoft.VisualBasic.dll提供的 Strings.StrConv(text,VbStrConv.TraditionalChinese)方法,遇到了這個問題。客戶的代碼如下:
Strings.StrConv("飛龍騎臉怎么輸!?", VbStrConv.TraditionalChinese)在 .NETFramework下輸出結(jié)果是:飛龍騎臉怎么輸!??。注意,最后的 emoji表情"?"被顯示成了兩個問號“??”。
在 .NETCore下,該代碼運(yùn)行報異常,提示需要操作系統(tǒng)支持(可能需要安裝語言包),具體報錯內(nèi)容是:“ ArgumentException:Thissystem doesnotcontain supportfortheTraditionalChineselocale.”。
這個區(qū)別說明,該函數(shù)最好別在 .NETCore上使用。
后來我找到了一個好辦法,安裝 NuGet包 CHTCHSConv,然后使用類似代碼即可,結(jié)果為 飛龍騎臉怎么輸!?,完全正確。
ChineseConverter.Convert("飛龍騎臉怎么輸!?", ChineseConversionDirection.SimplifiedToTraditional)但我在尋求這個問題的過程中誤入了另一條路,我想將字符串分隔開來,然后單獨(dú)判斷是不是一個 char能包含整個字符。雖然我后來知道解決這個問題不需要,也不應(yīng)該這樣做。但我在這條錯誤的路上越陷越深,然后出現(xiàn)了本篇文章?。
微信可能無法評論,請點(diǎn)擊左下角“閱讀原文”前往我的博客園點(diǎn)贊/留言。
喜歡的朋友 請關(guān)注我的微信公眾號:【DotNet騷操作】
總結(jié)
以上是生活随笔為你收集整理的.NET如何将字符串分隔为字符的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: gRPC 流式调用
- 下一篇: Orleans 知多少 | 3. Hel