A forma mais rápida de contar quantas vezes um caractere aparece em uma string em C# é source.AsSpan().Count(toFind), uma única chamada que não aloca nada. A forma com LINQ é source.Count(c => c == toFind), que é mais legível e custa um enumerador e um delegado.
Todas as outras abordagens desta página contam a mesma coisa por outro caminho: um loop, IndexOf(), Split(), Replace() ou uma expressão regular. Elas diferem em velocidade e alocação, não no resultado, e o benchmark no final do artigo mostra essa diferença em números.
Uso do Count() do LINQ
Podemos usar System.Linq para contar o número de caracteres com o método Count().
Todos os métodos deste artigo ficam em uma classe pública CountChars, em um arquivo CountChars.cs próprio dentro de uma aplicação de console. Vamos criar um método para ver como usar Count:
public int CountCharsUsingLinqCount(string source, char toFind)
{
return source.Count(t => t == toFind);
}
O método Count() do Linq itera sobre cada caractere da string e executa o predicado, no nosso caso t => t == toFind, para cada um deles.
Executamos cada método no corpo de um teste [Fact] do xUnit, em um projeto de testes que referencia a aplicação de console e cuja classe de teste tem um campo private static readonly CountChars _countChars = new();. Ao executar o método, obtemos o número de ocorrências do caractere toFind na string main:
string main = "Mary Had A Little Lamb"; char toFind = 'L'; int actual = _countChars.CountCharsUsingLinqCount(main, toFind); Assert.Equal(2, actual);
Uso do loop foreach
Uma forma simples de iterar sobre todos os caracteres da string é usar o loop foreach. Isso nos permite manter a contagem das ocorrências de qualquer caractere.
Vamos criar um método que conta quantas vezes um caractere aparece na string usando foreach:
public int CountCharsUsingForeach(string source, char toFind)
{
int count = 0;
foreach (var ch in source)
{
if (ch == toFind)
count++;
}
return count;
}
Mantemos uma variável contadora count e aumentamos o valor dela toda vez que encontramos o caractere desejado na string.
Assim, esse método retorna o número de ocorrências de toFind na string main:
string main = "Mary Had A Little Lamb"; char toFind = 'L'; int actual = _countChars.CountCharsUsingForeach(main, toFind); Assert.Equal(2, actual);
Uso do loop foreach com Span
Também podemos usar ReadOnlySpan<T> dentro de um loop foreach para contar as ocorrências do caractere. O método de extensão AsSpan() retorna um ReadOnlySpan<char> sobre uma string. Um ReadOnlySpan<char> é uma visão da memória que a string já possui: AsSpan() não copia nada e não aloca nada. O ganho é poder fatiar sem alocar. Isso não deixa um foreach escrito à mão nem um pouco mais rápido: no .NET 10, o loop com span e o loop com string têm o mesmo desempenho.
Aqui, a única mudança em relação à abordagem anterior é que aplicamos o método AsSpan() à string de origem:
public int CountCharsUsingForeachSpan(string source, char toFind)
{
int count = 0;
foreach (var c in source.AsSpan())
{
if (c == toFind)
count++;
}
return count;
}
O método retorna quantas vezes toFind ocorreu na variável string main:
string main = "Mary Had A Little Lamb"; char toFind = 'L'; int actual = _countChars.CountCharsUsingForeachSpan(main, toFind); Assert.Equal(2, actual);
Uso do loop for
Outra forma de iterar sobre uma string é usar o loop for. Ele funciona de forma parecida com foreach. Contamos quantas vezes um caractere ocorre na string de origem:
public int CountCharsUsingFor(string source, char toFind)
{
int count = 0;
for (int n = 0; n < source.Length; n++)
{
if (source[n] == toFind)
count++;
}
return count;
}
Também podemos usar um char[] em vez de uma string, mas isso não traz nenhuma vantagem de desempenho. A chamada ao método ToCharArray() aloca um novo array e copia a string inteira para ele.
Ao ser executado, esse método retorna a contagem do caractere toFind na string main:
string main = "Mary Had A Little Lamb"; char toFind = 'L'; int actual = _countChars.CountCharsUsingFor(main, toFind); Assert.Equal(2, actual);
Uso do método IndexOf()
Uma forma de contar as ocorrências de um caractere em uma string é usar o método IndexOf() da classe string.
Mantemos uma variável contadora e a incrementamos toda vez que a instrução source.IndexOf(toFind, n) + 1 retorna um valor maior que 0, ou seja, quando o caractere existe na string:
public int CountCharsUsingIndex(string source, char toFind)
{
int count = 0;
int n = 0;
while ((n = source.IndexOf(toFind, n) + 1) != 0)
count++;
return count;
}
Como IndexOf() retorna a posição da ocorrência e a expressão soma um, n já aponta para o caractere seguinte à ocorrência, então a próxima busca começa no lugar certo e, quando há duas ocorrências adjacentes, as duas são contadas.
Assim como nos métodos anteriores, ao executar o método obtemos o número de ocorrências do caractere toFind na string main:
string main = "Mary Had A Little Lamb"; char toFind = 'L'; int actual = _countChars.CountCharsUsingIndex(main, toFind); Assert.Equal(2, actual);
Uso do método Split()
Podemos contar o número de caracteres com o método Split():
public int CountCharsUsingSplit(string source, char toFind)
{
return source.Split(toFind).Length - 1;
}
Neste exemplo, dividimos a string principal usando o caractere como delimitador. O resultado é um array de strings cujo comprimento é 1 a mais que o número de ocorrências do caractere.
Podemos executar esse método para descobrir quantas vezes o caractere ocorre:
string main = "Mary Had A Little Lamb"; char toFind = 'L'; int actual = _countChars.CountCharsUsingSplit(main, toFind); Assert.Equal(2, actual);
Uso do método Replace() de String
Vamos criar um método que conta o número de ocorrências de um caractere com o método Replace():
public int CountCharsUsingReplace(string source, char toFind)
{
return source.Length - source.Replace(toFind.ToString(), "").Length;
}
Aqui, convertemos o caractere em string e o substituímos por uma string vazia. Depois, calculamos a diferença entre o comprimento da string original e o da string resultante.
Ao executar o método, obtemos o número de ocorrências de um caractere:
string main = "Mary Had A Little Lamb"; char toFind = 'L'; int actual = _countChars.CountCharsUsingReplace(main, toFind); Assert.Equal(2, actual);
Uso de correspondência de padrões com Regex
Também podemos usar a correspondência de padrões (pattern matching) com Regex para contar o número de ocorrências de um caractere.
Vamos criar um método para isso, com using System.Text.RegularExpressions; no início de CountChars.cs:
public int CountCharsUsingRegex(string source, char toFind)
{
return Regex.Count(source, Regex.Escape(toFind.ToString()));
}
O método estático Regex.Count() chegou no .NET 7 e conta as correspondências sem que precisemos criar um objeto Regex a cada chamada. Regex.Escape() continua no código, porque é ele que faz o método funcionar tanto com letras quanto com caracteres como . e *.

E-book gratuito
Sua Web API está pronta para produção?
33 itens para verificar antes de implantá-la, com a correção de cada um. Um PDF gratuito de 76 páginas para .NET 10.
O e-book está em inglês.
Baixe o checklist gratuitoPDF gratuito. Um único e-mail para enviá-lo. Cancele a inscrição quando quiser.
Em seguida, podemos executar esse método para contar as ocorrências de um caractere em uma string:
string main = "Mary Had A Little Lamb"; char toFind = 'L'; int actual = _countChars.CountCharsUsingRegex(main, toFind); Assert.Equal(2, actual);
Uso do Count() de Span
Count() faz parte da biblioteca de classes base. MemoryExtensions.Count() estende ReadOnlySpan<T> e Span<T>, então AsSpan() seguido de Count() não precisa de nenhuma referência de pacote:
public int CountCharsUsingSpanCount(string source, char toFind)
{
return source.AsSpan().Count(toFind);
}
A documentação da Microsoft para MemoryExtensions.Count descreve a sobrecarga que estamos chamando aqui:
Conta o número de vezes que o valor especificado ocorre no span.
Essa é a abordagem mais rápida do benchmark abaixo, e é a linha que vale a pena guardar deste artigo.
Esse método chegou no .NET 8. No .NET 7 e anteriores, a mesma extensão vinha do pacote CommunityToolkit.HighPerformance, de onde este artigo a tirou originalmente, e esse pacote continua sendo a forma de usá-la em frameworks de destino mais antigos.
Agradecemos ao nosso leitor Joel por ter mencionado isso na seção de comentários.
O C# tem um método String.Count()?
Não. System.String expõe Length, e não Count, e toda chamada a Count() nesta página vem de outro lugar.
Dois métodos de extensão o fornecem, e eles não são o mesmo método. Enumerable.Count() fica em System.Linq e se aplica porque uma string é um IEnumerable<char>, então percorre a string com um enumerador e testa cada caractere com um predicado.
MemoryExtensions.Count() fica em System e se aplica ao ReadOnlySpan<char> que AsSpan() retorna, então procura um único valor na memória, sem predicado e sem enumerador.
O compilador define qual é qual. Em um projeto com ImplicitUsings desativado, se removermos using System.Linq de um arquivo, source.Count(c => c == toFind) deixa de compilar, com error CS0411: The type arguments for method 'MemoryExtensions.Count<T>(Span<T>, T)' cannot be inferred from the usage.
Então, quem procura por string.Count está, na verdade, fazendo duas perguntas. Se o critério de correspondência é uma condição, a resposta é o LINQ. Se o que contamos é um único caractere conhecido, a resposta é a sobrecarga de span.
Como contar as ocorrências de uma substring em C#?
Count() em um span aceita um segundo span, então contar uma palavra custa a mesma chamada que contar um caractere: source.AsSpan().Count("la") retorna quantas vezes la aparece.
A contagem não considera sobreposições, e esse é o detalhe que vale conhecer antes de confiar no número. "aaaa".AsSpan().Count("aa") retorna 2, e não 3, porque a busca recomeça depois de cada ocorrência, em vez de avançar só uma posição. Split() e Regex.Count() concordam com ele e também retornam 2.
Para ocorrências sobrepostas, usamos um loop em vez disso. Chame IndexOf() com StringComparison.Ordinal, conte a ocorrência e avance a posição inicial em um, e não pelo comprimento da ocorrência.
Esse argumento StringComparison não é enfeite. A sobrecarga string.IndexOf(string) compara usando a cultura atual por padrão, então o mesmo código pode retornar uma contagem diferente em outra máquina, enquanto as sobrecargas com char e as sobrecargas de span são ordinais, a menos que passemos a elas um StringComparison sensível à cultura.
Vamos escrever as duas interpretações como um par de métodos:
public int CountSubstringNonOverlapping(string source, string toFind)
{
return source.AsSpan().Count(toFind);
}
public int CountSubstringOverlapping(string source, string toFind)
{
var count = 0;
for (var n = 0; (n = source.IndexOf(toFind, n, StringComparison.Ordinal)) != -1; n++)
count++;
return count;
}
Contar quantas vezes uma substring ocorre é uma pergunta diferente de encontrar todas as posições em que uma substring aparece. E, se além do total também queremos os trechos entre as ocorrências, as sobrecargas de Split() e StringSplitOptions são a melhor ferramenta.

E-book gratuito
Sua Web API está pronta para produção?
33 itens para verificar antes de implantá-la, com a correção de cada um. Um PDF gratuito de 76 páginas para .NET 10.
O e-book está em inglês.
Baixe o checklist gratuitoPDF gratuito. Um único e-mail para enviá-lo. Cancele a inscrição quando quiser.
Como contar todos os caracteres de uma string de uma vez?
Uma única passagem e um dicionário dão a resposta para todos os caracteres de uma vez, em vez de executar um dos métodos acima uma vez para cada caractere.
Com LINQ, isso cabe em uma linha. source.GroupBy(c => c).ToDictionary(g => g.Key, g => g.Count()) agrupa a string por caractere e armazena o tamanho de cada grupo, então consultar 'l' no resultado mostra quantas vezes ele apareceu.
Um loop simples evita as alocações do agrupamento e é igualmente legível. Percorra a string uma vez e escreva counted[c] = counted.GetValueOrDefault(c) + 1, que insere a chave na primeira vez que ela aparece e, nas vezes seguintes, incrementa a contagem associada a ela.
As duas formas diferenciam maiúsculas de minúsculas, então A e a são chaves separadas. Quando isso não for desejado, padronize as letras com char.ToLowerInvariant(c) antes do agrupamento.
Nenhuma das duas gera uma classificação. Um Dictionary<char, int> não tem ordem, então, para achar o caractere mais frequente, é preciso ordenar o dicionário por valor depois.
As duas formas, lado a lado:
public Dictionary<char, int> CountEveryCharUsingLinq(string source)
{
return source.GroupBy(c => c).ToDictionary(g => g.Key, g => g.Count());
}
public Dictionary<char, int> CountEveryCharUsingLoop(string source)
{
var counted = new Dictionary<char, int>();
foreach (var c in source)
counted[c] = counted.GetValueOrDefault(c) + 1;
return counted;
}
As duas formas retornam um Dictionary<char, int>. Quando a tarefa é mais restrita do que todos os caracteres e mais ampla do que um só, como contar um conjunto inteiro de caracteres, por exemplo todas as vogais, uma única passagem com um teste dentro dela resolve, sem precisar montar dicionário nenhum.
Comparação de desempenho
Agora que vimos vários métodos para contar as ocorrências de caracteres em uma string, vamos ver como eles se saem quando comparados entre si.
Vamos usar o BenchmarkDotNet para executar os benchmarks de desempenho. Adicionamos o pacote com dotnet add package BenchmarkDotNet, colocamos using BenchmarkDotNet.Attributes; no início de CountChars.cs e [MemoryDiagnoser] na classe para ter as colunas de alocação, e substituímos o código de Program.cs por BenchmarkDotNet.Running.BenchmarkRunner.Run<CountChars>();.
Primeiro, vamos criar o método GenerateStringWithCharArgs():
public IEnumerable<object[]> GenerateStringWithCharArgs()
{
yield return new object[] { "Mary had a little lamb", 'l' };
}
Esse método vai nos ajudar a executar testes de desempenho em todos os métodos vistos acima que contam as ocorrências de um caractere em uma string.
Para medir o desempenho dos métodos com o BenchmarkDotNet, precisamos marcá-los com os atributos Benchmark e ArgumentsSource:
[Benchmark] [ArgumentsSource(nameof(GenerateStringWithCharArgs))] public int CountCharsUsingLinqCount(string source, char toFind)
O atributo ArgumentsSource recebe o nome do método público que vai fornecer os valores.
Vamos executá-lo com dotnet run -c Release e avaliar os resultados de desempenho:
| Method | source | toFind | Mean | Error | StdDev | Gen0 | Allocated | |----------------------------------- |--------------------- |------- |-----------:|----------:|----------:|-------:|----------:| | CountCharsUsingSpanCount | Mary (...) lamb [22] | l | 2.781 ns | 0.0832 ns | 0.0779 ns | - | - | | CountCharsUsingForReverseIteration | Mary (...) lamb [22] | l | 10.171 ns | 0.1138 ns | 0.1064 ns | - | - | | CountCharsUsingFor | Mary (...) lamb [22] | l | 10.713 ns | 0.1961 ns | 0.1834 ns | - | - | | CountCharsUsingForeachSpan | Mary (...) lamb [22] | l | 10.817 ns | 0.2450 ns | 0.3886 ns | - | - | | CountCharsUsingForeach | Mary (...) lamb [22] | l | 11.132 ns | 0.2502 ns | 0.2089 ns | - | - | | CountCharsUsingLinqCount | Mary (...) lamb [22] | l | 18.059 ns | 0.3919 ns | 0.6101 ns | 0.0029 | 24 B | | CountCharsUsingIndex | Mary (...) lamb [22] | l | 20.087 ns | 0.1714 ns | 0.1519 ns | - | - | | CountCharsUsingForWithSpan | Mary (...) lamb [22] | l | 20.277 ns | 0.3121 ns | 0.2766 ns | - | - | | CountCharsUsingSplit | Mary (...) lamb [22] | l | 58.498 ns | 1.2234 ns | 2.6595 ns | 0.0238 | 200 B | | CountCharsUsingReplace | Mary (...) lamb [22] | l | 64.534 ns | 1.2493 ns | 2.7159 ns | 0.0105 | 88 B | | CountCharsUsingRegex | Mary (...) lamb [22] | l | 185.276 ns | 2.1502 ns | 1.9061 ns | 0.0029 | 24 B |
Há mais alguns métodos no resultado do benchmark e, se quiser, você pode ver a implementação deles no código-fonte.
Vemos que CountCharsUsingSpanCount é a abordagem mais rápida, com uma margem clara, porque MemoryExtensions.Count() testa um vetor inteiro de caracteres a cada passo, em vez de um caractere por vez. Os loops escritos à mão desta página vêm em seguida e ficam próximos uns dos outros, quer percorram a string, quer o ReadOnlySpan<char>. Também vemos que esses métodos de iteração, assim como o método Index, não alocam memória.
Por outro lado, a correspondência de padrões com Regex é a solução mais lenta para encontrar o número de ocorrências de um caractere em uma string, e Split() e Replace() são os dois que mais alocam.
Conclusão
Neste artigo, vimos como contar o número de ocorrências de um caractere em uma string. Examinamos diferentes formas de fazer isso: com LINQ, iterando sobre a string e com outros métodos nativos.
A tabela abaixo lista todas as abordagens, se cada uma aloca memória e quando usá-la:
| Abordagem | Código | Aloca | Quando usar |
|---|---|---|---|
Count() de Span | source.AsSpan().Count(toFind) | Não | Sempre, a menos que a contagem tenha uma condição |
Loop for / foreach | if (c == toFind) count++; | Não | O loop já existe e também faz outro trabalho |
IndexOf() | while ((n = source.IndexOf(toFind, n) + 1) != 0) | Não | As posições importam, não só o total |
Count() do LINQ | source.Count(t => t == toFind) | Sim | O critério é um predicado, não um único caractere |
Replace() | source.Length - source.Replace(toFind.ToString(), "").Length | Sim | Nunca, para esta tarefa |
Split() | source.Split(toFind).Length - 1 | Sim | Os trechos interessam, além da contagem |
Regex.Count() | Regex.Count(source, Regex.Escape(toFind.ToString())) | Sim | O que se conta é um padrão |
Se for para guardar uma única linha, que seja source.AsSpan().Count(toFind) para um caractere conhecido e source.Count(c => c == toFind) quando o critério é uma condição, e não um único caractere.
Testado com .NET 10.0.10 e BenchmarkDotNet 0.15.8.