La forma más rápida de contar cuántas veces aparece un carácter en una cadena en C# es source.AsSpan().Count(toFind), una sola llamada que no asigna memoria. La forma con LINQ es source.Count(c => c == toFind), que se lee mejor y cuesta un enumerador y un delegado.
Los demás enfoques de esta página cuentan lo mismo por otro camino: un bucle, IndexOf(), Split(), Replace() o una expresión regular. Se diferencian en velocidad y en asignación de memoria, no en el resultado, y el benchmark del final del artículo pone cifras a esa diferencia.
Usar Count() de LINQ
Podemos usar System.Linq para contar las apariciones de un carácter con el método Count().
Todos los métodos de este artículo van en una clase pública CountChars, en su propio archivo CountChars.cs de una aplicación de consola. Vamos a crear un método para ver cómo se usa Count:
public int CountCharsUsingLinqCount(string source, char toFind)
{
return source.Count(t => t == toFind);
}
El método Count() de LINQ itera sobre cada carácter de la cadena y ejecuta el predicado, en nuestro caso t => t == toFind, para cada uno de ellos.
Ejecutamos cada método desde el cuerpo de una prueba [Fact] de xUnit, en un proyecto de pruebas que hace referencia a la aplicación de consola y cuya clase de pruebas tiene un campo private static readonly CountChars _countChars = new();. Al ejecutar el método, obtenemos el número de apariciones del carácter toFind en la cadena main:
string main = "Mary Had A Little Lamb"; char toFind = 'L'; int actual = _countChars.CountCharsUsingLinqCount(main, toFind); Assert.Equal(2, actual);
Usar un bucle foreach
Una forma sencilla de iterar sobre todos los caracteres de la cadena es usar el bucle foreach. Así podemos llevar la cuenta de las apariciones de cualquier carácter.
Vamos a crear un método que cuente con foreach cuántas veces aparece un carácter en la cadena:
public int CountCharsUsingForeach(string source, char toFind)
{
int count = 0;
foreach (var ch in source)
{
if (ch == toFind)
count++;
}
return count;
}
Mantenemos una variable contador, count, y aumentamos su valor cada vez que encontramos el carácter buscado en la cadena.
Así, este método devuelve el número de apariciones de toFind en la cadena main:
string main = "Mary Had A Little Lamb"; char toFind = 'L'; int actual = _countChars.CountCharsUsingForeach(main, toFind); Assert.Equal(2, actual);
Usar un bucle foreach con Span
También podemos usar ReadOnlySpan<T> dentro de un bucle foreach para contar las apariciones del carácter. El método de extensión AsSpan() devuelve un ReadOnlySpan<char> sobre un string. Un ReadOnlySpan<char> es una vista sobre memoria que ya pertenece a la cadena: AsSpan() no copia nada ni asigna nada. Lo que eso aporta es poder dividir en porciones (slicing) sin asignar memoria. No hace más rápido un foreach escrito a mano: en .NET 10, el bucle sobre el span y el bucle sobre la cadena rinden igual.
Aquí, el único cambio respecto al enfoque anterior es que aplicamos el método AsSpan() a la cadena de origen:
public int CountCharsUsingForeachSpan(string source, char toFind)
{
int count = 0;
foreach (var c in source.AsSpan())
{
if (c == toFind)
count++;
}
return count;
}
El método devuelve cuántas veces aparece toFind en la variable de cadena main:
string main = "Mary Had A Little Lamb"; char toFind = 'L'; int actual = _countChars.CountCharsUsingForeachSpan(main, toFind); Assert.Equal(2, actual);
Usar un bucle for
Otro enfoque para iterar sobre una cadena es usar el bucle for. Funciona de forma parecida a foreach, y en él contamos cuántas veces aparece un carácter en la cadena de origen:
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;
}
También podemos usar un char[] en lugar de un string, pero no aporta ninguna ventaja de rendimiento. La llamada al método ToCharArray() asigna un array nuevo y copia en él toda la cadena.
Al ejecutarlo, este método devuelve el número de veces que aparece el carácter toFind en la cadena main:
string main = "Mary Had A Little Lamb"; char toFind = 'L'; int actual = _countChars.CountCharsUsingFor(main, toFind); Assert.Equal(2, actual);
Usar el método IndexOf()
Una forma de contar las apariciones de un carácter dentro de una cadena es usar el método IndexOf() de la clase string.
Mantenemos una variable contador y la incrementamos cada vez que la expresión source.IndexOf(toFind, n) + 1 devuelve un valor mayor que 0, es decir, cada vez que el carácter existe en la cadena:
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() devuelve la posición de la coincidencia y la expresión le suma uno, n ya apunta al carácter que sigue a la coincidencia, así que la siguiente búsqueda empieza en el lugar correcto y, si hay dos apariciones contiguas, se cuentan las dos.
Igual que con los métodos anteriores, al ejecutar el método obtenemos el número de apariciones del carácter toFind en la cadena main:
string main = "Mary Had A Little Lamb"; char toFind = 'L'; int actual = _countChars.CountCharsUsingIndex(main, toFind); Assert.Equal(2, actual);
Usar el método Split()
Podemos contar las apariciones de un carácter con el método Split():
public int CountCharsUsingSplit(string source, char toFind)
{
return source.Split(toFind).Length - 1;
}
En este ejemplo, dividimos la cadena principal usando el carácter como delimitador. El resultado es un array de cadenas cuya longitud supera en 1 el número de apariciones del carácter.
Podemos ejecutar este método para averiguar cuántas veces aparece el carácter:
string main = "Mary Had A Little Lamb"; char toFind = 'L'; int actual = _countChars.CountCharsUsingSplit(main, toFind); Assert.Equal(2, actual);
Usar el método Replace() de String
Vamos a crear un método que cuente el número de apariciones de un carácter con el método Replace():
public int CountCharsUsingReplace(string source, char toFind)
{
return source.Length - source.Replace(toFind.ToString(), "").Length;
}
Aquí, convertimos el carácter en una cadena y lo reemplazamos por una cadena vacía. Después, calculamos la diferencia entre la longitud de la cadena original y la de la cadena resultante.
Al ejecutar el método, obtenemos el número de apariciones de un carácter:
string main = "Mary Had A Little Lamb"; char toFind = 'L'; int actual = _countChars.CountCharsUsingReplace(main, toFind); Assert.Equal(2, actual);
Usar la búsqueda de patrones con Regex
También podemos usar la búsqueda de patrones con Regex para contar el número de apariciones de un carácter.
Vamos a crear un método para hacerlo, con using System.Text.RegularExpressions; al principio de CountChars.cs:
public int CountCharsUsingRegex(string source, char toFind)
{
return Regex.Count(source, Regex.Escape(toFind.ToString()));
}
El método estático Regex.Count() llegó con .NET 7 y cuenta las coincidencias sin que tengamos que construir un objeto Regex en cada llamada. Regex.Escape() se mantiene, porque es lo que hace que el método funcione con caracteres como . y *, además de con letras.

Ebook gratis
¿Tu Web API está lista para producción?
33 puntos que comprobar antes de desplegarla, con la solución de cada uno. Un PDF gratuito de 76 páginas para .NET 10.
El ebook está en inglés.
Descarga la checklist gratisPDF gratuito. Un solo correo para enviártelo. Puedes darte de baja cuando quieras.
Después, podemos ejecutar este método para contar las apariciones de un carácter dentro de una cadena:
string main = "Mary Had A Little Lamb"; char toFind = 'L'; int actual = _countChars.CountCharsUsingRegex(main, toFind); Assert.Equal(2, actual);
Usar Count() de Span
Count() forma parte de la biblioteca de clases base. MemoryExtensions.Count() extiende ReadOnlySpan<T> y Span<T>, así que AsSpan() seguido de Count() no necesita ninguna referencia a paquetes:
public int CountCharsUsingSpanCount(string source, char toFind)
{
return source.AsSpan().Count(toFind);
}
La documentación de Microsoft sobre MemoryExtensions.Count describe así la sobrecarga que llamamos aquí:
Cuenta el número de veces que el valor especificado aparece en el span.
Es el enfoque más rápido del benchmark que aparece más abajo, y es la línea que hay que recordar de este artículo.
Este método llegó con .NET 8. En .NET 7 y versiones anteriores, el mismo método de extensión venía del paquete CommunityToolkit.HighPerformance, que es de donde lo tomó este artículo en su primera versión, y ese paquete sigue siendo la forma de usarlo en frameworks de destino más antiguos.
Tenemos que dar las gracias a nuestro lector Joel por mencionarlo en la sección de comentarios.
¿Tiene C# un método String.Count()?
No. System.String expone Length, no Count, y todas las llamadas a Count() de esta página vienen de otra parte.
Lo aportan dos métodos de extensión, y no son el mismo método. Enumerable.Count() está en System.Linq y se aplica porque un string es un IEnumerable<char>, así que recorre la cadena con un enumerador y evalúa cada carácter con un predicado.
MemoryExtensions.Count() está en System y se aplica al ReadOnlySpan<char> que devuelve AsSpan(), así que busca un valor en memoria sin predicado y sin enumerador.
El compilador deja claro cuál es cuál. En un proyecto con ImplicitUsings desactivado, si quitas using System.Linq de un archivo, source.Count(c => c == toFind) deja de compilar, con error CS0411: The type arguments for method 'MemoryExtensions.Count<T>(Span<T>, T)' cannot be inferred from the usage.
Así que quien busca string.Count se está haciendo en realidad dos preguntas. Si la coincidencia es una condición, es LINQ. Si se cuenta un carácter conocido, es la sobrecarga de span.
¿Cómo contar las apariciones de una subcadena en C#?
Count() sobre un span acepta un segundo span, así que contar una palabra requiere la misma llamada que contar un carácter: source.AsSpan().Count("la") devuelve cuántas veces aparece la.
El recuento no admite solapamientos, y ese es el detalle que conviene conocer antes de fiarse del número. "aaaa".AsSpan().Count("aa") devuelve 2, no 3, porque la búsqueda continúa después de cada coincidencia y no una posición más adelante. Split() y Regex.Count() coinciden con él y también devuelven 2.
Para coincidencias solapadas, usamos un bucle. Llama a IndexOf() con StringComparison.Ordinal, cuenta la coincidencia y avanza la posición inicial en uno, no en la longitud de la coincidencia.
Ese argumento StringComparison no es decorativo. La sobrecarga string.IndexOf(string) compara de forma predeterminada según la cultura actual, así que el mismo código puede devolver un recuento distinto en otra máquina, mientras que las sobrecargas char y las sobrecargas de span son ordinales salvo que les pasemos un StringComparison que tenga en cuenta la cultura.
Vamos a escribir las dos interpretaciones como un 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 cuántas veces aparece una subcadena es una pregunta distinta de encontrar todas las posiciones en las que aparece una subcadena. Y si, además del total, queremos los fragmentos que hay entre las coincidencias, las sobrecargas de Split() y StringSplitOptions son la mejor herramienta.

Ebook gratis
¿Tu Web API está lista para producción?
33 puntos que comprobar antes de desplegarla, con la solución de cada uno. Un PDF gratuito de 76 páginas para .NET 10.
El ebook está en inglés.
Descarga la checklist gratisPDF gratuito. Un solo correo para enviártelo. Puedes darte de baja cuando quieras.
¿Cómo contar todos los caracteres de una cadena a la vez?
Una sola pasada y un diccionario dan la respuesta para todos los caracteres a la vez, en lugar de ejecutar uno de los métodos anteriores una vez por carácter.
Con LINQ se escribe en una línea. source.GroupBy(c => c).ToDictionary(g => g.Key, g => g.Count()) agrupa la cadena por carácter y guarda el tamaño de cada grupo, así que, si buscamos 'l' en el resultado, obtenemos cuántas veces apareció.
Un bucle normal evita las asignaciones de la agrupación y se lee igual de bien. Recorre la cadena una vez y escribe counted[c] = counted.GetValueOrDefault(c) + 1, que inserta la clave la primera vez que aparece y después incrementa su valor.
Los dos distinguen mayúsculas de minúsculas, así que A y a son claves distintas. Si no es lo que quieres, normalízalas con char.ToLowerInvariant(c) antes de agrupar.
Ninguno de los dos da una clasificación. Un Dictionary<char, int> no tiene orden, así que el carácter más frecuente se obtiene ordenando después el diccionario por valor.
Las dos formas, una junto a la otra:
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;
}
Las dos formas devuelven un Dictionary<char, int>. Cuando la tarea abarca más de un carácter, pero no todos, como contar todo un conjunto de caracteres, por ejemplo todas las vocales, una sola pasada con una comprobación dentro hace el trabajo sin construir ningún diccionario.
Comparación de rendimiento
Ahora que hemos visto varios métodos para contar las apariciones de caracteres dentro de una cadena, veamos cómo rinden comparados entre sí.
Usaremos BenchmarkDotNet para ejecutar los benchmarks de rendimiento. Lo añadimos con dotnet add package BenchmarkDotNet, ponemos using BenchmarkDotNet.Attributes; al principio de CountChars.cs y [MemoryDiagnoser] en la clase para obtener las columnas de asignación de memoria, y reemplazamos el código de Program.cs por BenchmarkDotNet.Running.BenchmarkRunner.Run<CountChars>();.
Primero, vamos a crear un método GenerateStringWithCharArgs():
public IEnumerable<object[]> GenerateStringWithCharArgs()
{
yield return new object[] { "Mary had a little lamb", 'l' };
}
Este método nos servirá para ejecutar pruebas de rendimiento sobre todos los métodos anteriores que cuentan las apariciones de un carácter en una cadena.
Para medir el rendimiento de los métodos con BenchmarkDotNet, tenemos que marcarlos con los atributos Benchmark y ArgumentsSource:
[Benchmark] [ArgumentsSource(nameof(GenerateStringWithCharArgs))] public int CountCharsUsingLinqCount(string source, char toFind)
El atributo ArgumentsSource recibe el nombre del método público que va a proporcionar los valores.
Vamos a ejecutarlo con dotnet run -c Release y a evaluar los resultados de rendimiento:
| 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 |
En el resultado del benchmark aparecen algunos métodos más y, si quieres, puedes consultar su implementación en el código fuente.
Vemos que CountCharsUsingSpanCount es, con un margen claro, el enfoque más rápido, porque MemoryExtensions.Count() evalúa un vector entero de caracteres en cada paso en lugar de un carácter cada vez. Después vienen los bucles escritos a mano de esta página, próximos entre sí, tanto si recorren el string como el ReadOnlySpan<char>. Además, vemos que estos métodos de iteración, junto con el método Index, no asignan memoria.
Por otro lado, la búsqueda de patrones con Regex es la solución más lenta para averiguar el número de apariciones de un carácter en una cadena, y Split() y Replace() son los dos que más memoria asignan.
Conclusión
En este artículo, hemos visto cómo contar el número de apariciones de un carácter dentro de una cadena. Hemos repasado distintas formas de hacerlo: con LINQ, iterando sobre la cadena y con otros métodos integrados.
La tabla siguiente recoge cada enfoque, si asigna memoria y cuándo conviene usarlo:
| Enfoque | Código | Asigna memoria | Úsalo cuando |
|---|---|---|---|
Count() de Span | source.AsSpan().Count(toFind) | No | Siempre, salvo que el recuento tenga una condición |
Bucle for / foreach | if (c == toFind) count++; | No | El bucle ya existe y también hace otro trabajo |
IndexOf() | while ((n = source.IndexOf(toFind, n) + 1) != 0) | No | Importan las posiciones, no solo el total |
Count() de LINQ | source.Count(t => t == toFind) | Sí | La coincidencia es un predicado, no un solo carácter |
Replace() | source.Length - source.Replace(toFind.ToString(), "").Length | Sí | Nunca, para esta tarea |
Split() | source.Split(toFind).Length - 1 | Sí | Se quieren los fragmentos además del recuento |
Regex.Count() | Regex.Count(source, Regex.Escape(toFind.ToString())) | Sí | Lo que se cuenta es un patrón |
Si hay que quedarse con una sola línea, es source.AsSpan().Count(toFind) para un carácter conocido, y source.Count(c => c == toFind) cuando la coincidencia es una condición y no un solo carácter.
Probado con .NET 10.0.10 y BenchmarkDotNet 0.15.8.