はじめに 「文字頻度カウント」は“テキストのクセを数字で見る”道具
文字頻度カウントは、「この文字列の中で、どの文字が何回出てくるか」を数える処理です。
一見お勉強っぽいテーマですが、業務でもログ解析、入力チェック、簡易統計、レポート生成などで普通に出番があります。
例えば、
「このログメッセージで ‘E’(Error)がどれくらい出ているか知りたい」
「ユーザー入力に、特定の記号が多すぎないかチェックしたい」
「テキストの傾向をざっくり見たい」
といったときに、文字頻度カウントのユーティリティがあると便利です。
ここでは、初心者向けに、
基本的な実装パターン、Dictionary<char,int> を使う理由、LINQ版との違い、Unicodeをどう考えるか、
そして業務ユーティリティとしてどうまとめるかを、例題付きでかみ砕いて説明します。
基本の考え方 「1文字ずつ見て、カウンタを増やす」
文字頻度カウントの考え方はとてもシンプルです。
文字列を先頭から1文字ずつ見る。
その文字の「カウンタ」を1増やす。
最後に「文字 → 出現回数」の一覧ができている。
この「文字 → 回数」の対応を持つ入れ物として、C#では Dictionary<char, int> を使うのが定番です。
キーが文字、値がその文字の出現回数、というイメージです。
基本実装:Dictionary<char,int> を使った文字頻度カウント
一番素直なユーティリティメソッド
まずは、string を受け取って Dictionary<char,int> を返す、基本形を作ってみます。
using System;
using System.Collections.Generic;
public static class CharFrequencyUtil
{
public static IReadOnlyDictionary<char, int> CountChars(string? text)
{
var result = new Dictionary<char, int>();
if (string.IsNullOrEmpty(text))
{
return result;
}
foreach (char c in text)
{
if (result.ContainsKey(c))
{
result[c]++;
}
else
{
result[c] = 1;
}
}
return result;
}
}
C#ここでやっていることを言葉で整理すると、こうなります。
まず、結果を入れるための Dictionary<char,int> を用意する。
文字列が null や空なら、そのまま空の辞書を返す(呼び出し側を楽にするための方針)。
文字列を1文字ずつループする。
その文字が辞書にすでにあれば、カウンタを1増やす。
なければ、新しくキーを追加して 1 を入れる。
これだけで、「どの文字が何回出たか」が全部集計できます。
動作例でイメージを固める
var freq = CharFrequencyUtil.CountChars("ABCAA");
foreach (var pair in freq)
{
Console.WriteLine($"'{pair.Key}' : {pair.Value}");
}
C#出力イメージは次のようになります。
'A' : 3'B' : 1'C' : 1
辞書は順序を保証しないので、出力順は変わることがありますが、
「文字と回数の対応」が取れていればOKです。
少しだけ洗練させる:TryGetValue を使う書き方
ContainsKey とインデクサを2回使う代わりに、TryGetValue を使うと、
辞書へのアクセス回数を少し減らせます。
public static IReadOnlyDictionary<char, int> CountChars(string? text)
{
var result = new Dictionary<char, int>();
if (string.IsNullOrEmpty(text))
{
return result;
}
foreach (char c in text)
{
if (result.TryGetValue(c, out int count))
{
result[c] = count + 1;
}
else
{
result[c] = 1;
}
}
return result;
}
C#やっていることは同じですが、
「キーがあるかどうかのチェック」と「値の取得」を1回で済ませているのがポイントです。
文字列が長くなってくると、こういう小さな工夫が積み重なって効いてきます。
LINQ を使った書き方との比較
GroupBy を使った“宣言的”な書き方
LINQを使うと、次のような書き方もできます。
using System.Linq;
public static IReadOnlyDictionary<char, int> CountCharsLinq(string? text)
{
if (string.IsNullOrEmpty(text))
{
return new Dictionary<char, int>();
}
return text
.GroupBy(c => c)
.ToDictionary(g => g.Key, g => g.Count());
}
C#ここでは、
GroupBy(c => c) で「同じ文字ごとにグループ化」ToDictionary で「キー=文字、値=グループの件数」という辞書に変換
という流れになっています。
コードは短くて読みやすいですが、
内部的にはイテレータやグループオブジェクトが生成されるので、
パフォーマンス的には手書きループより少し重くなります。
業務で普通の長さの文字列を扱う分には問題ありませんが、
「大量のテキストを何度も集計する」ような場面では、
手書きループ版のほうが有利になることもあります。
Unicode をどう考えるか:「char単位」でいいのか問題
C#の char は「UTF-16 の1単位」
C#の char は「UTF-16 の1コード単位」です。
多くの日本語や英数字は char 1つで1文字ですが、
絵文字や一部の漢字・記号は「サロゲートペア」として char 2つで1文字になることがあります。
文字頻度カウントを char 単位で行うと、
サロゲートペアは「2つの別々の文字」としてカウントされます。
例えば、絵文字 😊 が含まれる文字列をカウントすると、
内部的には「上位サロゲート」「下位サロゲート」が別々にカウントされる形になります。
多くの業務システムで「日本語+英数字中心」のテキストを扱うなら、char 単位のカウントで困ることはほとんどありません。
ただし、「絵文字を1文字として数えたい」「見た目の1文字単位で頻度を見たい」といった要件があるなら、
もう一段階踏み込んだ実装が必要になります。
見た目の1文字単位で数えたい場合の入口
厳密にやるなら、「テキスト要素(グラフェムクラスター)」単位で数える必要があります。
これは、前の回答でも触れた StringInfo を使うアプローチです。
イメージだけ示すと、次のようになります。
using System.Collections.Generic;
using System.Globalization;
public static IReadOnlyDictionary<string, int> CountTextElements(string? text)
{
var result = new Dictionary<string, int>();
if (string.IsNullOrEmpty(text))
{
return result;
}
var enumerator = StringInfo.GetTextElementEnumerator(text);
while (enumerator.MoveNext())
{
string element = (string)enumerator.Current!;
if (result.TryGetValue(element, out int count))
{
result[element] = count + 1;
}
else
{
result[element] = 1;
}
}
return result;
}
C#この場合、キーは char ではなく string(見た目の1文字)になります。
絵文字や結合文字も「1要素」としてカウントされるようになります。
業務ユーティリティとしてどうまとめるか
「用途」と「粒度」を決めておく
実務でユーティリティ化するなら、
まず「何を数えたいのか」をはっきりさせるのが大事です。
ログやID、コード、英数字中心のテキストの傾向を見たいなら、Dictionary<char,int> ベースの CountChars で十分です。
ユーザー名やメッセージに絵文字が多用されるサービスで、
「見た目の1文字単位で頻度を見たい」なら、CountTextElements のような“テキスト要素単位”のカウントを用意しておく価値があります。
例えば、こんなラッパーを用意しておくと、呼び出し側の意図が分かりやすくなります。
public static class TextAnalysis
{
public static IReadOnlyDictionary<char, int> CountCharsRaw(string? text)
=> CharFrequencyUtil.CountChars(text);
public static IReadOnlyDictionary<string, int> CountCharsVisual(string? text)
=> CountTextElements(text);
}
C#呼び出し側は、「生のchar単位でいいのか」「見た目の1文字単位がいいのか」を選んで呼び分けられます。
null や空文字の扱いをユーティリティ側で固定する
どの実装でも共通しているのが、
「null や空文字なら、空の辞書を返す」という方針です。
これをユーティリティ側で決めておくことで、
呼び出し側は「とりあえず結果を foreach すればいい」という書き方ができます。
毎回 null チェックを書く必要がなくなり、業務コードがすっきりします。
まとめ 「文字頻度カウントユーティリティ」は“テキストの中身を数字で眺めるためのルーペ”
文字頻度カウントは、単なる練習問題ではなく、
テキストのクセや傾向を数字で把握するための、実務でも使える道具です。
押さえておきたいポイントは、Dictionary<char,int> で「文字 → 回数」を持つのが基本形であること。
手書きループ版と LINQ版があり、パフォーマンスと読みやすさのバランスで選べること。
Unicode的には「1文字=1char」とは限らず、必要なら StringInfo で“見た目の1文字”単位に踏み込めること。
ユーティリティとしては、「nullは空の結果」「用途ごとに粒度を分ける」といったルールを決めておくと、呼び出し側が楽になること。
ここまで理解できれば、「なんとなく数えている」段階から一歩進んで、
“テキストの中身を意識的に分析できる文字列ユーティリティ”として、
文字頻度カウントを自分のC#コードに自然に組み込めるようになっていきます。
