C#中".NET5中使用String.Contains('\0', StringComparison.InvariantCulture)返回true,但在早期版本中返回false"。

11

当我试图将我的项目从.NET Core 3.1升级到最新的.NET 5时,我遇到了一个不兼容的问题。

我的原始代码使用Path.GetInvalidFileNameChars() API返回的每个字符来检查无效的文件名字符以进行验证逻辑。


var invalidFilenameChars = Path.GetInvalidFileNameChars();
bool validFileName = !invalidFilenameChars.Any(ch => fileName.Contains(ch, StringComparison.InvariantCulture));

假设您给出类似“test.txt”这样的常规值作为fileName,这应该是有效的。然而,令人惊讶的是,如果您在“net5”目标框架下运行它,则上述代码会使文件名无效。

经过一段时间的调试,我发现返回的无效字符集包含'\0',空的ASCII字符和"text.txt"。Contains("\0", StringComparison.InvariantCulture) 返回true。

    class Program
    {
        static void Main(string[] args)
        {
            var containsNullChar = "test".Contains("\0", StringComparison.InvariantCulture);
    
            Console.WriteLine($"Contains null char {containsNullChar}");
        }
    }

如果您在 .NET Core 3.1 中运行,它永远不会说普通字符串包含空字符。此外,如果我省略第二个参数(StringComparison.InvariantCulture)或使用StringComparison.Ordinal,就永远不会返回奇怪的结果。

为什么在.NET5中会改变这种行为呢?

编辑:正如Karl-Johan Sjögren之前所评论的那样,在.NET5中确实有关于字符串比较的行为变化:

在.NET 5+上比较字符串时的行为更改

还请参阅相关票证:

.Net 5中string.IndexOf获得不同的结果

虽然这个问题应该与上面有关,但是与'\0'相关的当前结果看起来对我来说仍然很奇怪,并且可能仍然被认为是一个Bug,正如@xanatos所回答的那样。

编辑2:现在我意识到这个问题的实际原因是我对InvariantCulture和Ordinal字符串比较的混淆。它们实际上是非常不同的东西。请参阅下面的票证:

InvariantCulture和Ordinal字符串比较之间的区别

还要注意,这应该是.NET的唯一问题,因为其他主要编程语言(如Java、C++和Python)默认情况下处理顺序比较。


5
是的,它在 .Net 5 中进行了更改。详情请参见:https://learn.microsoft.com/en-us/dotnet/standard/base-types/string-comparison-net-5-plus - Karl-Johan Sjögren
1
@Charlieface 尽管 NUL 字符在 C# 中不用于检测字符串结尾(因为 "a\0b".Length 返回 3),而且主要是为了使交互操作更容易。 - ckuri
2
问题甚至更大...... "test".IndexOf("\0", StringComparison.InvariantCulture) == 0 而不是 -1 或 4。 - xanatos
我想知道在哪里可以提交错误报告。@xanatos 你能否请提交一个错误报告吗? - Ryo Asai
1
@RyoAsai 我已经收到回复了。我扩展了回复并加入了一些我的想法。 - xanatos
显示剩余7条评论
1个回答

9

不是Bug,而是特性

我打开的问题已经关闭了,但是他们给出了一个非常好的解释。现在... 在.NET 5.0中,他们开始在Windows上(在Linux上已经存在)使用新的字符串比较库,即ICU库。它是Unicode联盟的官方库,因此它是“动词”。该库用于CurrentCultureInvariantCulture(加上相应的IgnoreCase)和任何其他语言环境。唯一的例外是Ordinal/OrdinalIgnoreCase。该库的目标是文本,对于非文本有一些“特别”的想法。在这种情况下,有一些字符被简单地忽略掉了。在0000-00FF块中,我会说被忽略的字符都是控制字符(请忽略它们显示为€‚ƒ„†‡ˆ‰Š‹ŒŽ‘’“”•–—™š›œžŸ这一事实,在某个时候,这些字符已经被重新映射到Unicode的其他位置,但是显示的字形没有反映出来,但是如果你尝试查看它们的代码,比如做char ch = '€'; int val = (int)ch;,你会看到它),而'\0'则是一个控制字符。

现在... 我个人认为,如果要比较今天的string,你需要一个Unicode技术硕士学位,我确实希望他们会在.NET 6.0中进行一些花招,使默认比较成为Ordinal(这是.NET 6.0的建议之一,即选项B)。请注意,如果您想制作可以在土耳其运行的程序,您已经需要一个Unicode技术硕士学位(请参见土耳其i问题)。

一般而言,若要查找非关键字/固定字词(例如列名称)的词汇,应使用区域性敏感比较;若要查找关键字/固定字词(例如列名称)和符号/控制码,则应使用基数比较。问题在于,当您想同时查找两者时。通常情况下,您正在寻找确切的词汇,因此可以使用基数比较。否则,就会变得非常困难。我甚至不想考虑在区域性敏感环境中正则表达式的内部工作方式。那是我不想去想的事情。因为在那个方向上只可能出现愚蠢和噩梦。
顺便提一下,在“默认”的区域性敏感比较之前,有一些秘密交易...例如:
int ix = "ʹ$ʹ".IndexOf("$"); // -1 on .NET Framework or .NET Core <= 3.1

我之前所写的内容

我会说这是一个错误。在IndexOf中也存在类似的 bug. 我在 Github 上开了一个 Issue 来跟踪它。

正如你所写的,OrdinalOrdinalIgnoreCase 的工作效果是符合预期的(可能是因为它们不需要使用新的 ICU 库来处理 Unicode)。

一些示例代码:

Console.WriteLine($"Ordinal Contains null char {"test".Contains("\0", StringComparison.Ordinal)}");
Console.WriteLine($"OrdinalIgnoreCase Contains null char {"test".Contains("\0", StringComparison.OrdinalIgnoreCase)}");

Console.WriteLine($"CurrentCulture Contains null char {"test".Contains("\0", StringComparison.CurrentCulture)}");
Console.WriteLine($"CurrentCultureIgnoreCase Contains null char {"test".Contains("\0", StringComparison.CurrentCultureIgnoreCase)}");

Console.WriteLine($"InvariantCulture Contains null char {"test".Contains("\0", StringComparison.InvariantCulture)}");
Console.WriteLine($"InvariantCultureIgnoreCase Contains null char {"test".Contains("\0", StringComparison.InvariantCultureIgnoreCase)}");

Console.WriteLine($"Ordinal IndexOf null char {"test".IndexOf("\0t", StringComparison.Ordinal)}");
Console.WriteLine($"OrdinalIgnoreCase IndexOf null char {"test".IndexOf("\0", StringComparison.OrdinalIgnoreCase)}");

Console.WriteLine($"CurrentCulture IndexOf null char {"test".IndexOf("\0", StringComparison.CurrentCulture)}");
Console.WriteLine($"CurrentCultureIgnoreCase IndexOf null char {"test".IndexOf("\0", StringComparison.CurrentCultureIgnoreCase)}");

Console.WriteLine($"InvariantCulture IndexOf null char {"test".IndexOf("\0", StringComparison.InvariantCulture)}");
Console.WriteLine($"InvariantCultureIgnoreCase IndexOf null char {"test".IndexOf("\0", StringComparison.InvariantCultureIgnoreCase)}");

and

Console.WriteLine($"Ordinal Contains null char {"test".Contains("\0test", StringComparison.Ordinal)}");
Console.WriteLine($"OrdinalIgnoreCase Contains null char {"test".Contains("\0test", StringComparison.OrdinalIgnoreCase)}");

Console.WriteLine($"CurrentCulture Contains null char {"test".Contains("\0test", StringComparison.CurrentCulture)}");
Console.WriteLine($"CurrentCultureIgnoreCase Contains null char {"test".Contains("\0test", StringComparison.CurrentCultureIgnoreCase)}");

Console.WriteLine($"InvariantCulture Contains null char {"test".Contains("\0test", StringComparison.InvariantCulture)}");
Console.WriteLine($"InvariantCultureIgnoreCase Contains null char {"test".Contains("\0test", StringComparison.InvariantCultureIgnoreCase)}");

Console.WriteLine($"Ordinal IndexOf null char {"test".IndexOf("\0t", StringComparison.Ordinal)}");
Console.WriteLine($"OrdinalIgnoreCase IndexOf null char {"test".IndexOf("\0test", StringComparison.OrdinalIgnoreCase)}");

Console.WriteLine($"CurrentCulture IndexOf null char {"test".IndexOf("\0test", StringComparison.CurrentCulture)}");
Console.WriteLine($"CurrentCultureIgnoreCase IndexOf null char {"test".IndexOf("\0test", StringComparison.CurrentCultureIgnoreCase)}");

Console.WriteLine($"InvariantCulture IndexOf null char {"test".IndexOf("\0test", StringComparison.InvariantCulture)}");
Console.WriteLine($"InvariantCultureIgnoreCase IndexOf null char {"test".IndexOf("\0test", StringComparison.InvariantCultureIgnoreCase)}");

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接