Updated on

The fastest way to count how many times a character appears in a string in C# is source.AsSpan().Count(toFind), a single call that allocates nothing. The LINQ way is source.Count(c => c == toFind), which reads better and costs an enumerator and a delegate.

Every other approach on this page counts the same thing by a different route: a loop, IndexOf(), Split(), Replace() or a regular expression. They differ in speed and allocation, not in the answer, and the benchmark at the end of the article puts numbers on the difference.

To download the source code for this article, you can visit our GitHub repository.

Using LINQ Count()

We can use System.Linq to count the number of characters using the Count() method.

Let’s create a method to look at the usage of Count:

public int CountCharsUsingLinqCount(string source, char toFind)
{
    return source.Count(t => t == toFind);
}

The Count() method of Linq iterates over every character of the string and executes the predicate, in our case t => t == toFind for each of them.

On executing the method, we get the number of occurrences of the character toFind in the string main:

string main = "Mary Had A Little Lamb";
char toFind = 'L';

int actual = _countChars.CountCharsUsingLinqCount(main, toFind);

Assert.Equal(2, actual);

Using foreach Loop

A simple approach to iterating over all the characters of the given string is to use the foreach loop. This allows us to keep a count of the occurrence of any character.

Let’s create a method to count how many times a character appears in the string using foreach:

public int CountCharsUsingForeach(string source, char toFind)
{
    int count = 0;

    foreach (var ch in source)
    {
        if (ch == toFind)
            count++;
    }

    return count;
}

We keep a counter variable count and keep increasing its value every time we encounter the desired character in the string.

Thus, this method returns the number of occurrences of toFind in the string main:

string main = "Mary Had A Little Lamb";
char toFind = 'L';

int actual = _countChars.CountCharsUsingForeach(main, toFind);

Assert.Equal(2, actual);

Using foreach Loop With Span

We can also make use of Span<T> inside a foreach loop to count the character occurrences. The AsSpan() extension method returns a ReadOnlySpan<char> over a string. A ReadOnlySpan<char> is a view over memory the string already owns: AsSpan() copies nothing and allocates nothing. What that buys is slicing without allocating. It does not make a hand-written foreach any faster: on .NET 10 the span loop and the string loop measure the same.

Here, the only change from the previous approach is that we apply the AsSpan() method on the source string:

public int CountCharsUsingForeachSpan(string source, char toFind)
{
    int count = 0;

    foreach (var c in source.AsSpan())
    {
        if (c == toFind)
            count++;
    }

    return count;
}

The method returns how many times toFind occurred in the string variable main:

string main = "Mary Had A Little Lamb";
char toFind = 'L';

int actual = _countChars.CountCharsUsingForeachSpan(main, toFind);

Assert.Equal(2, actual);

Using For Loop

Another approach to iterating over a string is to use for loop. This works in a similar manner to foreach where we count the number of times a character occurs in the source string:

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;
}

We can also use a char[] instead of a string, but it brings no performance advantage. The call to the ToCharArray() method allocates a new array and copies the whole string into it.

This method returns the count of the character toFind in the string main on execution:

string main = "Mary Had A Little Lamb";
char toFind = 'L';

int actual = _countChars.CountCharsUsingFor(main, toFind);

Assert.Equal(2, actual);

Using IndexOf() Method

A way to count the occurrence of a character within a string is using the IndexOf() method of the string class.

We keep a counter variable and increment it every time the statement source.IndexOf(toFind, n) + 1 returns a value greater than 0. i.e. the character exists in the 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;
}

Because IndexOf() returns the match position and the expression adds one, n already points at the character after the match, so the next search starts in the right place and two adjacent occurrences are both counted.

Similar to the previous methods, on executing the method we get the number of occurrences of the character toFind in the string main:

string main = "Mary Had A Little Lamb";
char toFind = 'L';

int actual = _countChars.CountCharsUsingIndex(main, toFind);

Assert.Equal(2, actual);

Using Split() Method

We can count the number of characters using the Split() method:

public int CountCharsUsingSplit(string source, char toFind)
{
    return source.Split(toFind).Length - 1;
}

In this example, we split the main string using the character as a delimiter. This results in an array of strings whose length is 1 more than the substring occurrence.

We can execute this method to find the occurrence of the character:

string main = "Mary Had A Little Lamb";
char toFind = 'L';

int actual = _countChars.CountCharsUsingSplit(main, toFind);

Assert.Equal(2, actual);

Using String Replace() Method

Let’s create a method to count the number of occurrences of a character using the Replace() method:

public int CountCharsUsingReplace(string source, char toFind)
{
    return source.Length - source.Replace(toFind.ToString(), "").Length;
}

Here, we convert the character to a string and replace it with an empty string. We then find the difference between the length of the original string and the resulting string.

On executing the method, we get the number of occurrences of a character:

string main = "Mary Had A Little Lamb";
char toFind = 'L';

int actual = _countChars.CountCharsUsingReplace(main, toFind);

Assert.Equal(2, actual);

Using Regex Pattern Matching

We can also use Regex pattern matching to count the number of occurrences of a character.

Let’s create a method to do so:

public int CountCharsUsingRegex(string source, char toFind)
{
    return Regex.Count(source, Regex.Escape(toFind.ToString()));
}

The static Regex.Count() method arrived in .NET 7 and counts the matches without us building a Regex object on every call. Regex.Escape() stays, because it is what makes the method work for characters such as . and * as well as for letters.

We can then execute this method to count the number of a character within a string:

string main = "Mary Had A Little Lamb";
char toFind = 'L';

int actual = _countChars.CountCharsUsingRegex(main, toFind);

Assert.Equal(2, actual);

Using Span Count()

Count() is part of the base class library. MemoryExtensions.Count() extends ReadOnlySpan<T> and Span<T>, so AsSpan() followed by Count() needs no package reference at all:

public int CountCharsUsingSpanCount(string source, char toFind)
{
    return source.AsSpan().Count(toFind);
}

Microsoft’s documentation for MemoryExtensions.Count describes the overload we are calling here:

Counts the number of times the specified value occurs in the span.

This is the fastest approach in the benchmark below, and it is the one line to remember from this article.

This method arrived in .NET 8. On .NET 7 and earlier the same extension came from the CommunityToolkit.HighPerformance package, which is where this article first got it, and that package is still the way to reach it on older target frameworks.

We have to give thanks to our reader Joel for mentioning this in the comment section.

Does C# Have a String.Count() Method?

No. System.String exposes Length, not Count, and every Count() call on this page arrives from somewhere else.

Two extension methods supply it, and they are not the same method. Enumerable.Count() lives in System.Linq and applies because a string is an IEnumerable<char>, so it walks the string through an enumerator and tests each character against a predicate.

MemoryExtensions.Count() lives in System and applies to the ReadOnlySpan<char> that AsSpan() returns, so it searches memory for one value with no predicate and no enumerator.

The compiler settles which is which. In a project with ImplicitUsings turned off, drop using System.Linq from a file and source.Count(c => c == toFind) stops compiling, with error CS0411: The type arguments for method 'MemoryExtensions.Count<T>(Span<T>, T)' cannot be inferred from the usage.

So a search for string.Count is really two questions. If the match is a condition, that is LINQ. If one known character is being counted, that is the span overload.

How Do We Count Occurrences of a Substring in C#?

Count() on a span takes a second span, so counting a word costs the same call as counting a character: source.AsSpan().Count("la") returns how many times la appears.

Counting is non-overlapping, which is the detail worth knowing before trusting the number. "aaaa".AsSpan().Count("aa") returns 2, not 3, because the search resumes after each match rather than one position later. Split() and Regex.Count() agree with it and return 2 as well.

Overlapping matches need the loop instead. Call IndexOf() with StringComparison.Ordinal, count the hit, then advance the start position by one rather than by the length of the match.

That StringComparison argument is not decoration. The string.IndexOf(string) overload compares using the current culture by default, so the same code can return a different count on a different machine, while the char overloads and the span overloads are ordinal unless we pass them a culture-sensitive StringComparison.

Let’s write both readings as a pair of methods:

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;
}

Counting how many times a substring occurs is a different question from asking find every position a substring appears at, and if the pieces between the matches are wanted as well as the total, the Split() overloads and StringSplitOptions are the better tool.

How Do We Count Every Character in a String at Once?

One pass and a dictionary answers for every character at once, instead of running one of the methods above once per character.

LINQ writes it in one line. source.GroupBy(c => c).ToDictionary(g => g.Key, g => g.Count()) groups the string by character and stores each group’s size, so looking up 'l' in the result reads back how many times it appeared.

A plain loop avoids the grouping allocations and reads just as well. Walk the string once and write counted[c] = counted.GetValueOrDefault(c) + 1, which inserts the key the first time it is seen and increments it afterwards.

Both are case sensitive, so A and a are separate keys. Fold them with char.ToLowerInvariant(c) before the grouping when that is not wanted.

Neither gives a ranking. A Dictionary<char, int> has no order, so the most frequent character comes from ordering the dictionary by value afterwards.

Both forms, side by side:

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;
}

Both forms return a Dictionary<TKey, int>. When the job is narrower than every character and wider than one, such as counting a whole set of characters, such as every vowel, a single pass with a test inside it does the job without building a dictionary at all.

Performance Comparison

Now that we have looked at various methods to count the occurrences of characters within a string, let’s see how they perform against each other.

We’ll be using BenchmarkDotNet to run the performance benchmarks.

First, let’s create a method GenerateStringWithCharArgs():

public IEnumerable<object[]> GenerateStringWithCharArgs()
{
    yield return new object[] { "Mary had a little lamb", 'l' };
}

This method will help us run performance tests on all the methods mentioned above that count the occurrences of a character in a string.

To performance test the methods with BenchmarkDotNet, we must mark the methods using the attributes Benchmark and ArgumentsSource:

[Benchmark]
[ArgumentsSource(nameof(GenerateStringWithCharArgs))]
public int CountCharsUsingLinqCount(string source, char toFind)

The attribute ArgumentsSource takes the name of the public method that is going to provide the values.

Let’s assess the performance results:

| 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 |

There are a few more methods in the benchmark result, and if you want, you can inspect their implementation in the source code.

We can see that CountCharsUsingSpanCount is the fastest approach by a clear margin, because MemoryExtensions.Count() tests a whole vector of characters per step instead of one character at a time. The hand-written loops on this page come next and are close to each other, whether they walk the string or the Span<char>. Also, we see these iteration methods, alongside the Index method, allocate no memory.

On the other hand, Regex pattern matching is the slowest solution to find the number of occurrences of a character in a string, and Split() and Replace() are the two that allocate the most.

Conclusion

In this article, we learned about counting the number of occurrences of a character within a string. We looked at different ways to do so using LINQ, iterating over the string, and other in-built methods.

The table below lists every approach, whether it allocates, and when to reach for it:

ApproachCodeAllocatesReach for it when
Span Count()source.AsSpan().Count(toFind)NoAlways, unless the count has a condition
for / foreach loopif (c == toFind) count++;NoThe loop already exists and does other work too
IndexOf()while ((n = source.IndexOf(toFind, n) + 1) != 0)NoThe positions matter, not only the total
LINQ Count()source.Count(t => t == toFind)YesThe match is a predicate, not one character
Replace()source.Length - source.Replace(toFind.ToString(), "").LengthYesNever, on this job
Split()source.Split(toFind).Length - 1YesThe pieces are wanted as well as the count
Regex.Count()Regex.Count(source, Regex.Escape(toFind.ToString()))YesThe thing being counted is a pattern

If there is one line to take away, it is source.AsSpan().Count(toFind) for a known character, and source.Count(c => c == toFind) when the match is a condition rather than a single character.

Tested with .NET 10.0.10 and BenchmarkDotNet 0.15.8.