先想想看,你认为下面代码返回值是多少?
"hello".IndexOf("", 2);
"hello".IndexOf("\0", 2);
"hello".IndexOf('\0', 2);
今天和大家分享关于.net core中与字符相关的一些奇怪问题。
首先我们先以.NET8目标框架做为测试环境。直接上代码:
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“等。
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来达到我们想要的效果。
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都有存在同样的问题:
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 参数,则该参数将优先于任何默认值。
最后总结一下
- IndexOf对于Empty字符查找会返回开始查找索引startIndex,而不是我们想象中的-1;
- Unicode可忽略字符受StringComparison参数影响很大,会直接把相应字符直接忽略掉
- .NET全球化进程中,
区域语言支持 (NLS)在向Unicode 国际组件 (ICU)迁移是必然,因此我们在使用相关方法时一定要小心 - 如果可以尽量主动显示设置当前文化区域设置