.NET如何將字符串分隔為字符


.NET如何將字符串分隔為字符

如果這是一道面試題,答案也許非常簡單:.ToCharArray(),這基本正確……

我們以“AB吉𠮷😁👨‍👩‍👧‍👦”作為輸入參數,首先如果按照“正常”處理的思路,用.ToCharArray(),然后轉換為JSON(以便方便查看)返回結果如下:

[
  "A",
  "B",
  "吉",
  "�",
  "�",
  "�",
  "�",
  "�",
  "�",
  "‍",
  "�",
  "�",
  "‍",
  "�",
  "�",
  "‍",
  "�",
  "�"
]

不出所料,出現了大量亂碼。

正常一個字符(Unicode基平面)應該是占用一個char(2字節)沒錯,但如果涉及4字節UnicodeEmoji,這個問題就不簡單了。

  • 首先,32Unicode占用兩個char,如:𠮷;
  • 其次,某些emoji可能占用超過兩個char,可能多達11個,如:👨‍👩‍👧‍👦;

代碼演示如圖:

下面我將一一演示我的解決過程。

32Unicode

我知道在.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",
  "吉",
  "𠮷",
  "😁",
  "👨",
  "‍",
  "👩",
  "‍",
  "👧",
  "‍",
  "👦"
]

可見,它成功“破解”了32Unicode,“𠮷”顯示正常,部分表情如😁,也顯示正常。但👨‍👩‍👧‍👦還是被“暴力”拆成了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

在一次偶然的機會,看UWPWin2D 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 OnlyDirectWrite技術,有沒有平台無關的方法呢?

經常我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上使用。

后來我找到了一個好辦法,安裝NuGetCHTCHSConv,然后使用類似代碼即可,結果為飛龍騎臉怎么輸!😁,完全正確。

ChineseConverter.Convert("飛龍騎臉怎么輸!😁", ChineseConversionDirection.SimplifiedToTraditional)

但我在尋求這個問題的過程中誤入了另一條路,我想將字符串分隔開來,然后單獨判斷是不是一個char能包含整個字符。雖然我后來知道解決這個問題不需要,也不應該這樣做。但我在這條錯誤的路上越陷越深,然后出現了本篇文章😂。

喜歡的朋友 請關注我的微信公眾號:【DotNet騷操作】

DotNet騷操作


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM