C# "anyString".Contains('\0', StringComparison.InvariantCulture) returns 在 .NET5 中为真,在旧版本中为假

C# "anyString".Contains('\0', StringComparison.InvariantCulture) returns true in .NET5 but false in older versions

我在尝试将我的项目从 .NET core 3.1 升级到最新的 .NET 5 时遇到了不兼容的问题。

我的原始代码有一个验证逻辑,通过检查从 Path.GetInvalidFileNameChars() API.

返回的每个字符来检查无效的文件名字符

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

假设您为 fileName 指定一个有效的常规值,例如“test.txt”。然而,令人惊讶的是,如果您 运行 使用 'net5' 目标框架,上面的代码给出的文件名是无效的。

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

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

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

为什么在 .NET5 中更改了此行为?

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

Behavior changes when comparing strings on .NET 5+

另见相关工单:

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

编辑2:

现在我意识到这个问题的真正原因是我混淆了 InvariantCulture 和 Ordinal 字符串比较。它们实际上是完全不同的东西。看下面的票:

Difference between InvariantCulture and Ordinal string comparison

另请注意,这应该是 .NET 的独特问题,因为其他主要编程语言(例如 Java、C++ 和 Python 默认处理序号比较。

不是错误,是功能

issue that I've opened has been closed, but they gave a very good explanation. Now... In .NET 5.0 they began using on Windows (on Linux it was already present) a new library for comparing strings, the ICU library. It is the official library of the Unicode Consortium, so it is "the verb". That library is used for CurrentCulture, InvariantCulture (plus the respective IgnoreCase) and and any other culture. The only exception is the Ordinal/OrdinalIgnoreCase. The library is targetted for text and it has some "particular" ideas about non-text. In this particular case, there are some characters that are simply ignored。在块 0000-00FF 中,我会说被忽略的字符都是控制代码(请忽略它们显示为 €‚ƒ„†‡ˆ‰Š‹ŒŽ‘’“”•–—™š›œžŸ 的事实,在某些时候这些字符已被重新映射到 Unicode 中的其他地方,但是 glyps显示不反映它,但如果你尝试查看他们的代码,就像做 char ch = '€'; int val = (int)ch; 你会看到它),并且 '[=20=]' 是控制代码。

现在......我个人的想法是,从今天开始比较 string 你需要 Unicode 技术的硕士学位,我希望他们能在 .NET 6.0 中做一些恶作剧来进行默认比较 Ordinal(它是 proposals for .NET 6.0, the Option B). Note that if you want to make programs that can run in Turkey you already needed a master's degree in Unicode Technologies (see the Turkish i problem 之一)。

一般来说,要查找不是 keywords/fixed 个词的词(例如列名),您应该使用文化感知比较,而要查找 keywords/fixed 个词(例如列名)和 symbols/control 代码,您应该使用序号比较。问题是当您想同时寻找两者时。通常在这种情况下,您正在寻找 exact 个单词,因此您可以使用 Ordinal。否则就变成地狱了。而且我什至不想考虑 Regex 在文化感知环境中的内部工作方式。我不想去想。因为在那个方向上只能是愚蠢和噩梦。

作为旁注,甚至在“默认”文化意识比较之前就有一些秘密的 shaeaningans...例如:

int ix = "ʹ$ʹ".IndexOf("$"); // -1 on .NET Framework or .NET Core <= 3.1

之前写的

我会说这是一个错误。 IndexOf 也有类似的错误。 I've opened an Issue on github to track it.

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

一些示例代码:

Console.WriteLine($"Ordinal Contains null char {"test".Contains("[=11=]", StringComparison.Ordinal)}");
Console.WriteLine($"OrdinalIgnoreCase Contains null char {"test".Contains("[=11=]", StringComparison.OrdinalIgnoreCase)}");

Console.WriteLine($"CurrentCulture Contains null char {"test".Contains("[=11=]", StringComparison.CurrentCulture)}");
Console.WriteLine($"CurrentCultureIgnoreCase Contains null char {"test".Contains("[=11=]", StringComparison.CurrentCultureIgnoreCase)}");

Console.WriteLine($"InvariantCulture Contains null char {"test".Contains("[=11=]", StringComparison.InvariantCulture)}");
Console.WriteLine($"InvariantCultureIgnoreCase Contains null char {"test".Contains("[=11=]", StringComparison.InvariantCultureIgnoreCase)}");

Console.WriteLine($"Ordinal IndexOf null char {"test".IndexOf("[=11=]t", StringComparison.Ordinal)}");
Console.WriteLine($"OrdinalIgnoreCase IndexOf null char {"test".IndexOf("[=11=]", StringComparison.OrdinalIgnoreCase)}");

Console.WriteLine($"CurrentCulture IndexOf null char {"test".IndexOf("[=11=]", StringComparison.CurrentCulture)}");
Console.WriteLine($"CurrentCultureIgnoreCase IndexOf null char {"test".IndexOf("[=11=]", StringComparison.CurrentCultureIgnoreCase)}");

Console.WriteLine($"InvariantCulture IndexOf null char {"test".IndexOf("[=11=]", StringComparison.InvariantCulture)}");
Console.WriteLine($"InvariantCultureIgnoreCase IndexOf null char {"test".IndexOf("[=11=]", StringComparison.InvariantCultureIgnoreCase)}");

Console.WriteLine($"Ordinal Contains null char {"test".Contains("[=12=]test", StringComparison.Ordinal)}");
Console.WriteLine($"OrdinalIgnoreCase Contains null char {"test".Contains("[=12=]test", StringComparison.OrdinalIgnoreCase)}");

Console.WriteLine($"CurrentCulture Contains null char {"test".Contains("[=12=]test", StringComparison.CurrentCulture)}");
Console.WriteLine($"CurrentCultureIgnoreCase Contains null char {"test".Contains("[=12=]test", StringComparison.CurrentCultureIgnoreCase)}");

Console.WriteLine($"InvariantCulture Contains null char {"test".Contains("[=12=]test", StringComparison.InvariantCulture)}");
Console.WriteLine($"InvariantCultureIgnoreCase Contains null char {"test".Contains("[=12=]test", StringComparison.InvariantCultureIgnoreCase)}");

Console.WriteLine($"Ordinal IndexOf null char {"test".IndexOf("[=12=]t", StringComparison.Ordinal)}");
Console.WriteLine($"OrdinalIgnoreCase IndexOf null char {"test".IndexOf("[=12=]test", StringComparison.OrdinalIgnoreCase)}");

Console.WriteLine($"CurrentCulture IndexOf null char {"test".IndexOf("[=12=]test", StringComparison.CurrentCulture)}");
Console.WriteLine($"CurrentCultureIgnoreCase IndexOf null char {"test".IndexOf("[=12=]test", StringComparison.CurrentCultureIgnoreCase)}");

Console.WriteLine($"InvariantCulture IndexOf null char {"test".IndexOf("[=12=]test", StringComparison.InvariantCulture)}");
Console.WriteLine($"InvariantCultureIgnoreCase IndexOf null char {"test".IndexOf("[=12=]test", StringComparison.InvariantCultureIgnoreCase)}");