Skip to main content

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árioUsar SearchValues?Motivo
Procurar 1 caractere uma vezNão precisaIndexOf(char) já resolve bem
Procurar 2 ou 3 caracteres uma vezTalvez nãoIndexOfAny('a', 'b') pode ser suficiente
Procurar vários caracteres repetidamenteSimReutiliza estrutura otimizada
Validar caracteres proibidos em muitas stringsSimÓtimo caso de uso
Parser de CSV, logs, protocolo ou textoSimBusca rápida por delimitadores
Buscar palavras ou tokens fixosSimPode usar SearchValues<string>
Lista dinâmica que muda toda horaCuidadoRecriar 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érioSearchValues<T>HashSet<T>
ObjetivoBusca em Span/textoColeção genérica para consulta
Melhor usoParsing, texto, bytes, hot pathRegras de negócio, conjuntos dinâmicos
MutávelNãoSim
Otimizado para IndexOfAnySimNão
Bom para reuso fixoSimSim
Bom para adicionar/remover valoresNãoSim

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érioSearchValuesRegex
Busca por conjunto de caracteresExcelenteFunciona, mas pode ser exagero
Busca por padrão complexoNãoSim
Validação simplesMuito bomPode ser mais custoso
ExpressividadeBaixaAlta
Performance em hot pathGeralmente melhor para busca simplesDepende 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étodoTendência
LINQ + ContainsMais 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

PerguntaResposta
O que é?Um conjunto imutável otimizado para busca
NamespaceSystem.Buffers
Principal usoSpan.IndexOfAny(SearchValues<T>)
Tipos comunschar, 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 comumIndexOf, 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;
}