C#/.net core “hello”.IndexOf(“\0”,2)中的坑

先想想看,你认为下面代码返回值是多少?

cs 复制代码
"hello".IndexOf("", 2);
"hello".IndexOf("\0", 2);
"hello".IndexOf('\0', 2);

今天和大家分享关于.net core中与字符相关的一些奇怪问题。

首先我们先以.NET8目标框架做为测试环境。直接上代码:

cs 复制代码
using System.Reflection;
using System.Runtime.Versioning;
namespace TestNetCore
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var assembly = Assembly.GetExecutingAssembly();
            var targetFramework = (TargetFrameworkAttribute?)Attribute.GetCustomAttribute(assembly, typeof(TargetFrameworkAttribute));
            if (targetFramework != null)
            {
                Console.WriteLine($"目标框架: {targetFramework.FrameworkName}");
            }

            Console.WriteLine(@"""hello"".IndexOf("""", 2) 结果:" + "hello".IndexOf("", 2));
            Console.WriteLine(@"""hello"".IndexOf(""\0"", 2) 结果:" + "hello".IndexOf("\0", 2));
            Console.WriteLine(@"""hello"".IndexOf('\0', 2) 结果:" + "hello".IndexOf('\0', 2));
            Console.ReadKey();
        }
    }
}

运行结果如下:

这与你设想的结果有差别吗?虽然只是一个方法,三行代码,但是这里面包含了很多知识点,下面我们就来具体聊聊为什么是这样的结果,底层逻辑是什么。

相信大多数人会有一下几个疑问:

  • 为什么查找空字符串返回2
  • 为什么查找"\0"也返回2
  • 为什么查找'\0'又返回-1

1、为什么"hello".IndexOf("", 2)返回2

下面我们一个一个来说,首先来说为什么"hello".IndexOf("", 2)返回2,这个问题比较简单,就是方法本身定义问题,可以查看官方文档有详细说明,如下图:

方法定义如此,如果查询的值为空字符串,则返回值为startIndex,即查找起始位置索引,因为"hello".IndexOf("", 2)的2表示从索引2处开始查找,所以此这行代码返回2。

2、为什么"hello".IndexOf("\0", 2)也返回2

虽然这里只是简单的把空字符串改成了"\0",但是里的问题就比较复杂了,涉及到多个知识点,首先这里涉及到Unicode编码问题,当前文化设置问题,以及.NET全球化问题。

我们先回到这个问题,先来看看官方文档说明,如下图:

从这里面可以大胆猜测"\0"就是属于可忽略字符,并且如果查询的字符串包含了可忽略字符,则结果和移除该字符搜索等效,那么"hello".IndexOf("\0", 2)就等效与"hello".IndexOf("", 2),因此返回2也就顺利成章了。

首先我们猜测是正确的,"\0的确属于可忽略字符,这就是Unicode编码规范问题,而且不单单"\0"会有这样的问题,Unicdoe可忽略字符都会有这样的问题,比如"\u0010"、"\u001B"等。

cs 复制代码
Console.WriteLine(@"""hello"".IndexOf(""\u0010"", 2) 结果:" + "hello".IndexOf("\u0010", 2));
Console.WriteLine(@"""hello"".IndexOf(""\u001B"", 2) 结果:" + "hello".IndexOf("\u001B", 2));

执行效果如下

3、为什么"hello".IndexOf('\0', 2)返回-1

这个答案在上面的官方文档说明中也可以看到蛛丝马迹,"在执行语言性的或区分区域性的比较时该字符不被考虑",这句话是关键,说明IndexOf方法是会受当前文化设置影响的,虽然我们写的代码里没有看到相关当前文化设置,但是不代表没有,我们可以看下IndexOf相关的重载方法。

红框中StringComparison参数就可以设置当前文化。我们看看有哪些设置选项。

因为"hello".IndexOf("\0", 2)内部使用了StringComparison.CurrentCulture 而"hello".IndexOf('\0', 2) 内部使用了StringComparison.Ordinal,就是因为CurrentCulture枚举值导致"在执行语言性的或区分区域性的比较时"\0"不被考虑",被直接忽略了,而Ordinal枚举值不会有这样的问题,所以没有被忽略,所以"hello".IndexOf('\0', 2)返回-1。

我们也可以直接调用IndexOf重载方法,指定StringComparison来达到我们想要的效果。

cs 复制代码
Console.WriteLine(@"""hello"".IndexOf(""\0"", 2, 3, StringComparison.CurrentCulture) 结果:" + "hello".IndexOf("\0", 2, 3, StringComparison.CurrentCulture));
Console.WriteLine(@"""hello"".IndexOf(""\0"", 2, 3, StringComparison.Ordinal) 结果:" + "hello".IndexOf("\0", 2, 3, StringComparison.Ordinal));

运行代码如下:

虽然原因找到了,但是我们再深入思考一下,为什么会有这样的差异呢?只有IndexOf方法有这样的问题吗?

要回答这个问题,就是我们上面提到的.NET全球化问题了。在 .NET 5 前,.NET 全球化 API 在不同的平台上使用不同的基础库。 在 Unix 上,API 使用 Unicode 国际组件 (ICU),在 Windows 上,API 使用 区域语言支持 (NLS)。 这导致在不同平台上运行应用程序时,在少数全球化 API 中存在一些行为差异。 但是以下方面存在明显的行为差异:区域性和区域性数据、字符串大小写、字符串排序和搜索、排序关键字、字符串规范化、国际化域名 (IDN) 支持、Linux 上的时区显示名称。

因此不单单IndexOf方法有这样的问题,下面这些API都有存在同样的问题:

cs 复制代码
System.String.Compare
System.String.EndsWith
System.String.IndexOf
System.String.StartsWith
System.String.ToLower
System.String.ToLowerInvariant
System.String.ToUpper
System.String.ToUpperInvariant
System.Globalization.TextInfo(大多数成员)
System.Globalization.CompareInfo(大多数成员)
System.Array.Sort(对字符串数组进行排序时)
System.Collections.Generic.List<T>.Sort()(当列表元素为字符串时)
System.Collections.Generic.SortedDictionary<TKey,TValue>(当键为字符串时)
System.Collections.Generic.SortedList<TKey,TValue>(当键为字符串时)
System.Collections.Generic.SortedSet<T>(当集包含字符串时)

这里面很多方法都是有多个重载方法的,而每个重载方法默认当前文化设置可能并不相同。因此大家在开发的时候一定要注意使用,一不小心肯能就好引起一些奇怪的问题,因此大家尽量自己手动指定当前文化设置。

下表列出一些方法其对应的默认行为。

:当然如果调用方提供显式 CultureInfo 或 StringComparison 参数,则该参数将优先于任何默认值。

最后总结一下

  1. IndexOf对于Empty字符查找会返回开始查找索引startIndex,而不是我们想象中的-1;
  2. Unicode可忽略字符受StringComparison参数影响很大,会直接把相应字符直接忽略掉
  3. .NET全球化进程中,
    区域语言支持 (NLS)在向Unicode 国际组件 (ICU)迁移是必然,因此我们在使用相关方法时一定要小心
  4. 如果可以尽量主动显示设置当前文化区域设置
相关推荐
__water8 小时前
『功能项目』回调函数处理死亡【54】
c#·回调函数·unity引擎
__water8 小时前
『功能项目』眩晕图标显示【52】
c#·unity引擎·动画事件
__water9 小时前
『功能项目』第二职业法师的平A【57】
c#·unity引擎·魔法球伤害传递
__water11 小时前
『功能项目』战士的伤害型技能【45】
c#·unity引擎·战士职业伤害型技能
君莫愁。12 小时前
【Unity】检测鼠标点击位置是否有2D对象
unity·c#·游戏引擎
Lingbug13 小时前
.Net日志组件之NLog的使用和配置
后端·c#·.net·.netcore
咩咩觉主13 小时前
Unity实战案例全解析:PVZ 植物卡片状态分析
unity·c#·游戏引擎
Echo_Lee013 小时前
C#与Python脚本使用共享内存通信
开发语言·python·c#
__water20 小时前
『功能项目』QFrameWork框架重构OnGUI【63】
c#·unity引擎·重构背包框架
Crazy Struggle20 小时前
C# + WPF 音频播放器 界面优雅,体验良好
c#·wpf·音频播放器·本地播放器