SearchValues no .NET | CSharp
- SearchValues no .NET | CSharp
SearchValues<T> é uma estrutura de apoio para buscas performáticas em Span<T> e ReadOnlySpan<T>.
Ela fica no namespace:
using System.Buffers;
A ideia é simples: você cria uma representação otimizada de um conjunto de valores e reutiliza essa representação para buscar dentro de textos, bytes ou spans.
A própria documentação define SearchValues<T> como um conjunto imutável e somente leitura, otimizado para busca eficiente. Ele é indicado principalmente quando o mesmo conjunto de valores será usado várias vezes em tempo de execução. (Microsoft Learn)
Problema que ele resolve
Imagine que você precisa verificar se uma string contém algum caractere inválido:
string texto = "abc#123";
char[] invalidos = ['#', '@', '$', '%'];
bool contemInvalido = texto.Any(c => invalidos.Contains(c));
Funciona, mas esse código:
- cria mais abstração do que o necessário;
- pode gerar mais custo em hot paths;
- não aproveita tão bem otimizações internas de
Span; - repete a lógica de busca várias vezes.
Com SearchValues, você prepara os valores uma vez:
using System.Buffers;
static readonly SearchValues<char> CaracteresInvalidos =
SearchValues.Create("#@$%");
string texto = "abc#123";
bool contemInvalido = texto.AsSpan().IndexOfAny(CaracteresInvalidos) >= 0;
Como funciona
O SearchValues<T> não faz a busca sozinho. Ele é usado junto com métodos como:
IndexOfAny(...)
Contains(...)
Exemplo:
using System.Buffers;
SearchValues<char> valores = SearchValues.Create("aeiou");
ReadOnlySpan<char> texto = "mongodb";
int indice = texto.IndexOfAny(valores);
Console.WriteLine(indice); // 1, porque encontrou 'o'
O método IndexOfAny retorna o primeiro índice onde qualquer valor do conjunto foi encontrado. Se não encontrar, retorna -1. (Microsoft Learn)
Exemplo básico com char
using System.Buffers;
static readonly SearchValues<char> Separadores =
SearchValues.Create(",;|");
string linha = "produto;preco;quantidade";
int posicao = linha.AsSpan().IndexOfAny(Separadores);
if (posicao >= 0)
{
Console.WriteLine($"Separador encontrado na posição {posicao}");
}
Nesse caso, ele procura qualquer um destes caracteres:
,
;
|
Exemplo validando caracteres inválidos
using System.Buffers;
public static class ValidadorNomeArquivo
{
private static readonly SearchValues<char> CaracteresInvalidos =
SearchValues.Create(@"<>:""/\|?*");
public static bool PossuiCaracterInvalido(string nomeArquivo)
{
return nomeArquivo.AsSpan().IndexOfAny(CaracteresInvalidos) >= 0;
}
}
Uso:
Console.WriteLine(ValidadorNomeArquivo.PossuiCaracterInvalido("relatorio.pdf"));
// false
Console.WriteLine(ValidadorNomeArquivo.PossuiCaracterInvalido("relatorio?.pdf"));
// true
Exemplo com bytes
Também existe suporte para byte.
using System.Buffers;
static readonly SearchValues<byte> BytesControle =
SearchValues.Create(new byte[]
{
0x00,
0x01,
0x02,
0x03
});
ReadOnlySpan<byte> payload = stackalloc byte[]
{
10, 20, 30, 0x01, 40
};
int indice = payload.IndexOfAny(BytesControle);
Console.WriteLine(indice); // 3
Isso é útil em parsers, protocolos, validação de payload binário, processamento de arquivos e pipelines de rede.
Exemplo com strings
Além de char e byte, também existe criação para string, usando StringComparison.
using System.Buffers;
static readonly SearchValues<string> PalavrasBloqueadas =
SearchValues.Create(
["drop", "delete", "truncate"],
StringComparison.OrdinalIgnoreCase);
string comando = "DELETE FROM users";
int indice = comando.AsSpan().IndexOfAny(PalavrasBloqueadas);
Console.WriteLine(indice); // 0
Para SearchValues<string>, a documentação informa que o Create recebe um ReadOnlySpan<string> e um StringComparison, usando semântica Ordinal ou OrdinalIgnoreCase. (Microsoft Learn)
Comparação com Contains tradicional
Forma comum
char[] separadores = [',', ';', '|'];
bool possuiSeparador = texto.Any(c => separadores.Contains(c));
Forma com SearchValues
static readonly SearchValues<char> Separadores =
SearchValues.Create(",;|");
bool possuiSeparador = texto.AsSpan().IndexOfAny(Separadores) >= 0;
A segunda forma tende a ser melhor quando:
- a busca acontece muitas vezes;
- o conjunto de valores é fixo;
- o código está em caminho crítico;
- você quer reduzir alocações e overhead;
- você está trabalhando com
Span,ReadOnlySpan, parsing ou processamento de texto.
Ponto mais importante
Não crie SearchValues toda vez que for buscar.
Ruim:
public static bool TemSeparador(string texto)
{
SearchValues<char> separadores = SearchValues.Create(",;|");
return texto.AsSpan().IndexOfAny(separadores) >= 0;
}
Melhor:
public static class SeparadorHelper
{
private static readonly SearchValues<char> Separadores =
SearchValues.Create(",;|");
public static bool TemSeparador(string texto)
{
return texto.AsSpan().IndexOfAny(Separadores) >= 0;
}
}
O ganho vem justamente de preparar a estrutura uma vez e reutilizar várias vezes.
Quando usar
| Cenário | Usar SearchValues? | Motivo |
|---|---|---|
| Procurar 1 caractere uma vez | Não precisa | IndexOf(char) já resolve bem |
| Procurar 2 ou 3 caracteres uma vez | Talvez não | IndexOfAny('a', 'b') pode ser suficiente |
| Procurar vários caracteres repetidamente | Sim | Reutiliza estrutura otimizada |
| Validar caracteres proibidos em muitas strings | Sim | Ótimo caso de uso |
| Parser de CSV, logs, protocolo ou texto | Sim | Busca rápida por delimitadores |
| Buscar palavras ou tokens fixos | Sim | Pode usar SearchValues<string> |
| Lista dinâmica que muda toda hora | Cuidado | Recriar SearchValues pode anular o benefício |
Exemplo prático: parser simples de CSV
using System.Buffers;
public static class CsvHelper
{
private static readonly SearchValues<char> SeparadoresCsv =
SearchValues.Create(",\r\n");
public static bool PossuiSeparadorCsv(string linha)
{
return linha.AsSpan().IndexOfAny(SeparadoresCsv) >= 0;
}
}
Uso:
Console.WriteLine(CsvHelper.PossuiSeparadorCsv("nome,idade,cidade"));
// true
Console.WriteLine(CsvHelper.PossuiSeparadorCsv("Adolfo"));
// false
Exemplo prático: validar rota
using System.Buffers;
public static class RotaValidator
{
private static readonly SearchValues<char> CaracteresInvalidos =
SearchValues.Create(" ?#%");
public static bool RotaEhValida(string rota)
{
return rota.AsSpan().IndexOfAny(CaracteresInvalidos) < 0;
}
}
Uso:
Console.WriteLine(RotaValidator.RotaEhValida("/api/produtos"));
// true
Console.WriteLine(RotaValidator.RotaEhValida("/api/produtos?id=1"));
// false
Exemplo prático: localizar quebra de linha
using System.Buffers;
public static class TextoHelper
{
private static readonly SearchValues<char> QuebrasDeLinha =
SearchValues.Create("\r\n");
public static bool PossuiQuebraDeLinha(string texto)
{
return texto.AsSpan().IndexOfAny(QuebrasDeLinha) >= 0;
}
}
Esse é um caso bem comum: procurar \r ou \n em muito texto.
O blog oficial de performance do .NET 8 mostra SearchValues como uma das melhorias importantes da versão, especialmente para buscas com muitos caracteres-alvo, onde o runtime consegue selecionar implementações internas especializadas e aproveitar vetorização em determinados cenários. (Microsoft for Developers)
Diferença entre SearchValues e HashSet
| Critério | SearchValues<T> | HashSet<T> |
|---|---|---|
| Objetivo | Busca em Span/texto | Coleção genérica para consulta |
| Melhor uso | Parsing, texto, bytes, hot path | Regras de negócio, conjuntos dinâmicos |
| Mutável | Não | Sim |
Otimizado para IndexOfAny | Sim | Não |
| Bom para reuso fixo | Sim | Sim |
| Bom para adicionar/remover valores | Não | Sim |
Exemplo onde HashSet faz mais sentido:
HashSet<string> permissoes = new(StringComparer.OrdinalIgnoreCase)
{
"admin",
"financeiro",
"suporte"
};
bool autorizado = permissoes.Contains("ADMIN");
Exemplo onde SearchValues faz mais sentido:
static readonly SearchValues<char> Invalidos =
SearchValues.Create("<>:\"/\\|?*");
bool invalido = nomeArquivo.AsSpan().IndexOfAny(Invalidos) >= 0;
Diferença entre SearchValues e Regex
| Critério | SearchValues | Regex |
|---|---|---|
| Busca por conjunto de caracteres | Excelente | Funciona, mas pode ser exagero |
| Busca por padrão complexo | Não | Sim |
| Validação simples | Muito bom | Pode ser mais custoso |
| Expressividade | Baixa | Alta |
| Performance em hot path | Geralmente melhor para busca simples | Depende do padrão e configuração |
Ruim para um caso simples:
Regex.IsMatch(texto, "[,;|]");
Melhor:
static readonly SearchValues<char> Separadores =
SearchValues.Create(",;|");
bool possui = texto.AsSpan().IndexOfAny(Separadores) >= 0;
Use Regex quando você precisa de padrão, não apenas de conjunto de caracteres.
Cuidados
Não use para tudo
Para buscar um único caractere:
int index = texto.IndexOf('/');
Isso já é simples e eficiente.
Não recrie em loop
Evite:
foreach (string linha in linhas)
{
var searchValues = SearchValues.Create(",;");
bool possui = linha.AsSpan().IndexOfAny(searchValues) >= 0;
}
Prefira:
static readonly SearchValues<char> Separadores =
SearchValues.Create(",;");
foreach (string linha in linhas)
{
bool possui = linha.AsSpan().IndexOfAny(Separadores) >= 0;
}
Cuidado com cultura em strings
Para SearchValues<string>, prefira comparação ordinal quando estiver lidando com identificadores técnicos, comandos, rotas, tokens, headers, nomes de campos e protocolos.
SearchValues.Create(
["Bearer", "Basic"],
StringComparison.OrdinalIgnoreCase);
Evite usar regras culturais para esse tipo de comparação técnica.
Benchmark simples
Exemplo de benchmark para testar no seu ambiente:
using System.Buffers;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
BenchmarkRunner.Run<SearchValuesBenchmark>();
[MemoryDiagnoser]
public class SearchValuesBenchmark
{
private readonly string _texto = new('a', 10_000) + "#";
private static readonly char[] InvalidosArray =
[
'#', '@', '$', '%', '&', '*', '!', '?'
];
private static readonly SearchValues<char> InvalidosSearchValues =
SearchValues.Create("#@$%&*!?");
[Benchmark]
public bool LinqContains()
{
return _texto.Any(c => InvalidosArray.Contains(c));
}
[Benchmark]
public bool SpanIndexOfAnyComArray()
{
return _texto.AsSpan().IndexOfAny(InvalidosArray) >= 0;
}
[Benchmark]
public bool SpanIndexOfAnyComSearchValues()
{
return _texto.AsSpan().IndexOfAny(InvalidosSearchValues) >= 0;
}
}
Pacote:
dotnet add package BenchmarkDotNet
Resultado esperado conceitualmente:
| Método | Tendência |
|---|---|
LINQ + Contains | Mais overhead |
Span.IndexOfAny(char[]) | Melhor |
Span.IndexOfAny(SearchValues<char>) | Melhor quando o conjunto é reutilizado muitas vezes |
Não assuma ganho sem medir. Em performance, o cenário real manda.
Exemplo completo
using System.Buffers;
public static class Program
{
private static readonly SearchValues<char> CaracteresInvalidos =
SearchValues.Create("<>:\"/\\|?*");
public static void Main()
{
string[] arquivos =
[
"relatorio.pdf",
"vendas-2026.xlsx",
"backup?.zip",
"clientes:ativos.csv"
];
foreach (string arquivo in arquivos)
{
Console.WriteLine($"{arquivo} => {EhNomeArquivoValido(arquivo)}");
}
}
private static bool EhNomeArquivoValido(string nomeArquivo)
{
return nomeArquivo.AsSpan().IndexOfAny(CaracteresInvalidos) < 0;
}
}
Saída:
relatorio.pdf => True
vendas-2026.xlsx => True
backup?.zip => False
clientes:ativos.csv => False
Resumo prático
| Pergunta | Resposta |
|---|---|
| O que é? | Um conjunto imutável otimizado para busca |
| Namespace | System.Buffers |
| Principal uso | Span.IndexOfAny(SearchValues<T>) |
| Tipos comuns | char, byte, string |
| Quando vale a pena? | Quando o mesmo conjunto de busca é reutilizado |
| Quando evitar? | Busca única, valor único ou conjunto que muda toda hora |
| Alternativa comum | IndexOf, IndexOfAny, HashSet, Regex |
Regra prática
Use SearchValues quando a frase mental for:
“Eu tenho um conjunto fixo de caracteres, bytes ou strings e vou procurar isso muitas vezes em textos/spans diferentes.”
Exemplo típico:
private static readonly SearchValues<char> Separadores =
SearchValues.Create(",;|\r\n");
public static bool ContemSeparador(string texto)
{
return texto.AsSpan().IndexOfAny(Separadores) >= 0;
}