Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>

</Project>
143 changes: 143 additions & 0 deletions snippets/csharp/System.Buffers/SearchValues/Overview/searchvalues.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// <SnippetUsings>
using System;
using System.Buffers;
using System.Globalization;
using System.Text;
// </SnippetUsings>

namespace SearchValuesExamples;

public static class Validation
{
// <SnippetValidation>
// 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<char> s_hexDigits =
SearchValues.Create("0123456789ABCDEFabcdef");

// Rejects any value that contains a character outside of the allowed set.
public static bool IsHexString(ReadOnlySpan<char> value) =>
value.Length % 2 == 0 && !value.ContainsAnyExcept(s_hexDigits);
// </SnippetValidation>
}

public static class Escaping
{
// <SnippetEscaping>
private static readonly SearchValues<char> s_charsToEscape = SearchValues.Create("\"\\\b\f\n\r\t");

public static void AppendEscaped(StringBuilder builder, ReadOnlySpan<char> 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)..];
}
}
// </SnippetEscaping>
}

public static class Bytes
{
// <SnippetBytes>
// 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<byte> 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<byte> utf8Line) =>
utf8Line.IndexOfAny(s_delimiters);
// </SnippetBytes>
}

public static class Strings
{
// <SnippetStrings>
private static readonly SearchValues<string> 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<char> text) =>
text.IndexOfAny(s_schemes);
// </SnippetStrings>
}

public static class SingleString
{
// <SnippetSingleString>
private static readonly SearchValues<string> 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<char> text) =>
text.IndexOfAny(s_chunked);
// </SnippetSingleString>
}

public static class SingleValues
{
// <SnippetContains>
// The prefix that starts an escape sequence. A single-value SearchValues<string>
// is a faster alternative to IndexOf(value, StringComparison).
private static readonly SearchValues<string> s_escapePrefix =
SearchValues.Create(["\\u"], StringComparison.Ordinal);

// Characters that aren't allowed to appear unescaped in the output.
private static readonly SearchValues<char> 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<char> 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<char> 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);
}
}
}
// </SnippetContains>
}
55 changes: 51 additions & 4 deletions xml/System.Buffers/SearchValues.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,18 @@
<Docs>
<summary>Provides a set of initialization methods for instances of the <see cref="T:System.Buffers.SearchValues`1" /> class.</summary>
<remarks>
<see cref="T:System.Buffers.SearchValues`1" /> instances are optimized for situations where the same set of values is frequently used for searching at run time.</remarks>
<format type="text/markdown"><![CDATA[

## Remarks

<xref:System.Buffers.SearchValues`1> 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 <xref:System.MemoryExtensions> methods that search a span, such as <xref:System.MemoryExtensions.IndexOfAny*>, <xref:System.MemoryExtensions.IndexOfAnyExcept*>, <xref:System.MemoryExtensions.ContainsAny*>, and <xref:System.MemoryExtensions.ContainsAnyExcept*>.

:::code language="csharp" source="~/snippets/csharp/System.Buffers/SearchValues/Overview/searchvalues.cs" id="SnippetEscaping":::

For more use cases and examples, see <xref:System.Buffers.SearchValues`1>.

]]></format>
</remarks>
</Docs>
<Members>
<MemberGroup MemberName="Create">
Expand Down Expand Up @@ -70,7 +81,17 @@
<param name="values">The set of values.</param>
<summary>Creates an optimized representation of <paramref name="values" /> used for efficient searching.</summary>
<returns>The optimized representation of <paramref name="values" /> used for efficient searching.</returns>
<remarks>To be added.</remarks>
<remarks>
<format type="text/markdown"><![CDATA[

## Remarks

Cache the returned instance, for example in a `static readonly` field, and reuse it for every search. A UTF-8 literal (`"..."u8`) is a convenient way to specify the set of bytes without allocating a string. Large sets benefit the most, because the <xref:System.MemoryExtensions> overloads that accept individual values only go up to three:

:::code language="csharp" source="~/snippets/csharp/System.Buffers/SearchValues/Overview/searchvalues.cs" id="SnippetBytes":::

]]></format>
</remarks>
</Docs>
</Member>
<Member MemberName="Create">
Expand Down Expand Up @@ -116,7 +137,17 @@
<param name="values">The set of values.</param>
<summary>Creates an optimized representation of <paramref name="values" /> used for efficient searching.</summary>
<returns>The optimized representation of <paramref name="values" /> used for efficient searching.</returns>
<remarks>To be added.</remarks>
<remarks>
<format type="text/markdown"><![CDATA[

## Remarks

Cache the returned instance, for example in a `static readonly` field, and reuse it for every search. A common use is to validate that an input only contains allowed characters:

:::code language="csharp" source="~/snippets/csharp/System.Buffers/SearchValues/Overview/searchvalues.cs" id="SnippetValidation":::

]]></format>
</remarks>
</Docs>
</Member>
<Member MemberName="Create">
Expand Down Expand Up @@ -152,7 +183,23 @@
<param name="comparisonType">Specifies whether to use <see cref="F:System.StringComparison.Ordinal" /> or <see cref="F:System.StringComparison.OrdinalIgnoreCase" /> search semantics.</param>
<summary>Creates an optimized representation of <paramref name="values" /> used for efficient searching.</summary>
<returns>The optimized representation of <paramref name="values" /> used for efficient searching.</returns>
<remarks>Only <see cref="F:System.StringComparison.Ordinal" /> or <see cref="F:System.StringComparison.OrdinalIgnoreCase" /> may be used.</remarks>
<remarks>
<format type="text/markdown"><![CDATA[

## Remarks

Only <xref:System.StringComparison.Ordinal?displayProperty=nameWithType> or <xref:System.StringComparison.OrdinalIgnoreCase?displayProperty=nameWithType> can be used.

The returned instance searches for whole substrings, so it can only be used with the <xref:System.MemoryExtensions.IndexOfAny(System.ReadOnlySpan{System.Char},System.Buffers.SearchValues{System.String})> and <xref:System.MemoryExtensions.ContainsAny(System.ReadOnlySpan{System.Char},System.Buffers.SearchValues{System.String})> overloads that accept a `SearchValues<string>`. If you're searching for individual characters, use <xref:System.Buffers.SearchValues.Create(System.ReadOnlySpan{System.Char})> 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 <xref:System.MemoryExtensions.IndexOf(System.ReadOnlySpan{System.Char},System.ReadOnlySpan{System.Char},System.StringComparison)>, 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":::

]]></format>
</remarks>
</Docs>
</Member>
</Members>
Expand Down
78 changes: 76 additions & 2 deletions xml/System.Buffers/SearchValues`1.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,59 @@
<summary>Provides an immutable, read-only set of values optimized for efficient searching.
Instances are created by <see cref="M:System.Buffers.SearchValues.Create(System.ReadOnlySpan{System.Byte})" /> or <see cref="M:System.Buffers.SearchValues.Create(System.ReadOnlySpan{System.Char})" />.</summary>
<remarks>
<see cref="T:System.Buffers.SearchValues`1" /> instances are optimized for situations where the same set of values is frequently used for searching at run time.</remarks>
<format type="text/markdown"><![CDATA[
Comment on lines 28 to +31

## Remarks

<xref:System.Buffers.SearchValues`1> 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 <xref:System.Buffers.SearchValues`1> to one of the searching methods on <xref:System.MemoryExtensions> 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 |
|--|--|
| <xref:System.MemoryExtensions.IndexOfAny*> | Find the first position of any of the values. |
| <xref:System.MemoryExtensions.IndexOfAnyExcept*> | Find the first position of anything that isn't one of the values. |
| <xref:System.MemoryExtensions.LastIndexOfAny*> / <xref:System.MemoryExtensions.LastIndexOfAnyExcept*> | Search backwards for the same conditions. |
| <xref:System.MemoryExtensions.ContainsAny*> / <xref:System.MemoryExtensions.ContainsAnyExcept*> | Test whether a span contains any of the values, or anything other than the values. |
| <xref:System.MemoryExtensions.CountAny*> | Count how many elements in the span are in the set. |
| <xref:System.MemoryExtensions.ReplaceAny*> / <xref:System.MemoryExtensions.ReplaceAnyExcept*> | Replace the elements that are (or aren't) in the set. |
| <xref:System.MemoryExtensions.SplitAny*> | 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 <xref:System.MemoryExtensions.ContainsAnyExcept*> 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 <xref:System.MemoryExtensions.IndexOfAny*> 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 <xref:System.Buffers.SearchValues`1> of <xref:System.Byte> from a UTF-8 literal to search the bytes without transcoding them first. The benefit grows with the number of values: the <xref:System.MemoryExtensions> overloads that take individual values only go up to three, and passing a longer span of values is slower than using a cached <xref:System.Buffers.SearchValues`1>:

:::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 <xref:System.Buffers.SearchValues`1> of <xref:System.String> with <xref:System.Buffers.SearchValues.Create(System.ReadOnlySpan{System.String},System.StringComparison)>. Such an instance can only be used with the <xref:System.MemoryExtensions.IndexOfAny(System.ReadOnlySpan{System.Char},System.Buffers.SearchValues{System.String})> and <xref:System.MemoryExtensions.ContainsAny(System.ReadOnlySpan{System.Char},System.Buffers.SearchValues{System.String})> overloads that accept a `SearchValues<string>`:

:::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 <xref:System.MemoryExtensions.IndexOf(System.ReadOnlySpan{System.Char},System.ReadOnlySpan{System.Char},System.StringComparison)>, 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

<xref:System.Buffers.SearchValues`1> is immutable and all of its members are thread-safe, so a single cached instance can be shared across the whole app.

]]></format>
</remarks>
<altmember cref="M:System.MemoryExtensions.IndexOfAny``1(System.ReadOnlySpan{``0},System.Buffers.SearchValues{``0})" />
<altmember cref="M:System.MemoryExtensions.IndexOfAnyExcept``1(System.ReadOnlySpan{``0},System.Buffers.SearchValues{``0})" />
<altmember cref="M:System.MemoryExtensions.LastIndexOfAny``1(System.ReadOnlySpan{``0},System.Buffers.SearchValues{``0})" />
<altmember cref="M:System.MemoryExtensions.ContainsAny``1(System.ReadOnlySpan{``0},System.Buffers.SearchValues{``0})" />
<altmember cref="M:System.MemoryExtensions.ContainsAnyExcept``1(System.ReadOnlySpan{``0},System.Buffers.SearchValues{``0})" />
<altmember cref="M:System.MemoryExtensions.CountAny``1(System.ReadOnlySpan{``0},System.Buffers.SearchValues{``0})" />
<altmember cref="M:System.MemoryExtensions.SplitAny``1(System.ReadOnlySpan{``0},System.Buffers.SearchValues{``0})" />
</Docs>
<Members>
<Member MemberName="Contains">
Expand Down Expand Up @@ -57,7 +109,29 @@
<summary>Searches for the specified value.</summary>
<returns>
<see langword="true" /> if <paramref name="value" /> was found; otherwise, <see langword="false" />.</returns>
<remarks>To be added.</remarks>
<remarks>
<format type="text/markdown"><![CDATA[

## Remarks

Testing individual values isn't the primary function of <xref:System.Buffers.SearchValues`1>. The type is designed to search a whole span at once, so prefer the <xref:System.MemoryExtensions> methods that accept a <xref:System.Buffers.SearchValues`1>, such as <xref:System.MemoryExtensions.IndexOfAny*>, <xref:System.MemoryExtensions.IndexOfAnyExcept*>, <xref:System.MemoryExtensions.ContainsAny*>, or <xref:System.MemoryExtensions.ContainsAnyExcept*>. 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 <xref:System.Buffers.SearchValues`1> of <xref:System.String>, this method tests whether the whole `value` is one of the strings in the set, using the <xref:System.StringComparison> that was specified when the instance was created. It doesn't perform a substring search. Such a lookup is slower than <xref:System.Collections.Frozen.FrozenSet`1.Contains*?displayProperty=nameWithType>, so only create a <xref:System.Buffers.SearchValues`1> of <xref:System.String> when you need to search a span for multiple substrings.

]]></format>
</remarks>
<altmember cref="M:System.MemoryExtensions.ContainsAny``1(System.ReadOnlySpan{``0},System.Buffers.SearchValues{``0})" />
<altmember cref="M:System.MemoryExtensions.IndexOfAny``1(System.ReadOnlySpan{``0},System.Buffers.SearchValues{``0})" />
</Docs>
</Member>
</Members>
Expand Down
Loading