From 086f1baf559c8418a4ac9c8a286df1e453caae61 Mon Sep 17 00:00:00 2001 From: shsat94 Date: Tue, 16 Dec 2025 23:04:16 +0530 Subject: [PATCH 01/10] Document SearchValues usage for efficient multi-value string comparisons --- .../string-comparison-net-5-plus.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/standard/base-types/string-comparison-net-5-plus.md b/docs/standard/base-types/string-comparison-net-5-plus.md index 5ea0b746a766f..b1f2a1a403d67 100644 --- a/docs/standard/base-types/string-comparison-net-5-plus.md +++ b/docs/standard/base-types/string-comparison-net-5-plus.md @@ -331,6 +331,38 @@ ReadOnlySpan span = s.AsSpan(); if (span.StartsWith("Hello", StringComparison.Ordinal)) { /* do something */ } // ordinal comparison ``` +## Efficient multi-value string comparisons + +When comparing a string against a fixed set of known values repeatedly, consider using `SearchValues` instead of chained comparisons or LINQ-based approaches. +`SearchValues` can precompute internal lookup structures and optimize the comparison logic based on the provided values. + +### Recommended usage + +Create and cache the `SearchValues` instance once, then reuse it for comparisons: + +```cs +using System.Buffers; + +private static readonly SearchValues Commands = SearchValues.Create(new[] { "start", "run", "go" }, StringComparison.OrdinalIgnoreCase); + +if (Commands.Contains(command)) +{ + /* do something */ +} +``` +### Avoid repeated creation +Avoid creating `SearchValues` instances inside hot paths or per comparison, as the creation step can be relatively expensive: + +```cs +// Avoid this pattern +if (SearchValues.Create(new[] { "start", "run", "go" }, StringComparison.OrdinalIgnoreCase).Contains(command)) +{ + /* do something */ +} +``` +Caching and reusing the instance ensures optimal performance. + + ## See also - [Globalization breaking changes in .NET 5](../../core/compatibility/5.0.md#globalization) From 41907bc47e496651746c8ba976d3fe35eff7fa53 Mon Sep 17 00:00:00 2001 From: "satyarth.sharma" Date: Wed, 17 Dec 2025 17:08:40 +0530 Subject: [PATCH 02/10] Link SearchValues performance docs from .NET 8 runtime notes --- docs/core/whats-new/dotnet-8/runtime.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/core/whats-new/dotnet-8/runtime.md b/docs/core/whats-new/dotnet-8/runtime.md index fe2f861165da6..9f528757df97f 100644 --- a/docs/core/whats-new/dotnet-8/runtime.md +++ b/docs/core/whats-new/dotnet-8/runtime.md @@ -513,7 +513,8 @@ IDataView predictions = model.Transform(split.TestSet); } ``` -- Methods like look for the first occurrence of *any value in the passed collection*. The new type is designed to be passed to such methods. Correspondingly, .NET 8 adds new overloads of methods like that accept an instance of the new type. When you create an instance of , all the data that's necessary to optimize subsequent searches is derived *at that time*, meaning the work is done up front. +- Methods like look for the first occurrence of *any value in the passed collection*. The new type is designed to be passed to such methods. Correspondingly, .NET 8 adds new overloads of methods like that accept an instance of the new type. When you create an instance of , all the data that's necessary to optimize subsequent searches is derived *at that time*, meaning the work is done up front. +For guidance on using `SearchValues` for efficient multi-value string comparisons, see [Efficient multi-value string comparisons](../../standard/base-types/string-comparison-net-5-plus.md#efficient-multi-value-string-comparisons). - The new type is useful for optimizing format strings that aren't known at compile time (for example, if the format string is loaded from a resource file). A little extra time is spent up front to do work such as parsing the string, but it saves the work from being done on each use. ```csharp From b4c105bab02c927dff905fc6ded124e8a196e9c8 Mon Sep 17 00:00:00 2001 From: "satyarth.sharma" Date: Wed, 17 Dec 2025 17:40:03 +0530 Subject: [PATCH 03/10] Fix link to canonical string comparison documentation --- docs/core/whats-new/dotnet-8/runtime.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core/whats-new/dotnet-8/runtime.md b/docs/core/whats-new/dotnet-8/runtime.md index 9f528757df97f..b2d4f61080cee 100644 --- a/docs/core/whats-new/dotnet-8/runtime.md +++ b/docs/core/whats-new/dotnet-8/runtime.md @@ -514,7 +514,7 @@ IDataView predictions = model.Transform(split.TestSet); ``` - Methods like look for the first occurrence of *any value in the passed collection*. The new type is designed to be passed to such methods. Correspondingly, .NET 8 adds new overloads of methods like that accept an instance of the new type. When you create an instance of , all the data that's necessary to optimize subsequent searches is derived *at that time*, meaning the work is done up front. -For guidance on using `SearchValues` for efficient multi-value string comparisons, see [Efficient multi-value string comparisons](../../standard/base-types/string-comparison-net-5-plus.md#efficient-multi-value-string-comparisons). +For guidance on using `SearchValues` for efficient multi-value string comparisons, see [Efficient multi-value string comparisons](../../standard/base-types/string-comparison.md#efficient-multi-value-string-comparisons). - The new type is useful for optimizing format strings that aren't known at compile time (for example, if the format string is loaded from a resource file). A little extra time is spent up front to do work such as parsing the string, but it saves the work from being done on each use. ```csharp From 40b0af0b22ba418a0a56640b07bd0de11f0e97f8 Mon Sep 17 00:00:00 2001 From: "satyarth.sharma" Date: Wed, 17 Dec 2025 18:20:23 +0530 Subject: [PATCH 04/10] Remove cross-link blocked by OPS rules --- docs/core/whats-new/dotnet-8/runtime.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/core/whats-new/dotnet-8/runtime.md b/docs/core/whats-new/dotnet-8/runtime.md index b2d4f61080cee..fe2f861165da6 100644 --- a/docs/core/whats-new/dotnet-8/runtime.md +++ b/docs/core/whats-new/dotnet-8/runtime.md @@ -513,8 +513,7 @@ IDataView predictions = model.Transform(split.TestSet); } ``` -- Methods like look for the first occurrence of *any value in the passed collection*. The new type is designed to be passed to such methods. Correspondingly, .NET 8 adds new overloads of methods like that accept an instance of the new type. When you create an instance of , all the data that's necessary to optimize subsequent searches is derived *at that time*, meaning the work is done up front. -For guidance on using `SearchValues` for efficient multi-value string comparisons, see [Efficient multi-value string comparisons](../../standard/base-types/string-comparison.md#efficient-multi-value-string-comparisons). +- Methods like look for the first occurrence of *any value in the passed collection*. The new type is designed to be passed to such methods. Correspondingly, .NET 8 adds new overloads of methods like that accept an instance of the new type. When you create an instance of , all the data that's necessary to optimize subsequent searches is derived *at that time*, meaning the work is done up front. - The new type is useful for optimizing format strings that aren't known at compile time (for example, if the format string is loaded from a resource file). A little extra time is spent up front to do work such as parsing the string, but it saves the work from being done on each use. ```csharp From 9573831d4ad5bca8f2fb8403e9c97fd4055e168f Mon Sep 17 00:00:00 2001 From: shsat94 Date: Wed, 17 Dec 2025 23:43:01 +0530 Subject: [PATCH 05/10] Clarify SearchValues introduction version and use xref --- docs/standard/base-types/string-comparison-net-5-plus.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/standard/base-types/string-comparison-net-5-plus.md b/docs/standard/base-types/string-comparison-net-5-plus.md index b1f2a1a403d67..070b2855da592 100644 --- a/docs/standard/base-types/string-comparison-net-5-plus.md +++ b/docs/standard/base-types/string-comparison-net-5-plus.md @@ -333,7 +333,7 @@ if (span.StartsWith("Hello", StringComparison.Ordinal)) { /* do something */ } / ## Efficient multi-value string comparisons -When comparing a string against a fixed set of known values repeatedly, consider using `SearchValues` instead of chained comparisons or LINQ-based approaches. +Starting with .NET 8, when comparing a string against a fixed set of known values repeatedly, consider using instead of chained comparisons or LINQ-based approaches. `SearchValues` can precompute internal lookup structures and optimize the comparison logic based on the provided values. ### Recommended usage @@ -362,7 +362,6 @@ if (SearchValues.Create(new[] { "start", "run", "go" }, StringComparison.Ordinal ``` Caching and reusing the instance ensures optimal performance. - ## See also - [Globalization breaking changes in .NET 5](../../core/compatibility/5.0.md#globalization) From 8056148476242032cadef3e9b7993dd86ba39131 Mon Sep 17 00:00:00 2001 From: shsat94 Date: Thu, 18 Dec 2025 09:12:17 +0530 Subject: [PATCH 06/10] Fix relative link to string comparison documentation --- docs/core/whats-new/dotnet-8/runtime.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/core/whats-new/dotnet-8/runtime.md b/docs/core/whats-new/dotnet-8/runtime.md index fe2f861165da6..ef5df4a8c1ec5 100644 --- a/docs/core/whats-new/dotnet-8/runtime.md +++ b/docs/core/whats-new/dotnet-8/runtime.md @@ -514,6 +514,8 @@ IDataView predictions = model.Transform(split.TestSet); ``` - Methods like look for the first occurrence of *any value in the passed collection*. The new type is designed to be passed to such methods. Correspondingly, .NET 8 adds new overloads of methods like that accept an instance of the new type. When you create an instance of , all the data that's necessary to optimize subsequent searches is derived *at that time*, meaning the work is done up front. +For guidance on using `SearchValues` for efficient multi-value string comparisons, see [Efficient multi-value string comparisons](../../../standard/base-types/string-comparison.md#efficient-multi-value-string-comparisons). + - The new type is useful for optimizing format strings that aren't known at compile time (for example, if the format string is loaded from a resource file). A little extra time is spent up front to do work such as parsing the string, but it saves the work from being done on each use. ```csharp From ddbbd8d2ece717e4f1987867a1a1d0ab164e9f6b Mon Sep 17 00:00:00 2001 From: shsat94 Date: Thu, 18 Dec 2025 09:24:39 +0530 Subject: [PATCH 07/10] Fix relative link to string comparison documentation --- docs/core/whats-new/dotnet-8/runtime.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core/whats-new/dotnet-8/runtime.md b/docs/core/whats-new/dotnet-8/runtime.md index ef5df4a8c1ec5..85cfbbadb7016 100644 --- a/docs/core/whats-new/dotnet-8/runtime.md +++ b/docs/core/whats-new/dotnet-8/runtime.md @@ -514,7 +514,7 @@ IDataView predictions = model.Transform(split.TestSet); ``` - Methods like look for the first occurrence of *any value in the passed collection*. The new type is designed to be passed to such methods. Correspondingly, .NET 8 adds new overloads of methods like that accept an instance of the new type. When you create an instance of , all the data that's necessary to optimize subsequent searches is derived *at that time*, meaning the work is done up front. -For guidance on using `SearchValues` for efficient multi-value string comparisons, see [Efficient multi-value string comparisons](../../../standard/base-types/string-comparison.md#efficient-multi-value-string-comparisons). +For guidance on using `SearchValues` for efficient multi-value string comparisons, see [Efficient multi-value string comparisons](../../../standard/base-types/string-comparison-net-5-plus.md#efficient-multi-value-string-comparisons). - The new type is useful for optimizing format strings that aren't known at compile time (for example, if the format string is loaded from a resource file). A little extra time is spent up front to do work such as parsing the string, but it saves the work from being done on each use. From 96b7616c80cdee0ab2d0e5cacae9fc9dca4814ea Mon Sep 17 00:00:00 2001 From: shsat94 Date: Thu, 18 Dec 2025 09:34:32 +0530 Subject: [PATCH 08/10] Revert to original runtime.md --- docs/core/whats-new/dotnet-8/runtime.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/core/whats-new/dotnet-8/runtime.md b/docs/core/whats-new/dotnet-8/runtime.md index 85cfbbadb7016..7d826930e87c6 100644 --- a/docs/core/whats-new/dotnet-8/runtime.md +++ b/docs/core/whats-new/dotnet-8/runtime.md @@ -514,7 +514,6 @@ IDataView predictions = model.Transform(split.TestSet); ``` - Methods like look for the first occurrence of *any value in the passed collection*. The new type is designed to be passed to such methods. Correspondingly, .NET 8 adds new overloads of methods like that accept an instance of the new type. When you create an instance of , all the data that's necessary to optimize subsequent searches is derived *at that time*, meaning the work is done up front. -For guidance on using `SearchValues` for efficient multi-value string comparisons, see [Efficient multi-value string comparisons](../../../standard/base-types/string-comparison-net-5-plus.md#efficient-multi-value-string-comparisons). - The new type is useful for optimizing format strings that aren't known at compile time (for example, if the format string is loaded from a resource file). A little extra time is spent up front to do work such as parsing the string, but it saves the work from being done on each use. From 3af2ff9bfb470e68c8e11a0e3c68dea90cbbda70 Mon Sep 17 00:00:00 2001 From: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Date: Mon, 22 Dec 2025 10:19:41 -0800 Subject: [PATCH 09/10] move to different article --- docs/core/whats-new/dotnet-8/runtime.md | 2 +- docs/csharp/how-to/search-strings.md | 16 ++-- .../code-analysis/quality-rules/ca1870.md | 6 +- .../base-types/best-practices-strings.md | 64 ++++++++----- .../string-comparison-net-5-plus.md | 89 ++++++------------- 5 files changed, 85 insertions(+), 92 deletions(-) diff --git a/docs/core/whats-new/dotnet-8/runtime.md b/docs/core/whats-new/dotnet-8/runtime.md index 7d826930e87c6..736898b2d3038 100644 --- a/docs/core/whats-new/dotnet-8/runtime.md +++ b/docs/core/whats-new/dotnet-8/runtime.md @@ -513,7 +513,7 @@ IDataView predictions = model.Transform(split.TestSet); } ``` -- Methods like look for the first occurrence of *any value in the passed collection*. The new type is designed to be passed to such methods. Correspondingly, .NET 8 adds new overloads of methods like that accept an instance of the new type. When you create an instance of , all the data that's necessary to optimize subsequent searches is derived *at that time*, meaning the work is done up front. +- Methods like look for the first occurrence of *any value in the passed collection*. The new type is designed to be passed to such methods. Correspondingly, .NET 8 adds new overloads of methods like that accept an instance of the new type. When you create an instance of , all the data that's necessary to optimize subsequent searches is derived *at that time*, meaning the work is done up front. (The `SearchValues` type was expanded in .NET 9. For more information, see [`SearchValues` expansion](../dotnet-9/libraries.md#searchvalues-expansion).) - The new type is useful for optimizing format strings that aren't known at compile time (for example, if the format string is loaded from a resource file). A little extra time is spent up front to do work such as parsing the string, but it saves the work from being done on each use. diff --git a/docs/csharp/how-to/search-strings.md b/docs/csharp/how-to/search-strings.md index b64dcf89d6e50..cb3350d36668d 100644 --- a/docs/csharp/how-to/search-strings.md +++ b/docs/csharp/how-to/search-strings.md @@ -2,7 +2,7 @@ title: "How to search strings" description: Learn about two strategies to search for text in strings in C#. String class methods search for specific text. Regular expressions search for patterns in text. ms.date: 02/18/2025 -helpviewer_keywords: +helpviewer_keywords: - "searching strings [C#]" - "strings [C#], searching with String methods" - "strings [C#], searching with regular expressions" @@ -51,13 +51,13 @@ The search pattern describes the text you search for. The following table descri The following code uses regular expressions to validate the format of each string in an array. The validation requires that each string is formatted as a telephone number: three groups of digits separated by dashes where the first two groups contain three digits and the third group contains four digits. The search pattern uses the regular expression `^\\d{3}-\\d{3}-\\d{4}$`. For more information, see [Regular Expression Language - Quick Reference](../../standard/base-types/regular-expression-language-quick-reference.md). -| Pattern | Meaning | -|---------|-------------------------------------| -| `^` | matches the beginning of the string | -| `\d{3}` | matches exactly three digit characters | -| `-` | matches the '-' character | -| `\d{4}` | matches exactly four digit characters | -| `$` | matches the end of the string | +| Pattern | Meaning | +|---------|----------------------------------------| +| `^` | Matches the beginning of the string | +| `\d{3}` | Matches exactly three digit characters | +| `-` | Matches the '-' character | +| `\d{4}` | Matches exactly four digit characters | +| `$` | Matches the end of the string | :::code language="csharp" source="./snippets/\strings/SearchStrings.cs" id="Snippet4"::: diff --git a/docs/fundamentals/code-analysis/quality-rules/ca1870.md b/docs/fundamentals/code-analysis/quality-rules/ca1870.md index 1367e4f56ac1c..4cd47c3db6344 100644 --- a/docs/fundamentals/code-analysis/quality-rules/ca1870.md +++ b/docs/fundamentals/code-analysis/quality-rules/ca1870.md @@ -21,7 +21,7 @@ ms.author: mizupan | **Title** | Use a cached 'SearchValues' instance | | **Category** | [Performance](performance-warnings.md) | | **Fix is breaking or non-breaking** | Non-breaking | -| **Enabled by default in .NET 10** | As suggestion | +| **Enabled by default in .NET 10** | As suggestion | ## Cause @@ -97,3 +97,7 @@ dotnet_diagnostic.CA1870.severity = none ``` For more information, see [How to suppress code analysis warnings](../suppress-warnings.md). + +## See also + +- [`MemoryExtensions.AsSpan.IndexOfAny` and the `SearchValues` type](../../../standard/base-types/best-practices-strings.md#memoryextensionsasspanindexofany-and-the-searchvaluest-type) diff --git a/docs/standard/base-types/best-practices-strings.md b/docs/standard/base-types/best-practices-strings.md index 9e34506a29125..42c7a2532f2e1 100644 --- a/docs/standard/base-types/best-practices-strings.md +++ b/docs/standard/base-types/best-practices-strings.md @@ -17,7 +17,6 @@ helpviewer_keywords: - "string sorting" - "comparing strings" - "strings [.NET],comparing" -ms.assetid: b9f0bf53-e2de-4116-8ce9-d4f91a1df4f7 --- # Best practices for comparing strings in .NET @@ -48,18 +47,18 @@ Avoid the following practices when you compare strings: - Don't use string operations based on in most cases. One of the few exceptions is when you're persisting linguistically meaningful but culturally agnostic data. - Don't use an overload of the or method and test for a return value of zero to determine whether two strings are equal. -## Specifying string comparisons explicitly +## Specify string comparisons explicitly Most of the string manipulation methods in .NET are overloaded. Typically, one or more overloads accept default settings, whereas others accept no defaults and instead define the precise way in which strings are to be compared or manipulated. Most of the methods that don't rely on defaults include a parameter of type , which is an enumeration that explicitly specifies rules for string comparison by culture and case. The following table describes the enumeration members. -|StringComparison member|Description| -|-----------------------------|-----------------| -||Performs a case-sensitive comparison using the current culture.| -||Performs a case-insensitive comparison using the current culture.| -||Performs a case-sensitive comparison using the invariant culture.| -||Performs a case-insensitive comparison using the invariant culture.| -||Performs an ordinal comparison.| -||Performs a case-insensitive ordinal comparison.| +| `StringComparison` member | Description | +|---------------------------------------------------------|-------------------------------------------------------------------| +| | Performs a case-sensitive comparison using the current culture. | +| | Performs a case-insensitive comparison using the current culture. | +| | Performs a case-sensitive comparison using the invariant culture. | +| | Performs a case-insensitive comparison using the invariant culture. | +| | Performs an ordinal comparison. | +| | Performs a case-insensitive ordinal comparison. | For example, the method, which returns the index of a substring in a object that matches either a character or a string, has nine overloads: @@ -188,12 +187,12 @@ When interpreting file names, cookies, or anything else where a combination such On balance, the invariant culture has few properties that make it useful for comparison. It does comparison in a linguistically relevant manner, which prevents it from guaranteeing full symbolic equivalence, but it isn't the choice for display in any culture. One of the few reasons to use for comparison is to persist ordered data for a cross-culturally identical display. For example, if a large data file that contains a list of sorted identifiers for display accompanies an application, adding to this list would require an insertion with invariant-style sorting. -## Choosing a StringComparison member for your method call +## How to choose a StringComparison member The following table outlines the mapping from semantic string context to a enumeration member: -|Data|Behavior|Corresponding System.StringComparison

value| -|----------|--------------|-----------------------------------------------------| +| Data | Behavior | Corresponding System.StringComparison

value | +|------|----------|---------------------------------------------------------| |Case-sensitive internal identifiers.

Case-sensitive identifiers in standards such as XML and HTTP.

Case-sensitive security-related settings.|A non-linguistic identifier, where bytes match exactly.|| |Case-insensitive internal identifiers.

Case-insensitive identifiers in standards such as XML and HTTP.

File paths.

Registry keys and values.

Environment variables.

Resource identifiers (for example, handle names).

Case-insensitive security-related settings.|A non-linguistic identifier, where case is irrelevant.|| |Some persisted, linguistically relevant data.

Display of linguistic data that requires a fixed sort order.|Culturally agnostic data that still is linguistically relevant.|

-or-

| @@ -203,7 +202,7 @@ The following table outlines the mapping from semantic string context to a . @@ -211,7 +210,7 @@ As the operation most central to string interpretation, all instances of these m The class, which is returned by the property, also includes a method that provides a large number of matching options (ordinal, ignoring white space, ignoring kana type, and so on) by means of the flag enumeration. -### String.CompareTo +### `String.CompareTo` Default interpretation: . @@ -222,13 +221,13 @@ Types that implement the and . The class lets you test for equality by calling either the static or instance method overloads, or by using the static equality operator. The overloads and operator use ordinal comparison by default. However, we still recommend that you call an overload that explicitly specifies the type even if you want to perform an ordinal comparison; this makes it easier to search code for a certain string interpretation. -### String.ToUpper and String.ToLower +### `String.ToUpper` and `String.ToLower` Default interpretation: . @@ -238,19 +237,19 @@ The and object that represents that culture to the method. -### Char.ToUpper and Char.ToLower +### `Char.ToUpper` and `Char.ToLower` Default interpretation: . The and methods work similarly to the and methods described in the previous section. -### String.StartsWith and String.EndsWith +### `String.StartsWith` and `String.EndsWith` Default interpretation: . By default, both of these methods perform a culture-sensitive comparison. In particular, they may ignore non-printing characters. -### String.IndexOf and String.LastIndexOf +### `String.IndexOf` and `String.LastIndexOf` Default interpretation: . @@ -258,6 +257,27 @@ There's a lack of consistency in how the default overloads of these methods perf If you call the or method and pass it a string to locate in the current instance, we recommend that you call an overload that explicitly specifies the type. The overloads that include a argument don't allow you to specify a type. +### `MemoryExtensions.AsSpan.IndexOfAny` and the `SearchValues` type + +.NET 8 introduced the type, which provides an optimized solution for searching for specific sets of characters or bytes within spans. + +If you're comparing a string against a fixed set of known values repeatedly, consider using the method instead of chained comparisons or LINQ-based approaches. `SearchValues` can precompute internal lookup structures and optimize the comparison logic based on the provided values. To see performance benefits, create and cache the `SearchValues` instance once, then reuse it for comparisons: + +```csharp +using System.Buffers; + +private static readonly SearchValues Commands = SearchValues.Create( + new[] { "start", "run", "go", "begin", "commence" }, + StringComparison.OrdinalIgnoreCase); + +if (Commands.Contains(command)) +{ + // ... +} +``` + +In .NET 9, `SearchValues` was extended to support searching for substrings within a larger string. For an example, see [`SearchValues` expansion](../../core/whats-new/dotnet-9/libraries.md#searchvalues-expansion). + ## Methods that perform string comparison indirectly Some non-string methods that have string comparison as a central operation use the type. The class includes six static properties that return instances whose methods perform the following types of string comparisons: @@ -269,7 +289,7 @@ Some non-string methods that have string comparison as a central operation use t - Ordinal comparison. This object is returned by the property. - Case-insensitive ordinal comparison. This object is returned by the property. -### Array.Sort and Array.BinarySearch +### `Array.Sort` and `Array.BinarySearch` Default interpretation: . @@ -288,7 +308,7 @@ If this data is persisted and moved across cultures, and sorting is used to pres :::code language="csharp" source="./snippets/best-practices-strings/csharp/indirect1/binarysearch.cs" id="invariant" highlight="11,15"::: :::code language="vb" source="./snippets/best-practices-strings/vb/indirect1/binarysearch.vb" id="invariant" highlight="10,14"::: -### Collections example: Hashtable constructor +### Collections example: `Hashtable` constructor Hashing strings provides a second example of an operation that is affected by the way in which strings are compared. diff --git a/docs/standard/base-types/string-comparison-net-5-plus.md b/docs/standard/base-types/string-comparison-net-5-plus.md index 070b2855da592..c6ddd04d137f0 100644 --- a/docs/standard/base-types/string-comparison-net-5-plus.md +++ b/docs/standard/base-types/string-comparison-net-5-plus.md @@ -19,7 +19,7 @@ If you use functions like `string.IndexOf(string)` without calling the overload This can manifest itself even in places where you aren't always expecting globalization facilities to be active. For example, the following code can produce a different answer depending on the current runtime. -```cs +```csharp const string greeting = "Hel\0lo"; Console.WriteLine($"{greeting.IndexOf("\0")}"); @@ -67,7 +67,7 @@ These specific rules aren't enabled by default. To enable them and show any viol The following snippet shows examples of code that produces the relevant code analyzer warnings or errors. -```cs +```csharp // // Potentially incorrect code - answer might vary based on locale. // @@ -93,7 +93,7 @@ Console.WriteLine(idx); Similarly, when instantiating a sorted collection of strings or sorting an existing string-based collection, specify an explicit comparer. -```cs +```csharp // // Potentially incorrect code - behavior might vary based on locale. // @@ -160,11 +160,11 @@ An `OrdinalIgnoreCase` comparer still operates on a char-by-char basis, but it e Some examples of this are provided in the following table: -| String 1 | String 2 | `Ordinal` comparison | `OrdinalIgnoreCase` comparison | -|---|---|---|---| -| `"dog"` | `"dog"` | equal | equal | -| `"dog"` | `"Dog"` | not equal | equal | -| `"resume"` | `"résumé"` | not equal | not equal | +| String 1 | String 2 | `Ordinal` comparison | `OrdinalIgnoreCase` comparison | +|------------|------------|----------------------|--------------------------------| +| `"dog"` | `"dog"` | equal | equal | +| `"dog"` | `"Dog"` | not equal | equal | +| `"resume"` | `"résumé"` | not equal | not equal | Unicode also allows strings to have several different in-memory representations. For example, an e-acute (é) can be represented in two possible ways: @@ -182,7 +182,7 @@ Under an ordinal comparer, none of these strings compare as equal to each other. When performing a `string.IndexOf(..., StringComparison.Ordinal)` operation, the runtime looks for an exact substring match. The results are as follows. -```cs +```csharp Console.WriteLine("resume".IndexOf("e", StringComparison.Ordinal)); // prints '1' Console.WriteLine("r\u00E9sum\u00E9".IndexOf("e", StringComparison.Ordinal)); // prints '-1' Console.WriteLine("r\u00E9sume\u0301".IndexOf("e", StringComparison.Ordinal)); // prints '5' @@ -201,18 +201,18 @@ Ordinal search and comparison routines are never affected by the current thread' Consider again the string `"résumé"` and its four different representations. The following table shows each representation broken down into its collation elements. -| String | As collation elements | -|---|---| -| `"r\u00E9sum\u00E9"` | `"r" + "\u00E9" + "s" + "u" + "m" + "\u00E9"` | -| `"r\u00E9sume\u0301"` | `"r" + "\u00E9" + "s" + "u" + "m" + "e\u0301"` | -| `"re\u0301sum\u00E9"` | `"r" + "e\u0301" + "s" + "u" + "m" + "\u00E9"` | +| String | As collation elements | +|------------------------|-------------------------------------------------| +| `"r\u00E9sum\u00E9"` | `"r" + "\u00E9" + "s" + "u" + "m" + "\u00E9"` | +| `"r\u00E9sume\u0301"` | `"r" + "\u00E9" + "s" + "u" + "m" + "e\u0301"` | +| `"re\u0301sum\u00E9"` | `"r" + "e\u0301" + "s" + "u" + "m" + "\u00E9"` | | `"re\u0301sume\u0301"` | `"r" + "e\u0301" + "s" + "u" + "m" + "e\u0301"` | A collation element corresponds loosely to what readers would think of as a single character or cluster of characters. It's conceptually similar to a [grapheme cluster](character-encoding-introduction.md#grapheme-clusters) but encompasses a somewhat larger umbrella. Under a linguistic comparer, exact matches aren't necessary. Collation elements are instead compared based on their semantic meaning. For example, a linguistic comparer treats the substrings `"\u00E9"` and `"e\u0301"` as equal since they both semantically mean "a lowercase e with an acute accent modifier." This allows the `IndexOf` method to match the substring `"e\u0301"` within a larger string that contains the semantically equivalent substring `"\u00E9"`, as shown in the following code sample. -```cs +```csharp Console.WriteLine("r\u00E9sum\u00E9".IndexOf("e")); // prints '-1' (not found) Console.WriteLine("r\u00E9sum\u00E9".IndexOf("\u00E9")); // prints '1' Console.WriteLine("\u00E9".IndexOf("e\u0301")); // prints '0' @@ -224,14 +224,14 @@ As a consequence of this, two strings of different lengths may compare as equal For example, [in the Hungarian alphabet](https://en.wikipedia.org/wiki/Hungarian_alphabet), when the two characters \ appear back-to-back, they are considered their own unique letter distinct from either \ or \. This means that when \ is seen in a string, a Hungarian culture-aware comparer treats it as a single collation element. -| String | As collation elements | Remarks | -|---|---|---| -| `"endz"` | `"e" + "n" + "d" + "z"` | (using a standard linguistic comparer) | -| `"endz"` | `"e" + "n" + "dz"` | (using a Hungarian culture-aware comparer) | +| String | As collation elements | Remarks | +|----------|-------------------------|--------------------------------------------| +| `"endz"` | `"e" + "n" + "d" + "z"` | (using a standard linguistic comparer) | +| `"endz"` | `"e" + "n" + "dz"` | (using a Hungarian culture-aware comparer) | When using a Hungarian culture-aware comparer, this means that the string `"endz"` *does not* end with the substring `"z"`, as \ and \ are considered collation elements with different semantic meaning. -```cs +```csharp // Set thread culture to Hungarian CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("hu-HU"); Console.WriteLine("endz".EndsWith("z")); // Prints 'False' @@ -254,7 +254,7 @@ For more information, see [Best practices for comparing strings in .NET](best-pr If your app uses an affected API for filtering, we recommend enabling the CA1307 and CA1309 code analysis rules to help locate places where a linguistic search may have inadvertently been used instead of an ordinal search. Code patterns like the following may be susceptible to security exploits. -```cs +```csharp // // THIS SAMPLE CODE IS INCORRECT. // DO NOT USE IT IN PRODUCTION. @@ -304,16 +304,16 @@ The following table lists the default search and comparison types for various st Unlike `string` APIs, all `MemoryExtensions` APIs perform *Ordinal* searches and comparisons by default, with the following exceptions. -| API | Default behavior | Remarks | -|---|---|---| -| `MemoryExtensions.ToLower` | CurrentCulture | (when passed a null `CultureInfo` argument) | -| `MemoryExtensions.ToLowerInvariant` | InvariantCulture | | -| `MemoryExtensions.ToUpper` | CurrentCulture | (when passed a null `CultureInfo` argument) | -| `MemoryExtensions.ToUpperInvariant` | InvariantCulture | | +| API | Default behavior | Remarks | +|-------------------------------------|------------------|---------------------------------------------| +| `MemoryExtensions.ToLower` | CurrentCulture | (when passed a null `CultureInfo` argument) | +| `MemoryExtensions.ToLowerInvariant` | InvariantCulture | | +| `MemoryExtensions.ToUpper` | CurrentCulture | (when passed a null `CultureInfo` argument) | +| `MemoryExtensions.ToUpperInvariant` | InvariantCulture | | A consequence is that when converting code from consuming `string` to consuming `ReadOnlySpan`, behavioral changes may be introduced inadvertently. An example of this follows. -```cs +```csharp string str = GetString(); if (str.StartsWith("Hello")) { /* do something */ } // this is a CULTURE-AWARE (linguistic) comparison @@ -323,7 +323,7 @@ if (span.StartsWith("Hello")) { /* do something */ } // this is an ORDINAL (non- The recommended way to address this is to pass an explicit `StringComparison` parameter to these APIs. The code analysis rules CA1307 and CA1309 can assist with this. -```cs +```csharp string str = GetString(); if (str.StartsWith("Hello", StringComparison.Ordinal)) { /* do something */ } // ordinal comparison @@ -331,37 +331,6 @@ ReadOnlySpan span = s.AsSpan(); if (span.StartsWith("Hello", StringComparison.Ordinal)) { /* do something */ } // ordinal comparison ``` -## Efficient multi-value string comparisons - -Starting with .NET 8, when comparing a string against a fixed set of known values repeatedly, consider using instead of chained comparisons or LINQ-based approaches. -`SearchValues` can precompute internal lookup structures and optimize the comparison logic based on the provided values. - -### Recommended usage - -Create and cache the `SearchValues` instance once, then reuse it for comparisons: - -```cs -using System.Buffers; - -private static readonly SearchValues Commands = SearchValues.Create(new[] { "start", "run", "go" }, StringComparison.OrdinalIgnoreCase); - -if (Commands.Contains(command)) -{ - /* do something */ -} -``` -### Avoid repeated creation -Avoid creating `SearchValues` instances inside hot paths or per comparison, as the creation step can be relatively expensive: - -```cs -// Avoid this pattern -if (SearchValues.Create(new[] { "start", "run", "go" }, StringComparison.OrdinalIgnoreCase).Contains(command)) -{ - /* do something */ -} -``` -Caching and reusing the instance ensures optimal performance. - ## See also - [Globalization breaking changes in .NET 5](../../core/compatibility/5.0.md#globalization) From a248220e1e2d5c3d7054bc3ec8df03d81289d0df Mon Sep 17 00:00:00 2001 From: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Date: Mon, 22 Dec 2025 10:25:52 -0800 Subject: [PATCH 10/10] fix bookmark --- docs/fundamentals/code-analysis/quality-rules/ca1307.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/fundamentals/code-analysis/quality-rules/ca1307.md b/docs/fundamentals/code-analysis/quality-rules/ca1307.md index 243a188a26107..7d344488a954b 100644 --- a/docs/fundamentals/code-analysis/quality-rules/ca1307.md +++ b/docs/fundamentals/code-analysis/quality-rules/ca1307.md @@ -21,7 +21,7 @@ ms.author: gewarren | **Title** | Specify StringComparison for clarity | | **Category** | [Globalization](globalization-warnings.md) | | **Fix is breaking or non-breaking** | Non-breaking | -| **Enabled by default in .NET 10** | No | +| **Enabled by default in .NET 10** | No | ## Cause @@ -31,7 +31,7 @@ A string comparison operation uses a method overload that does not set a enumeration value as a parameter. -Whenever an overload exists that takes a parameter, it should be used instead of an overload that does not take this parameter. By explicitly setting this parameter, your code is often made clearer and easier to maintain. For more information, see [Specifying string comparisons explicitly](../../../standard/base-types/best-practices-strings.md#specifying-string-comparisons-explicitly). +Whenever an overload exists that takes a parameter, it should be used instead of an overload that does not take this parameter. By explicitly setting this parameter, your code is often made clearer and easier to maintain. For more information, see [Specify string comparisons explicitly](../../../standard/base-types/best-practices-strings.md#specify-string-comparisons-explicitly). > [!NOTE] > This rule does not consider the default value used by the comparison method. Hence, it can be potentially noisy for methods that use the `Ordinal` string comparison by default and the user intended to use this default compare mode.