Description
Description
ReadOnlySpan<char>
in the .NET ecosystem is used for two different things, 1) a specific sequence of characters, often as a slice of a larger System.String
instance, 2) a collection of single characters, often as a slice of a char[]
.
In .NET 9 we undertook a change wherein we added params ReadOnlySpan<T>
overloads to method groups that already had a params T[]
overload. While this sounds like pure goodness, the dual nature of ReadOnlySpan<char>
leads to some potential confusion if there is ever a case where a single method group accepts a char[]
and a string
(in the same position) and they are treated differently. Is there ever such a case? Sure, a quite famous one: public static string [String::]Split(string separator, StringSplitOptions options)
considers the sequence of characters as one separator, e.g. "[]ne]-[Tw[]".Split("]-[", StringSplitOptions.None)
splits into new string[] { "[]ne", "Tw[]" }
; whereas public static [String::]Split(char[] separator, StringSplitOptions options)
considers each character as a distinct separator, and so the array-equivalent split yields new string[] { "", "", "ne", "", "", "Tw", "", "" }
. Therefore, any new overload accepting a ReadOnlySpan<char>
has to decide if it is string-like, or array-like. Generally speaking, we conform to the array-like behavior.
dotnet/runtime#77873 proposed the following new overloads accepting ReadOnlySpan<char>
:
public partial class String
{
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);
}
It’s fairly trivial to find cases where users have defined extension methods like
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;
}
}
which for existing .NET runtimes will remove the specific sequence from the end. Due to the overload resolution rules of C#, "12345....".TrimEnd("...")
will prefer the new method over the existing extension method, and change the result from "12345."
(removing only a full set of three periods) to "12345"
(removing all periods from the end). To resolve this break, there are two possible paths: 1) introduce an instance method public string TrimEnd(string trimString)
that is an even better target, or 2) remove the new method. The first option carries additional risk, as it needs to decide whether it returns one instance of the target string or all of them – and there are undoubtedly callers who have existing code doing each approach; therefore, the second is the most appropriate choice for this stage of the release cycle.
Callers of string.Trim who pass in individual characters using the params
feature, e.g. str.Trim(';', ',', '.')
will have automatically switched from calling string.Trim(params char[])
to string.Trim(params ReadOnlySpan<char>)
. Rebuilding against the GA build of .NET 9 will automatically switch them back to the char[]
overload.
Callers of string.Trim who explicitly pass in a ReadOnlySpan<char>
(or a type that is convertible to ReadOnlySpan<char>
that is not also convertible to char[]
) will have to change their code in order to successfully call Trim after this change.
Unlike with string.Trim
, string.Split
already has an overload that is both preferred over an extension method accepting a single string parameter and the newly added ReadOnlySpan<char>
overload (string Split(string separator, StringSplitOptions options = StringSplitOptions.None)
). Therefore the new overload of string.Split will remain in .NET 9 GA.
Note: Any assembly built against .NET 9 Preview 6, .NET 9 Preview 7, .NET 9 RC1, or .NET 9 RC2 has to be rebuilt to ensure that any calls to the removed method have been removed. Failure to do so may result in a MissingMethodException
at runtime.
Version
.NET 9 GA
Previous behavior
The following code would compile in .NET 9 Preview 6, .NET 9 Preview 7, .NET 9 RC1, and .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));
Prior to .NET 9 Preview 6, this code will yield "prefixinfix"
. For .NET 9 Preview 6 through .NET 9 RC2 it will instead yield "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");
New behavior
The code explicitly using a slice of an array will no longer compile, as there is no longer a suitable overload for it to call.
However, the code which features an extension method string TrimEnd(this string target, this string suffix)
will return to the behavior it had in .NET 8 and previous versions.
Type of breaking change
- Binary incompatible: Existing binaries might encounter a breaking change in behavior, such as failure to load or execute, and if so, require recompilation.
- Source incompatible: When recompiled using the new SDK or component or to target the new runtime, existing source code might require source changes to compile successfully.
- Behavioral change: Existing binaries might behave differently at run time.
Reason for change
Many projects have extension methods as described, and those projects experience behavioral changes after recompiling. The negative impact of these new instance methods was deemed to outweigh their positive benefit.
Recommended action
Recompile any projects that were built against .NET 9 Preview 6, .NET 9 Preview 7, .NET 9 RC1, or .NET 9 RC2. If the project compiles with no errors no further work is required. If the project no longer compiles then you need to adjust your code. One possible substitution example is shown here:
-private static ReadOnlySpan<char> s_trimChars = [ ';', ',', '.' ];
+private static readonly char[] s_trimChars = [ ';', ',', '.' ];
...
return input.Trim(s_trimChars);
Feature area
Core .NET libraries
Affected APIs
- System.String.Trim(ReadOnlySpan{char})
- System.String.TrimEnd(ReadOnlySpan{char})
- System.String.TrimStart(ReadOnlySpan{char})