The concept and environment
SearchValues stores a reusable set of values for repeated searching. This example uses SearchValues<char> to look for either a comma or a semicolon in a character span. IndexOfAny asks for the first position containing any member of that set. It is not searching for the complete two-character string comma-semicolon, and it is not evaluating a regular expression. The API is available in the .NET 9 environment. Its purpose is to express a reusable search set clearly; performance still depends on workload, implementation, and the actual execution environment.
Walk through the portable program
The text alpha-semicolon-beta contains its first separator after the five characters in alpha, so IndexOfAny returns five. Slicing before that known index produces alpha. A separate search over plain has no matching character and returns negative one. Output is index:5, prefix:alpha, and missing:-1. The sample only slices the first input, where a separator is deliberately present. It does not use the missing result as a range endpoint. That distinction matters because a negative not-found result is an outcome to handle, not a valid prefix length.
A deeper design boundary
Create a stable search set once when the same separators are reused across many inputs. A parser or tokenizer can keep it in a suitable static readonly field instead of rebuilding it for every character or token. The search set is independent of any particular input span. The span itself is only a view, so decide when returned data needs an owned string rather than a short-lived slice. In a larger tokenizer, define whether adjacent separators create empty tokens and whether whitespace is preserved; IndexOfAny does not decide those application policies.
Pitfalls and limits
Character indices in a .NET string count UTF-16 code units. They are not necessarily Unicode scalar counts, visible grapheme counts, or UTF-8 byte offsets. A char search therefore needs careful interpretation for non-ASCII text, although ASCII delimiter characters remain straightforward. Also distinguish the specialized SearchValues<char> set from other API shapes available in newer frameworks; do not assume every SearchValues overload exists in a .NET 9 profile. Guard the not-found result before slicing, and test an empty span rather than relying on the example's known separator to hide an invalid range.
Practice one: isolate a contract
First replace alpha-semicolon-beta with plain and implement a branch that returns the entire input when the search result is negative. Predict prefix plain without an exception. Hint: check the returned index before constructing a range; do not try to repair a failed slice afterward. Then test a separator at index zero and one at the final position. Define whether the desired prefix may be empty. Those cases separate valid boundary positions from the negative sentinel used for absence.
Practice two: challenge an assumption
For another exercise, write a bounded tokenizer that advances through the remaining span after each found separator and returns owned nonempty tokens. Test comma-comma, leading and trailing separators, and mixed delimiter characters. Hint: advance past the separator even when the preceding token is empty, or the loop cannot make progress. Add a Unicode input containing a supplementary character before a semicolon and explain its index in UTF-16 units. Keep that representation explanation separate from the token policy and avoid claiming a delimiter search performs Unicode normalization.
Review the resulting contract
Before using a reusable search set, state exactly what its values mean and what unit its indices use. Check no match, empty input, first-position matches, and final-position matches before optimizing a longer parser. The companion project extends this idea into a complete small tokenizer with explicit policy checks. The lesson's three lines establish the bounded character-search behavior only; they are not benchmark evidence. Keep owned strings, temporary views, and reusable separator configuration distinct so that a fast search primitive remains a reliable part of a comprehensible parsing boundary.
A working example
using System;
using System.Buffers;
SearchValues<char> separators = SearchValues.Create(",;");
ReadOnlySpan<char> text = "alpha;beta";
int index = text.IndexOfAny(separators);
Console.WriteLine($"index:{index}");
Console.WriteLine("prefix:" + text[..index].ToString());
Console.WriteLine($"missing:{"plain".AsSpan().IndexOfAny(separators)}");
Expected output
index:5 prefix:alpha missing:-1
Add an input with no separators and guard index<0 before slicing. SearchValues<char> describes a character set, not a substring or regex.