.NET如何将字符串分隔为字符

前言

如果这是一道面试题,答案也许非常简单:.ToCharArray(),这基本正确……

我们以“AB吉??????”作为输入参数,首先如果按照“正常”处理的思路,用 .ToCharArray(),然后转换为 JSON(以便方便查看)返回结果如下:

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

不出所料,出现了大量乱码。

正常一个字符( Unicode基平面)应该是占用一个 char(2字节)没错,但如果涉及 4字节 UnicodeEmoji,这个问题就不简单了。

  • 首先, 32位 Unicode占用两个 char,如:?;

  • 其次,某些 emoji可能占用超过两个 char,可能多达 11个,如:?‍?‍?‍?;

代码演示如图:

640?wx_fmt=png

下面我将一一演示我的解决过程。

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",	
  "吉",	
  "?",	
  "?",	
  "?",	
  "‍",	
  "?",	
  "‍",	
  "?",	
  "‍",	
  "?"	
]

可见,它成功“破解”了 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

在一次偶然的机会,看 UWPWin2DGallery时,我看到了这个 demo

640?wx_fmt=png

我心想, 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",	
  "吉",	
  "?",	
  "?",	
  "?‍?‍?‍?"	
]

终于……完全正常!但这是基于 WindowsOnlyDirectWrite技术,有没有平台无关的方法呢?

经常我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();

运行结果如下——果然它包含了三个零宽度连接符:

640?wx_fmt=png

因此我们可以利用这个 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完全一样,完美!

结语

说来话长,这其实是客户真正遇到的问题。事情起源于一次客户与我的微信聊天,客户遇到了一个问题:

640?wx_fmt=png

客户是想从简体中文转换为繁体中文,正使用 Microsoft.VisualBasic.dll提供的 Strings.StrConv(text,VbStrConv.TraditionalChinese)方法,遇到了这个问题。客户的代码如下:

Strings.StrConv("飞龙骑脸怎么输!?", VbStrConv.TraditionalChinese)

.NETFramework下输出结果是:飛龍騎臉怎么輸!??。注意,最后的 emoji表情"?"被显示成了两个问号“??”。

.NETCore下,该代码运行报异常,提示需要操作系统支持(可能需要安装语言包),具体报错内容是:“ ArgumentException:Thissystem doesnotcontain supportfortheTraditionalChineselocale.”。

这个区别说明,该函数最好别在 .NETCore上使用。

后来我找到了一个好办法,安装 NuGetCHTCHSConv,然后使用类似代码即可,结果为 飛龍騎臉怎么輸!?,完全正确。

ChineseConverter.Convert("飞龙骑脸怎么输!?", ChineseConversionDirection.SimplifiedToTraditional)

但我在寻求这个问题的过程中误入了另一条路,我想将字符串分隔开来,然后单独判断是不是一个 char能包含整个字符。虽然我后来知道解决这个问题不需要,也不应该这样做。但我在这条错误的路上越陷越深,然后出现了本篇文章?。

微信可能无法评论,请点击左下角“阅读原文”前往我的博客园点赞/留言。

喜欢的朋友 请关注我的微信公众号:【DotNet骚操作】

640?wx_fmt=jpeg

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值