From 72e7857119ad2b0c3b6c419e8c56817939cb4ae0 Mon Sep 17 00:00:00 2001 From: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Date: Tue, 5 Nov 2024 10:44:06 -0800 Subject: [PATCH 1/5] string.trim breaking change --- docs/core/compatibility/9.0.md | 1 + .../core-libraries/9.0/string-trim.md | 123 ++++++++++++++++++ docs/core/compatibility/toc.yml | 4 + 3 files changed, 128 insertions(+) create mode 100644 docs/core/compatibility/core-libraries/9.0/string-trim.md diff --git a/docs/core/compatibility/9.0.md b/docs/core/compatibility/9.0.md index f27c23d9b0df5..cbc6d94c631d0 100644 --- a/docs/core/compatibility/9.0.md +++ b/docs/core/compatibility/9.0.md @@ -46,6 +46,7 @@ If you're migrating an app to .NET 9, the breaking changes listed here might aff | [InMemoryDirectoryInfo prepends rootDir to files](core-libraries/9.0/inmemorydirinfo-prepends-rootdir.md) | Behavioral change | Preview 1 | | [New TimeSpan.From*() overloads that take integers](core-libraries/9.0/timespan-from-overloads.md) | Source incompatible | Preview 3 | | [RuntimeHelpers.GetSubArray returns different type](core-libraries/9.0/getsubarray-return.md) | Behavioral change | Preview 1 | +| [String.Trim(params ReadOnlySpan\) overload removed](core-libraries/9.0/string-trim.md) | Source/binary incompatible | GA | | [Support for empty environment variables](core-libraries/9.0/empty-env-variable.md) | Behavioral change | Preview 6 | | [ZipArchiveEntry names and comments respect UTF8 flag](core-libraries/9.0/ziparchiveentry-encoding.md) | Behavioral change | RC 1 | diff --git a/docs/core/compatibility/core-libraries/9.0/string-trim.md b/docs/core/compatibility/core-libraries/9.0/string-trim.md new file mode 100644 index 0000000000000..f2fbc058a41f2 --- /dev/null +++ b/docs/core/compatibility/core-libraries/9.0/string-trim.md @@ -0,0 +1,123 @@ +--- +title: "Breaking change: String.Trim(params ReadOnlySpan) removed" +description: Learn about the breaking change in core .NET libraries where the String.Trim(params ReadOnlySpan) method has been removed due to potential behavioral changes. +ms.date: 11/5/2024 +ai-usage: ai-assisted +--- + +# String.Trim(params ReadOnlySpan\) overload removed + +`ReadOnlySpan` is used for two different things in the .NET ecosystem: + +- A specific sequence of characters, often as a slice of a larger instance. +- A collection of single characters, often as a slice of a `char[]`. + +Earlier releases of .NET 9 added `params ReadOnlySpan` overloads to method groups that already had a `params T[]` overload. While this sounds like pure goodness, the dual nature of `ReadOnlySpan` can lead to confusion in the case where a single method group accepts a `char[]` and a (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` has to decide if it is string-like or array-like. Generally speaking, .NET conforms to the array-like behavior. + +Consider the following new overloads that accept a `ReadOnlySpan` argument as proposed in [dotnet/runtime#77873](https://github.com/dotnet/runtime/issues/77873): + +```csharp +public string[] Split(params ReadOnlySpan separator); +public string Trim(params ReadOnlySpan trimChars); +public string TrimStart(params ReadOnlySpan trimChars); +public string TrimEnd(params ReadOnlySpan trimChars); +``` + +In addition, consider the following commonly defined extension method: + +```csharp +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; + } +} +``` + +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 method 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). + +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. + +Callers of 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)`. When you rebuild against the GA release of .NET 9, the compiler will automatically switch back to the `char[]` overload. + +Callers of who explicitly pass in a `ReadOnlySpan` (or a type that's convertible to `ReadOnlySpan` that's not also convertible to `char[]`) must change their code to successfully call `Trim` after this change. + +As for , unlike with , 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` overload. For this reason, the new overload of was preserved. + +> [!NOTE] +> 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 at run time. + +## Version introduced + +.NET 9 GA + +## Previous behavior + +The following code compiled in .NET 9 Preview 6, .NET 9 Preview 7, .NET 9 RC1, and .NET 9 RC2: + +```csharp +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, the following code yielded `"prefixinfix"`. For .NET 9 Preview 6 through .NET 9 RC2, it instead yielded `"prefixin"`: + +```csharp +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 following code that explicitly uses a slice of an array no longer compiles, as there's no suitable overload for it to call: + +```csharp +private static readonly char[] s_allowedWhitespace = { ' ', '\t', '\u00A0', '\u2000' }; + +// Only remove the ASCII whitespace. +str = str.Trim(s_allowedWhitespace.AsSpan(0, 2)); +``` + +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"`. + +## Type of breaking change + +This change can affect [binary compatibility](../../categories.md#binary-compatibility) and [source compatibility](../../categories.md#source-compatibility). + +## Reason for change + +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. + +## 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, adjust your code. One possible substitution example is shown here: + +```csharp +-private static ReadOnlySpan s_trimChars = [ ';', ',', '.' ]; ++private static readonly char[] s_trimChars = [ ';', ',', '.' ]; + +... + +return input.Trim(s_trimChars); +``` + +## Affected APIs + +- +- +- diff --git a/docs/core/compatibility/toc.yml b/docs/core/compatibility/toc.yml index 46ab742692d9b..800e037ac3c21 100644 --- a/docs/core/compatibility/toc.yml +++ b/docs/core/compatibility/toc.yml @@ -46,6 +46,8 @@ items: href: core-libraries/9.0/timespan-from-overloads.md - name: RuntimeHelpers.GetSubArray returns different type href: core-libraries/9.0/getsubarray-return.md + - name: String.Trim(params ReadOnlySpan) overload removed + href: core-libraries/9.0/string-trim.md - name: Support for empty environment variables href: core-libraries/9.0/empty-env-variable.md - name: ZipArchiveEntry names and comments respect UTF8 flag @@ -1278,6 +1280,8 @@ items: href: core-libraries/9.0/timespan-from-overloads.md - name: RuntimeHelpers.GetSubArray returns different type href: core-libraries/9.0/getsubarray-return.md + - name: String.Trim(params ReadOnlySpan) overload removed + href: core-libraries/9.0/string-trim.md - name: Support for empty environment variables href: core-libraries/9.0/empty-env-variable.md - name: ZipArchiveEntry names and comments respect UTF8 flag From b3d02804d290ef958f192aec7d99cca2b3046ac8 Mon Sep 17 00:00:00 2001 From: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:08:53 -0800 Subject: [PATCH 2/5] some tweaks --- .../compatibility/core-libraries/9.0/string-trim.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/core/compatibility/core-libraries/9.0/string-trim.md b/docs/core/compatibility/core-libraries/9.0/string-trim.md index f2fbc058a41f2..0879f542c377e 100644 --- a/docs/core/compatibility/core-libraries/9.0/string-trim.md +++ b/docs/core/compatibility/core-libraries/9.0/string-trim.md @@ -1,13 +1,13 @@ --- -title: "Breaking change: String.Trim(params ReadOnlySpan) removed" -description: Learn about the breaking change in core .NET libraries where the String.Trim(params ReadOnlySpan) method has been removed due to potential behavioral changes. +title: "Breaking change: String.Trim*(params ReadOnlySpan) overloads removed" +description: Learn about the breaking change in core .NET libraries where the String.Trim*(params ReadOnlySpan) methods have been removed due to potential behavioral changes. ms.date: 11/5/2024 ai-usage: ai-assisted --- -# String.Trim(params ReadOnlySpan\) overload removed +# String.Trim*(params ReadOnlySpan\) overloads removed -`ReadOnlySpan` is used for two different things in the .NET ecosystem: +In the .NET ecosystem, `ReadOnlySpan` can represent: - A specific sequence of characters, often as a slice of a larger instance. - A collection of single characters, often as a slice of a `char[]`. @@ -37,7 +37,7 @@ public static class SomeExtensions { } ``` -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 method 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). +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). 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. From c6cf77f6b3cf7858a43833748456e3d5c152abba Mon Sep 17 00:00:00 2001 From: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:20:18 -0800 Subject: [PATCH 3/5] don't use xrefs --- docs/core/compatibility/core-libraries/9.0/string-trim.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/core/compatibility/core-libraries/9.0/string-trim.md b/docs/core/compatibility/core-libraries/9.0/string-trim.md index 0879f542c377e..29058bdd8d427 100644 --- a/docs/core/compatibility/core-libraries/9.0/string-trim.md +++ b/docs/core/compatibility/core-libraries/9.0/string-trim.md @@ -118,6 +118,6 @@ return input.Trim(s_trimChars); ## Affected APIs -- -- -- +- `System.String.Trim(System.ReadOnlySpan{System.Char})` +- `System.String.TrimEnd(System.ReadOnlySpan{System.Char})` +- `System.String.TrimStart(System.ReadOnlySpan{System.Char})` From ebd5f2adee210eeb9abeeaf2a045d5c76044f4b6 Mon Sep 17 00:00:00 2001 From: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:30:27 -0800 Subject: [PATCH 4/5] remove ai-assisted metadata --- docs/core/compatibility/core-libraries/9.0/string-trim.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/core/compatibility/core-libraries/9.0/string-trim.md b/docs/core/compatibility/core-libraries/9.0/string-trim.md index 29058bdd8d427..5059c9dae3d31 100644 --- a/docs/core/compatibility/core-libraries/9.0/string-trim.md +++ b/docs/core/compatibility/core-libraries/9.0/string-trim.md @@ -2,7 +2,6 @@ title: "Breaking change: String.Trim*(params ReadOnlySpan) overloads removed" description: Learn about the breaking change in core .NET libraries where the String.Trim*(params ReadOnlySpan) methods have been removed due to potential behavioral changes. ms.date: 11/5/2024 -ai-usage: ai-assisted --- # String.Trim*(params ReadOnlySpan\) overloads removed From 02361ace200ea99c4ba953adfdc71a9ecf285aee Mon Sep 17 00:00:00 2001 From: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Date: Wed, 6 Nov 2024 07:55:19 -0800 Subject: [PATCH 5/5] Update docs/core/compatibility/core-libraries/9.0/string-trim.md --- docs/core/compatibility/core-libraries/9.0/string-trim.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core/compatibility/core-libraries/9.0/string-trim.md b/docs/core/compatibility/core-libraries/9.0/string-trim.md index 5059c9dae3d31..7a8c75eca7c58 100644 --- a/docs/core/compatibility/core-libraries/9.0/string-trim.md +++ b/docs/core/compatibility/core-libraries/9.0/string-trim.md @@ -11,7 +11,7 @@ In the .NET ecosystem, `ReadOnlySpan` can represent: - A specific sequence of characters, often as a slice of a larger instance. - A collection of single characters, often as a slice of a `char[]`. -Earlier releases of .NET 9 added `params ReadOnlySpan` overloads to method groups that already had a `params T[]` overload. While this sounds like pure goodness, the dual nature of `ReadOnlySpan` can lead to confusion in the case where a single method group accepts a `char[]` and a (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` has to decide if it is string-like or array-like. Generally speaking, .NET conforms to the array-like behavior. +Earlier releases of .NET 9 added `params ReadOnlySpan` 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` can cause confusion for a method group that accepts a `char[]` and a (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` has to decide if it is string-like or array-like. Generally speaking, .NET conforms to the array-like behavior. Consider the following new overloads that accept a `ReadOnlySpan` argument as proposed in [dotnet/runtime#77873](https://github.com/dotnet/runtime/issues/77873):