Skip to content

Commit d0a5a09

Browse files
authored
String.Trim breaking change (#43344)
1 parent 2ffa54a commit d0a5a09

File tree

3 files changed

+127
-0
lines changed

3 files changed

+127
-0
lines changed

docs/core/compatibility/9.0.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ If you're migrating an app to .NET 9, the breaking changes listed here might aff
4646
| [InMemoryDirectoryInfo prepends rootDir to files](core-libraries/9.0/inmemorydirinfo-prepends-rootdir.md) | Behavioral change | Preview 1 |
4747
| [New TimeSpan.From*() overloads that take integers](core-libraries/9.0/timespan-from-overloads.md) | Source incompatible | Preview 3 |
4848
| [RuntimeHelpers.GetSubArray returns different type](core-libraries/9.0/getsubarray-return.md) | Behavioral change | Preview 1 |
49+
| [String.Trim(params ReadOnlySpan\<char>) overload removed](core-libraries/9.0/string-trim.md) | Source/binary incompatible | GA |
4950
| [Support for empty environment variables](core-libraries/9.0/empty-env-variable.md) | Behavioral change | Preview 6 |
5051
| [ZipArchiveEntry names and comments respect UTF8 flag](core-libraries/9.0/ziparchiveentry-encoding.md) | Behavioral change | RC 1 |
5152

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
---
2+
title: "Breaking change: String.Trim*(params ReadOnlySpan<char>) overloads removed"
3+
description: Learn about the breaking change in core .NET libraries where the String.Trim*(params ReadOnlySpan<char>) methods have been removed due to potential behavioral changes.
4+
ms.date: 11/5/2024
5+
---
6+
7+
# String.Trim*(params ReadOnlySpan\<char>) overloads removed
8+
9+
In the .NET ecosystem, `ReadOnlySpan<char>` can represent:
10+
11+
- A specific sequence of characters, often as a slice of a larger <xref:System.String?displayProperty=fullName> instance.
12+
- A collection of single characters, often as a slice of a `char[]`.
13+
14+
Earlier releases of .NET 9 added `params ReadOnlySpan<T>` overloads to method groups that already had a `params T[]` overload. While this overload was a positive addition for some method groups, the dual nature of `ReadOnlySpan<char>` can cause confusion for a method group that accepts a `char[]` and a <xref:System.String> (in the same position) and they're treated differently. As an example, `public static string [String::]Split(string separator, StringSplitOptions options)` considers the sequence of characters as one separator. For example, `"[]ne]-[Tw[]".Split("]-[", StringSplitOptions.None)` splits into `new string[] { "[]ne", "Tw[]" };`. On the other hand, `public static [String::]Split(char[] separator, StringSplitOptions options)` considers each character in `separator` as a distinct separator, so the array-equivalent split yields `new string[] { "", "", "ne", "", "", "Tw", "", "" }`. Therefore, any new overload that accepts a `ReadOnlySpan<char>` has to decide if it is string-like or array-like. Generally speaking, .NET conforms to the array-like behavior.
15+
16+
Consider the following new <xref:System.String> overloads that accept a `ReadOnlySpan<char>` argument as proposed in [dotnet/runtime#77873](https://github.com/dotnet/runtime/issues/77873):
17+
18+
```csharp
19+
public string[] Split(params ReadOnlySpan<char> separator);
20+
public string Trim(params ReadOnlySpan<char> trimChars);
21+
public string TrimStart(params ReadOnlySpan<char> trimChars);
22+
public string TrimEnd(params ReadOnlySpan<char> trimChars);
23+
```
24+
25+
In addition, consider the following commonly defined extension method:
26+
27+
```csharp
28+
public static class SomeExtensions {
29+
public static string TrimEnd(this string target, string trimString) {
30+
if (target.EndsWith(trimString) {
31+
return target.Substring(0, target.Length - trimString.Length);
32+
}
33+
34+
return target;
35+
}
36+
}
37+
```
38+
39+
For existing .NET runtimes, this extension method removes the specified sequence from the end of the string. However, due to the overload resolution rules of C#, `"12345!!!!".TrimEnd("!!!")` will prefer the new `TrimEnd` overload over the existing extension method, and change the result from `"12345!"` (removing only a full set of three exclamation marks) to `"12345"` (removing all exclamation marks from the end).
40+
41+
To resolve this break, there were two possible paths: Introduce an instance method `public string TrimEnd(string trimString)` that's an even better target, or 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 with existing code that uses each approach. Therefore, the second option was the most appropriate choice for this stage of the release cycle.
42+
43+
Callers of <xref:System.String.Trim*?displayProperty=nameWithType> who pass in individual characters using the `params` feature, for example, `str.Trim(';', ',', '.')`, won't see a break. Your code will have automatically switched from calling `string.Trim(params char[])` to `string.Trim(params ReadOnlySpan<char>)`. When you rebuild against the GA release of .NET 9, the compiler will automatically switch back to the `char[]` overload.
44+
45+
Callers of <xref:System.String.Trim*?displayProperty=nameWithType> who explicitly pass in a `ReadOnlySpan<char>` (or a type that's convertible to `ReadOnlySpan<char>` that's not also convertible to `char[]`) must change their code to successfully call `Trim` after this change.
46+
47+
As for <xref:System.String.Split*?displayProperty=nameWithType>, unlike with <xref:System.String.Trim*?displayProperty=nameWithType>, this method already has an [overload](xref:System.String.Split(System.String,System.StringSplitOptions)) that's both preferred over an extension method accepting a single string parameter and the newly added `ReadOnlySpan<char>` overload. For this reason, the new overload of <xref:System.String.Split*?displayProperty=nameWithType> was preserved.
48+
49+
> [!NOTE]
50+
> You should rebuild any assembly built against .NET 9 Preview 6, .NET 9 Preview 7, .NET 9 RC1, or .NET 9 RC2 to ensure that any calls to the removed method are removed. Failure to do so might result in a <xref:System.MissingMethodException> at run time.
51+
52+
## Version introduced
53+
54+
.NET 9 GA
55+
56+
## Previous behavior
57+
58+
The following code compiled in .NET 9 Preview 6, .NET 9 Preview 7, .NET 9 RC1, and .NET 9 RC2:
59+
60+
```csharp
61+
private static readonly char[] s_allowedWhitespace = { ' ', '\t', '\u00A0', '\u2000' };
62+
63+
// Only remove the ASCII whitespace.
64+
str = str.Trim(s_allowedWhitespace.AsSpan(0, 2));
65+
```
66+
67+
Prior to .NET 9 Preview 6, the following code yielded `"prefixinfix"`. For .NET 9 Preview 6 through .NET 9 RC2, it instead yielded `"prefixin"`:
68+
69+
```csharp
70+
internal static string TrimEnd(this string target, string suffix)
71+
{
72+
if (target.EndsWith(suffix))
73+
{
74+
return target.Substring(0, target.Length - suffix.Length);
75+
}
76+
77+
return target;
78+
}
79+
80+
...
81+
return "prefixinfixsuffix".TrimEnd("suffix");
82+
```
83+
84+
## New behavior
85+
86+
The following code that explicitly uses a slice of an array no longer compiles, as there's no suitable overload for it to call:
87+
88+
```csharp
89+
private static readonly char[] s_allowedWhitespace = { ' ', '\t', '\u00A0', '\u2000' };
90+
91+
// Only remove the ASCII whitespace.
92+
str = str.Trim(s_allowedWhitespace.AsSpan(0, 2));
93+
```
94+
95+
Code that features an extension method `string TrimEnd(this string target, this string suffix)` now has the same behavior it had in .NET 8 and previous versions. That is, it yields `"prefixinfix"`.
96+
97+
## Type of breaking change
98+
99+
This change can affect [binary compatibility](../../categories.md#binary-compatibility) and [source compatibility](../../categories.md#source-compatibility).
100+
101+
## Reason for change
102+
103+
Many projects have extension methods that experience behavioral changes after recompiling. The negative impact of these new instance methods was deemed to outweigh their positive benefit.
104+
105+
## Recommended action
106+
107+
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, adjust your code. One possible substitution example is shown here:
108+
109+
```csharp
110+
-private static ReadOnlySpan<char> s_trimChars = [ ';', ',', '.' ];
111+
+private static readonly char[] s_trimChars = [ ';', ',', '.' ];
112+
113+
...
114+
115+
return input.Trim(s_trimChars);
116+
```
117+
118+
## Affected APIs
119+
120+
- `System.String.Trim(System.ReadOnlySpan{System.Char})`
121+
- `System.String.TrimEnd(System.ReadOnlySpan{System.Char})`
122+
- `System.String.TrimStart(System.ReadOnlySpan{System.Char})`

docs/core/compatibility/toc.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ items:
4646
href: core-libraries/9.0/timespan-from-overloads.md
4747
- name: RuntimeHelpers.GetSubArray returns different type
4848
href: core-libraries/9.0/getsubarray-return.md
49+
- name: String.Trim(params ReadOnlySpan<char>) overload removed
50+
href: core-libraries/9.0/string-trim.md
4951
- name: Support for empty environment variables
5052
href: core-libraries/9.0/empty-env-variable.md
5153
- name: ZipArchiveEntry names and comments respect UTF8 flag
@@ -1290,6 +1292,8 @@ items:
12901292
href: core-libraries/9.0/timespan-from-overloads.md
12911293
- name: RuntimeHelpers.GetSubArray returns different type
12921294
href: core-libraries/9.0/getsubarray-return.md
1295+
- name: String.Trim(params ReadOnlySpan<char>) overload removed
1296+
href: core-libraries/9.0/string-trim.md
12931297
- name: Support for empty environment variables
12941298
href: core-libraries/9.0/empty-env-variable.md
12951299
- name: ZipArchiveEntry names and comments respect UTF8 flag

0 commit comments

Comments
 (0)