前言
如果这是一道面试题,答案也许非常简单:.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
的 Win2DGallery
时,我看到了这个 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", "吉", "?", "?", "????"
]
终于……完全正常!但这是基于 WindowsOnly
的 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)
在 .NETFramework
下输出结果是:飛龍騎臉怎么輸!??
。注意,最后的 emoji
表情"?"被显示成了两个问号“??”。
在
.NETCore
下,该代码运行报异常,提示需要操作系统支持(可能需要安装语言包),具体报错内容是:“ArgumentException:Thissystem doesnotcontain supportfortheTraditionalChineselocale.
”。这个区别说明,该函数最好别在
.NETCore
上使用。
后来我找到了一个好办法,安装 NuGet
包 CHTCHSConv
,然后使用类似代码即可,结果为 飛龍騎臉怎么輸!?
,完全正确。
ChineseConverter.Convert("飞龙骑脸怎么输!?", ChineseConversionDirection.SimplifiedToTraditional)
但我在寻求这个问题的过程中误入了另一条路,我想将字符串分隔开来,然后单独判断是不是一个 char
能包含整个字符。虽然我后来知道解决这个问题不需要,也不应该这样做。但我在这条错误的路上越陷越深,然后出现了本篇文章?。
微信可能无法评论,请点击左下角“阅读原文”前往我的博客园点赞/留言。
喜欢的朋友 请关注我的微信公众号:【DotNet骚操作】