String.Trim*(params ReadOnlySpan<char>) 重载已删除

在 .NET 生态系统中, ReadOnlySpan<char> 可以表示:

  • 特定字符序列,通常作为较大 System.String 实例的切片。
  • 单个字符的集合,通常作为一个 char[]切片。

早期版本的 .NET 9 向已具有params T[]重载的方法组添加了params ReadOnlySpan<T>重载。 虽然此重载是某些方法组的积极添加,但对于接受和char[] String (在同一位置)的方法组而言,这种双重性质ReadOnlySpan<char>可能会导致混淆,并且它们受到不同的处理。 例如, public static string [String::]Split(string separator, StringSplitOptions options) 将字符序列视为一个分隔符。 例如, "[]ne]-[Tw[]".Split("]-[", StringSplitOptions.None) 拆分为 new string[] { "[]ne", "Tw[]" };. 另一方面, public static [String::]Split(char[] separator, StringSplitOptions options) 将每个字符 separator 视为不同的分隔符,因此数组等效的拆分生成 new string[] { "", "", "ne", "", "", "Tw", "", "" }。 因此,任何接受新 ReadOnlySpan<char> 重载,必须确定它是否与字符串类似或数组一样。 一般来说,.NET 符合类似数组的行为。

请考虑以下接受 dotnet/runtime#77873建议的参数的新String重载ReadOnlySpan<char>

public string[] Split(params ReadOnlySpan<char> separator);
public string Trim(params ReadOnlySpan<char> trimChars);
public string TrimStart(params ReadOnlySpan<char> trimChars);
public string TrimEnd(params ReadOnlySpan<char> trimChars);

此外,请考虑以下常用的扩展方法:

public static class SomeExtensions {
    public static string TrimEnd(this string target, string trimString) {
        if (target.EndsWith(trimString) {
            return target.Substring(0, target.Length - trimString.Length);
        }

        return target;
    }
}

对于现有的 .NET 运行时,此扩展方法从字符串末尾删除指定的序列。 但是,由于 C# 的重载解析规则, "12345!!!!".TrimEnd("!!!") 将首选新 TrimEnd 重载而不是现有扩展方法,并将结果从 "12345!" (仅删除一组完整的三个感叹号)更改为 "12345" (从末尾删除所有感叹号)。

若要解决此中断问题,有两个可能的路径:引入实例方法 public string TrimEnd(string trimString) 是更好的目标,或删除新方法。 第一个选项会产生额外的风险,因为它需要决定是否返回目标字符串的一个实例或所有这些实例。 毫无疑问,存在使用每个方法的现有代码的调用方。 因此,第二个选项是发布周期的此阶段最合适的选择。

例如,str.Trim(';', ',', '.')使用params该功能传入单个字符的调用方String.Trim不会看到中断。 代码将自动从调用 string.Trim(params char[]) 切换到 string.Trim(params ReadOnlySpan<char>)。 根据 .NET 9 的 GA 版本重新生成时,编译器将自动切换回 char[] 重载。

显式传入(ReadOnlySpan<char>或可转换为不可转换的类型char[]ReadOnlySpan<char>)的调用方String.Trim必须更改其代码,才能在此更改后成功调用Trim

至于 String.Split,与上一样 String.Trim,此方法已具有一个 重载 ,既首选于接受单个字符串参数的扩展方法,也具有新添加 ReadOnlySpan<char> 的重载。 因此,保留了新重载 String.Split

注意

应重新生成针对 .NET 9 预览版 6、.NET 9 预览版 7、.NET 9 RC1 或 .NET 9 RC2 生成的任何程序集,以确保删除对已删除方法的任何调用。 否则可能会导致 MissingMethodException 运行时发生此情况。

引入的版本

.NET 9 正式版

旧行为

在 .NET 9 预览版 6、.NET 9 预览版 7、.NET 9 RC1 和 .NET 9 RC2 中编译的以下代码:

private static readonly char[] s_allowedWhitespace = { ' ', '\t', '\u00A0', '\u2000' };

// Only remove the ASCII whitespace.
str = str.Trim(s_allowedWhitespace.AsSpan(0, 2));

在 .NET 9 预览版 6 之前,将生成 "prefixinfix"以下代码。 对于 .NET 9 预览版 6 到 .NET 9 RC2,它改为生成 "prefixin"

internal static string TrimEnd(this string target, string suffix)
{
    if (target.EndsWith(suffix))
    {
        return target.Substring(0, target.Length - suffix.Length);
    }

    return target;
}

...
return "prefixinfixsuffix".TrimEnd("suffix");

新行为

以下显式使用数组切片的代码不再编译,因为它无法调用合适的重载:

private static readonly char[] s_allowedWhitespace = { ' ', '\t', '\u00A0', '\u2000' };

// Only remove the ASCII whitespace.
str = str.Trim(s_allowedWhitespace.AsSpan(0, 2));

具有扩展方法 string TrimEnd(this string target, this string suffix) 的代码现在的行为与 .NET 8 和早期版本中的行为相同。 也就是说,它产生 "prefixinfix"

中断性变更的类型

此项更改可能会影响二进制兼容性源兼容性

更改原因

许多项目都有扩展方法,这些方法在重新编译后体验行为更改。 这些新实例方法的负面影响被认为超过其积极利益。

重新编译针对 .NET 9 预览版 6、.NET 9 预览版 7、.NET 9 RC1 或 .NET 9 RC2 生成的任何项目。 如果项目编译时没有错误,则无需执行进一步的工作。 如果项目不再编译,请调整代码。 下面显示了一个可能的替换示例:

-private static ReadOnlySpan<char> s_trimChars = [ ';', ',', '.' ];
+private static readonly char[] s_trimChars = [ ';', ',', '.' ];

...

return input.Trim(s_trimChars);

受影响的 API

  • System.String.Trim(System.ReadOnlySpan{System.Char})
  • System.String.TrimEnd(System.ReadOnlySpan{System.Char})
  • System.String.TrimStart(System.ReadOnlySpan{System.Char})