diff --git a/snippets/csharp/System.Buffers/SearchValues/Overview/Project.csproj b/snippets/csharp/System.Buffers/SearchValues/Overview/Project.csproj new file mode 100644 index 00000000000..dfdef3fd2a7 --- /dev/null +++ b/snippets/csharp/System.Buffers/SearchValues/Overview/Project.csproj @@ -0,0 +1,8 @@ + + + + Library + net10.0 + + + diff --git a/snippets/csharp/System.Buffers/SearchValues/Overview/searchvalues.cs b/snippets/csharp/System.Buffers/SearchValues/Overview/searchvalues.cs new file mode 100644 index 00000000000..0e2a01a124d --- /dev/null +++ b/snippets/csharp/System.Buffers/SearchValues/Overview/searchvalues.cs @@ -0,0 +1,143 @@ +// +using System; +using System.Buffers; +using System.Globalization; +using System.Text; +// + +namespace SearchValuesExamples; + +public static class Validation +{ + // + // Cache the SearchValues instance in a static readonly field so that the + // optimized representation is computed once and reused for every search. + private static readonly SearchValues s_hexDigits = + SearchValues.Create("0123456789ABCDEFabcdef"); + + // Rejects any value that contains a character outside of the allowed set. + public static bool IsHexString(ReadOnlySpan value) => + value.Length % 2 == 0 && !value.ContainsAnyExcept(s_hexDigits); + // +} + +public static class Escaping +{ + // + private static readonly SearchValues s_charsToEscape = SearchValues.Create("\"\\\b\f\n\r\t"); + + public static void AppendEscaped(StringBuilder builder, ReadOnlySpan value) + { + while (true) + { + // Find the next character that needs special treatment. + // IndexOfAny returns -1 when none of the values are present. + int index = value.IndexOfAny(s_charsToEscape); + if (index < 0) + { + builder.Append(value); + return; + } + + // Everything up to that point can be copied in bulk. + builder.Append(value[..index]); + + builder.Append('\\'); + builder.Append(value[index] switch + { + '\b' => 'b', + '\f' => 'f', + '\n' => 'n', + '\r' => 'r', + '\t' => 't', + char c => c, + }); + + value = value[(index + 1)..]; + } + } + // +} + +public static class Bytes +{ + // + // Bytes that separate fields in the UTF-8 log lines this app reads. + // A UTF-8 literal ("u8") avoids allocating a string just to create the set. + private static readonly SearchValues s_delimiters = SearchValues.Create("\t ,;:|="u8); + + // Finds where the next field ends, or -1 when the last field is reached. + public static int IndexOfNextDelimiter(ReadOnlySpan utf8Line) => + utf8Line.IndexOfAny(s_delimiters); + // +} + +public static class Strings +{ + // + private static readonly SearchValues s_schemes = + SearchValues.Create(["http://", "https://", "ftp://"], StringComparison.OrdinalIgnoreCase); + + // Finds the position of the first substring in the set, ignoring case. + public static int IndexOfScheme(ReadOnlySpan text) => + text.IndexOfAny(s_schemes); + // +} + +public static class SingleString +{ + // + private static readonly SearchValues s_chunked = + SearchValues.Create(["chunked"], StringComparison.OrdinalIgnoreCase); + + // Equivalent to text.IndexOf("chunked", StringComparison.OrdinalIgnoreCase), + // but faster because the value is analyzed once when the instance is created. + public static int IndexOfChunked(ReadOnlySpan text) => + text.IndexOfAny(s_chunked); + // +} + +public static class SingleValues +{ + // + // The prefix that starts an escape sequence. A single-value SearchValues + // is a faster alternative to IndexOf(value, StringComparison). + private static readonly SearchValues s_escapePrefix = + SearchValues.Create(["\\u"], StringComparison.Ordinal); + + // Characters that aren't allowed to appear unescaped in the output. + private static readonly SearchValues s_mustStayEscaped = SearchValues.Create("\"\\\b\f\n\r\t"); + + // Turns "\uXXXX" sequences back into the characters they represent, but + // keeps the ones that must stay escaped as they are. + public static void AppendDecoded(StringBuilder builder, ReadOnlySpan value) + { + while (true) + { + int index = value.IndexOfAny(s_escapePrefix); + if (index < 0 || value.Length - index < 6) + { + builder.Append(value); + return; + } + + builder.Append(value[..index]); + + ReadOnlySpan escaped = value.Slice(index, 6); + value = value[(index + 6)..]; + + // The decoded character is computed one at a time, so there's no span + // to search and Contains is the right choice here. + if (!ushort.TryParse(escaped[2..], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ushort decoded) || + s_mustStayEscaped.Contains((char)decoded)) + { + builder.Append(escaped); + } + else + { + builder.Append((char)decoded); + } + } + } + // +} diff --git a/xml/System.Buffers/SearchValues.xml b/xml/System.Buffers/SearchValues.xml index f85c9422d39..77d4222278e 100644 --- a/xml/System.Buffers/SearchValues.xml +++ b/xml/System.Buffers/SearchValues.xml @@ -19,7 +19,18 @@ Provides a set of initialization methods for instances of the class. - instances are optimized for situations where the same set of values is frequently used for searching at run time. + instances are optimized for situations where the same set of values is frequently used for searching at run time. Creating an instance is relatively expensive because the values are analyzed to pick a specialized, often vectorized, search algorithm. Create the instance once and cache it, typically in a `static readonly` field, then pass it to the methods that search a span, such as , , , and . + +:::code language="csharp" source="~/snippets/csharp/System.Buffers/SearchValues/Overview/searchvalues.cs" id="SnippetEscaping"::: + +For more use cases and examples, see . + +]]> + @@ -70,7 +81,17 @@ The set of values. Creates an optimized representation of used for efficient searching. The optimized representation of used for efficient searching. - To be added. + + overloads that accept individual values only go up to three: + +:::code language="csharp" source="~/snippets/csharp/System.Buffers/SearchValues/Overview/searchvalues.cs" id="SnippetBytes"::: + +]]> + @@ -116,7 +137,17 @@ The set of values. Creates an optimized representation of used for efficient searching. The optimized representation of used for efficient searching. - To be added. + + + @@ -152,7 +183,23 @@ Specifies whether to use or search semantics. Creates an optimized representation of used for efficient searching. The optimized representation of used for efficient searching. - Only or may be used. + + or can be used. + +The returned instance searches for whole substrings, so it can only be used with the and overloads that accept a `SearchValues`. If you're searching for individual characters, use instead. + +:::code language="csharp" source="~/snippets/csharp/System.Buffers/SearchValues/Overview/searchvalues.cs" id="SnippetStrings"::: + +Passing a single value is also useful. The resulting instance is a faster alternative to , because the value is analyzed when the instance is created instead of on every search: + +:::code language="csharp" source="~/snippets/csharp/System.Buffers/SearchValues/Overview/searchvalues.cs" id="SnippetSingleString"::: + +]]> + diff --git a/xml/System.Buffers/SearchValues`1.xml b/xml/System.Buffers/SearchValues`1.xml index 64601241712..583faf47cb0 100644 --- a/xml/System.Buffers/SearchValues`1.xml +++ b/xml/System.Buffers/SearchValues`1.xml @@ -26,9 +26,61 @@ The type of the values to search for. Provides an immutable, read-only set of values optimized for efficient searching. - Instances are created by or . + Instances are created by , , or . - instances are optimized for situations where the same set of values is frequently used for searching at run time. + instances are optimized for situations where the same set of values is frequently used for searching at run time. When you create the instance, the runtime analyzes the values and picks a search algorithm that's specialized for that set, often using vectorized (SIMD) instructions. That analysis is done once, so create the instance once and cache it, typically in a `static readonly` field. + +Passing a to one of the searching methods on is usually much faster than passing the values as a span, especially for larger sets of values. Searching a span for a set of values is the primary purpose of the type, so reach for the following methods first: + +| Method | Use it to | +|--|--| +| | Find the first position of any of the values. | +| | Find the first position of anything that isn't one of the values. | +| / | Search backwards for the same conditions. | +| / | Test whether a span contains any of the values, or anything other than the values. | +| | Count how many elements in the span are in the set. | +| / | Replace the elements that are (or aren't) in the set. | +| | Split a span on any of the values. | + +## Common use cases + +Validation is one. Instead of testing each element in a loop, describe the set of allowed values once and use to find out whether the input contains anything else: + +:::code language="csharp" source="~/snippets/csharp/System.Buffers/SearchValues/Overview/searchvalues.cs" id="SnippetValidation"::: + +Escaping and encoding are another. Use to skip ahead to the next element that needs special treatment, so that everything in between is processed in bulk: + +:::code language="csharp" source="~/snippets/csharp/System.Buffers/SearchValues/Overview/searchvalues.cs" id="SnippetEscaping"::: + +The same applies to UTF-8 data. Create a of from a UTF-8 literal to search the bytes without transcoding them first. The benefit grows with the number of values: the overloads that take individual values only go up to three, and passing a longer span of values is slower than using a cached : + +:::code language="csharp" source="~/snippets/csharp/System.Buffers/SearchValues/Overview/searchvalues.cs" id="SnippetBytes"::: + +Starting in .NET 9, you can also search for a set of substrings by creating a of with . Such an instance can only be used with the and overloads that accept a `SearchValues`: + +:::code language="csharp" source="~/snippets/csharp/System.Buffers/SearchValues/Overview/searchvalues.cs" id="SnippetStrings"::: + +You can also create such an instance from a single string. Doing so is a faster alternative to , because the value is analyzed when the instance is created instead of on every search: + +:::code language="csharp" source="~/snippets/csharp/System.Buffers/SearchValues/Overview/searchvalues.cs" id="SnippetSingleString"::: + +## Thread safety + + is immutable and all of its members are thread-safe, so a single cached instance can be shared across the whole app. + +]]> + + + + + + + + @@ -57,7 +109,29 @@ Searches for the specified value. if was found; otherwise, . - To be added. + + . The type is designed to search a whole span at once, so prefer the methods that accept a , such as , , , or . Those methods can process many elements at a time, whereas calling `Contains` in a loop can't. + +Use this method when you only have a single value at hand and there's no span to search, for example: + +- You're inspecting or producing elements one at a time as part of a larger operation, such as decoding, transcoding, or transforming each element. +- You've already located an element with one of the searching methods and want to classify the element that follows it. +- You want to reuse an existing set of values as a general-purpose lookup table for a value you got from somewhere else. + +The following example turns `\uXXXX` escape sequences back into the characters they represent, but leaves the ones that must stay escaped alone. Because each decoded character is computed on the fly, there's no span to search and `Contains` is the appropriate choice: + +:::code language="csharp" source="~/snippets/csharp/System.Buffers/SearchValues/Overview/searchvalues.cs" id="SnippetContains"::: + +For a of , this method tests whether the whole `value` is one of the strings in the set, using the that was specified when the instance was created. It doesn't perform a substring search. Such a lookup is slower than , so only create a of when you need to search a span for multiple substrings. + +]]> + + +