C# 初級講座
VB2019 Visual Studio 2022

第23回 Dictionary

2023/12/28

この記事が対象とする製品・バージョン

Visual Studio 2022 Visual Studio 2022 対象です。
Visual Studio 2019 Visual Studio 2019 対象です。
Visual Studio 2017 Visual Studio 2017 対象外ですが、参考になります。
Visual Studio 2015 Visual Studio 2015 対象外ですが、参考になります。
Visual Studio 2013 Visual Studio 2013 対象外ですが、参考になります。
Visual Studio 2012 Visual Studio 2012 対象外ですが、参考になります。
Visual Studio 2010 Visual Studio 2010 対象外ですが、参考になります。
Visual Studio 2008 Visual Studio 2008 対象外ですが、参考になります。
Visual Studio 2005 Visual Studio 2005 対象外ですが、参考になります。
Visual Studio.NET 2003 Visual Studio.NET 2003 × 対象外です。
Visual Studio.NET 2002 Visual Studio.NET (2002) × 対象外です。
  Visual Studio Code 対象外ですが、参考になります。

 

目次

 

1.Dictionary

Dictionary (読み方:Dictionary=ディクショナリー)は Listの次によく使うコレクションです。

Dictionaryは、国語辞典のように見出しと値(内容)のペアを要素にし、このような要素を複数持ちします。見出しを使って個々の要素にアクセスできます。

この見出しのことをキーと呼びます。同じキーを複数追加することはできません。

次の画像の辞典を例にするとキーは「凄烈」で値が「すさまじく、はげしい様子。」です。

辞書の写真

■画像:Dictionaryのイメージ

Dictionaryに要素を追加するときには、キーと値の2つを指定します。

人間が使う辞典ではキーも値も文字列ですが、プログラムの世界なので、いろいろな型が使用でき、Listと同じく型パラメーターを使ってキーの型と値の型を指定します。

たとえば、キーが文字列型、値も文字列型のDictionaryは次のように作成します。

最初の型パラメーターがキーの型、2番目の型パラメーターが値の型です。


var myDic = new Dictionary<string, string>();

 

他にも自由にキーと値の型を指定できます。

//キーが int, 値が string の場合
var dic1 = new Dictionary<int, string>();

//キーが DateTime, 値が FileInfo の場合
var dic2 = new Dictionary<DateTime, System.IO.FileInfo>();

Dictionaryの要素の型は常にKeyValuePair型(読み方:キーバリューペア)です。型パラメーターは要素の型を指定しているのではなく、キーの型と値の型を指定していることになります。

 

2.Dictionary の使い方

2-1.コレクション初期化子

Dictionaryの宣言時に コレクション初期化子 を使って初期値(最初から保持している要素)を設定することができます。

しかし、Dictionaryの要素がはじめから決まっていることはほとんどなく、コレクション初期化子は見かけも複雑なので、サンプル以外ではあまり見ることがないようです。

Dictionaryのコレクション初期化子は、キーと値の両方を指定する必要があるので、Listのコレクション初期化子より少し複雑です。

例を示します。

var names = new Dictionary<string, string>
{
    {"A", "Apple" },
    {"B", "Banana" },
    {"C", "Cat" }
};

System.Diagnostics.Debug.WriteLine(names["A"]); // Apple と出力します。

Debug.WriteLineで出力される場所

1行で書くこともできるのですが、キーと値がごちゃごちゃしてわかりにくくなるので、ここでは改行しました。

この例を実行すると下記の3つの項目をもったDictionaryが作成されます。

  キー
要素1 A Apple
要素2 B Banana
要素3 C Cat

キーの型はString、値の型はString、要素の型はKeyValuePairです。

最後に表示しているDebug.WriteLineではキー「A」に該当する値として「Apple」が表示されます。このように値の取得方法はListとは異なります。

 

2-2.要素の追加

Dictionaryに要素を追加するにはAddメソッドを使用します。この点はListと同じで、他の多くのコレクションでも共通しています。

Dictionaryの場合は、要素を追加するときにキーと値の2つを指定する必要があるのでAddメソッドにこの2つを設定します。

次の例はDictionaryに要素を追加する例です。Addメソッドの第1引数がキーで、第2引数が値です。

var names = new Dictionary<string, string>();

names.Add("江戸幕府", "徳川家康");
names.Add("室町幕府", "足利尊氏");
names.Add("鎌倉幕府", "源頼朝");
names.Add("大和朝廷", "神武天皇");

System.Diagnostics.Debug.WriteLine(names["鎌倉幕府"]); // 源頼朝 と出力します。

Debug.WriteLineで出力される場所

 

この例ではDictionaryに4つの要素を追加しています。各要素ではキーに幕府の名前などを設定し、値にその開設者といわれている人物の名前を指定しています。

  キー
要素1 江戸幕府 徳川家康
要素2 室町幕府 足利尊氏
要素3 鎌倉幕府 源頼朝
要素4 大和朝廷 神武天皇

 

ちょっと特別な C# の文法で「インデクサー」という仕組みを使って次のように項目を追加することもできます。

var names = new Dictionary<string, string>();

names["江戸幕府"] = "徳川家康";

インデクサーとは [ ] を使った引数の指定で、クラスや構造体に何か機能を実行させる仕組みです。引数付きのプロパティのような存在です。Dictionaryから値を取得するときの Debug.WriteLine(names["鎌倉幕府"]) という記述もインデクサーを使って値を取得しています。

Dictionaryで、インデクサーを使って値を設定する場合は、[ ]内にキーを指定して = を使って値を読み書きできます。

こちらの構文の方がAddより少しだけ記述量が少なくなり、直感的であると好む人もいます。

 

インデクサーを使うと追加だけでなく、追加済みの値の変更を行うこともできます。Addメソッドの方は追加専用なので値の変更を行うことはできません。

var names = new Dictionary<string, string>();

names["大和朝廷"] = "神武天皇"; //新しい要素を追加します。
names["大和朝廷"] = "雄略天皇"; //既存の要素の値を変更します。

System.Diagnostics.Debug.WriteLine(names["大和朝廷"]); // 雄略天皇 と出力します。

この例では、はじめ「大和朝廷」のキーの値を「神武天皇」で登録していますが、そのすぐ次の行で「雄略天皇」に変更しています。結果として雄略天皇が表示されます。

Addメソッドでは同じキーを登録しようとするとArgumentException (読み方:オーギュメントエクセプション)の例外が発生します。さきほど説明した国語辞典のイメージで、同じ項目が2つあったらおかしいということです。

 

2-3.要素の削除

Dictionaryから要素を指定して削除するにはRemoveメソッド を使用します。引数には削除対象の要素のキーを指定します。

位置(インデックス)を指定して項目を削除するRemoveAtメソッドはDictionaryにはありません。Dictionaryはインデックスではなくキーで項目を識別しているため位置という考え方がないのです。

次の例はRemoveの使用例です。ポイントはRemoveの引数にインデックスの数値や、「足利尊氏」を指定するのではなく、キーである「室町幕府」を指定している点です。

var names = new Dictionary<string, string>();

names.Add("江戸幕府", "徳川家康");
names.Add("室町幕府", "足利尊氏");
names.Add("鎌倉幕府", "源頼朝");
names.Add("大和朝廷", "神武天皇");

//キー「室町幕府」を使って要素を削除
names.Remove("室町幕府");

//値の一覧表示。(徳川家康,源頼朝,神武天皇)
System.Diagnostics.Debug.WriteLine(string.Join(",", names.Values));

 

2-4.値の一覧とキーの一覧を取得

上記の例の最後の行ではDictionaryに含まれている値の一覧を表示しています。DictionaryのValuesプロパティ (読み方:Values=バリューズ)を使用すると値だけのコレクションを取得できますので、これを配列化してstring.Joinを使ってカンマをはさんで結合しています。Keysプロパティを使うとキーだけのコレクションを取得できます。

模式的に示すと次のような項目です。

各要素はキーと値のペアを持っている KeyValuePair型です。それとは別にKeysプロパティでキーだけのコレクションを取得できます。同様にValuesプロパティで値だけのコレクションを取得できます。

Keysプロパティの使用例を紹介しておきます。

var names = new Dictionary<string, string>();

names.Add("江戸幕府", "徳川家康");
names.Add("室町幕府", "足利尊氏");
names.Add("鎌倉幕府", "源頼朝");
names.Add("大和朝廷", "神武天皇");

//キーの一覧表示。(江戸幕府,室町幕府,鎌倉幕府,大和朝廷)
System.Diagnostics.Debug.WriteLine(string.Join(",", names.Keys));

 

2-5.値を1つ取得

既に説明したようにDictionaryから値を1つ取得するには、インデクサーにキーを指定します。

Dictionaryをあらわす変数名に直接角かっこでキーを指定するような書き方になります。

var names = new Dictionary<string, string>();

names.Add("江戸幕府", "徳川家康");
names.Add("室町幕府", "足利尊氏");
names.Add("鎌倉幕府", "源頼朝");
names.Add("大和朝廷", "神武天皇");

string edoStarter = names["江戸幕府"];

System.Diagnostics.Debug.WriteLine(edoStarter); // 徳川家康 と出力します。

 

 

2-6.キーの確認

Dictionaryは同じキーで項目を追加することはできません。

同じキーの要素を登録しようとするとArgumentExceptionの例外(≒エラー)(読み方:ArgumentException=オーギュメントエクセプション)になります。

var names = new Dictionary<string, string>();

names.Add("南朝", "劉裕");
names.Add("南朝", "後醍醐天皇"); //←ここでエラー

この例を実行した場合の例外メッセージは次の通りです。

System.ArgumentException: 'An item with the same key has already been added. Key: 南朝'

日本語に訳すと「同じキーを持つ要素が既に追加されています。キー:南朝」ということです。

 

このエラーを回避するには、ContainsKeyメソッド (読み方:ContainsKey=コンテインズキー)を使って要素を追加する前にキーが既に登録されているか調べるか、TryAddメソッド(読み方:TryAdd=トライアド)を使って、キーが登録されていなければ要素を追加するようにします。

ContainsKeyメソッドの例です。これで既にそのキーが含まれているか確認することができます。

var names = new Dictionary<string, string>();

names.Add("江戸幕府", "徳川家康");
names.Add("室町幕府", "足利尊氏");
names.Add("鎌倉幕府", "源頼朝");
names.Add("大和朝廷", "神武天皇");

if (names.ContainsKey("鎌倉幕府")) {
    System.Diagnostics.Debug.WriteLine("鎌倉幕府は既にnamesに登録されています。"); // ←これが出力されます。
}

 

「登録されていなければ新規登録したい、登録されていれば上書きしたい」という場合はインデクサーを使ってプログラムするのが楽です。

次の例は南朝がキーとして登録されていなければ項目を追加し、登録されていても上書きします。

var names = new Dictionary<string, string>();

names.Add("江戸幕府", "徳川家康");
names.Add("室町幕府", "足利尊氏");
names.Add("鎌倉幕府", "源頼朝");
names.Add("大和朝廷", "神武天皇");
//names.Add("南朝", "劉裕"); //←この行があってもなくても最終的な結果は同じ

names["南朝"] = "後醍醐天皇";

//徳川家康,足利尊氏,源頼朝,神武天皇,後醍醐天皇
System.Diagnostics.Debug.WriteLine(string.Join(",", names.Values));

 

登録されていれば何もしないというなら TryAddの出番です。TryAdd はキーが存在しない場合、Addメソッドと同じ機能です。キーが存在する場合は何もしません。要素の追加が成功したかどうかは戻り値わかります。戻り値が true の場合、要素の追加に成功しています。

var names = new Dictionary<string, string>();

names.Add("江戸幕府", "徳川家康");
names.Add("室町幕府", "足利尊氏");
names.Add("鎌倉幕府", "源頼朝");
names.Add("大和朝廷", "神武天皇");
names.Add("南朝", "劉裕"); 

if (names.TryAdd("南朝", "後醍醐天皇"))
{
    System.Diagnostics.Debug.WriteLine("南朝をキーに後醍醐天皇を登録しました。");
}
else
{
    //これが出力されます。
    System.Diagnostics.Debug.WriteLine("南朝のキーは既に存在しているため、に後醍醐天皇は登録できませんでした。");
}

 

ちょっとまとめておきます。

  キーが存在しない場合 キーが存在する場合
dic.Add(キー、値) 要素を追加します。 ArgumentExceptionの例外が発生します。
dic.TryAdd(キー、値) 要素を追加して、trueを返します。 何も変更せず、falseを返します。
dic[キー] = 値 要素を追加します。 要素を上書きします。

 

 

3.foreach での Dictionary の列挙

3-1.foreach

Dictionaryもコレクションなので foreach で要素を列挙できます。

foreach で取り出される要素の順番は保証されておらず追加した順番に取り出されるとは限りません。ちょっと試してみるとたいていは登録した順番でforeachも処理されるので、なんとなく登録順通りやってくれるものだと思ってしまうかもしれませんが、実際の仕様はそうではないので、何かの時に順番通り処理されないかもしれません。気を付けてください。

Dictionaryの要素は常に KeyValuePair<T1, T2>型です。Dictionary<string, string> の要素は KeyValuePair<string, string>です。Dictionary<long, int> の要素は KeyValuePair<long, int>です。KeyValuePairからキーと値を取り出すにはKeyプロパティとValueプロパティを使用します。そろそろ実例を見てみましょう。

この例では太陽系のそれぞれの惑星の名前をキーにして、衛星の数を値とするDictionaryを使用します。衛星は次々と新しいものが発見されるので、このサンプルの情報はすぐに古くなるかもしれません。

//惑星とその衛星の数。
var moons = new Dictionary<string, int>
{
    {"水星",0 },
    {"金星",0 },
    {"地球",1 }, //地球は月だけ。
    {"火星",2 }, //火星はフォボスとダイモス。
    {"木星",79 },//イオ、エウロパ、ガニメデ、カリストを筆頭に多数
    {"土星",82 },//タイタンなど多数
    {"天王星",27 },
    {"海王星", 14} //トリトンなど
};

//型推論により moon は KeyValuePair<string, int>型になります。
foreach (var moon in moons)
{
    string planetName = moon.Key;
    int moonCount = moon.Value;

    System.Diagnostics.Debug.WriteLine($"{planetName}の衛星の数は{moonCount}個");
}

Debug.WriteLineで出力される場所

moon は KeyValuePair<string, int>型です。この場合、そのKeyプロパティはstring型、Valueプロパティはint型になります。

このプログラムではそれぞれを変数に代入し、要素ごとにメッセージを表示します。

最終的にはたとえば次のように出力されます。

水星の衛星の数は0個
金星の衛星の数は0個
地球の衛星の数は1個
火星の衛星の数は2個
木星の衛星の数は79個
土星の衛星の数は82個
天王星の衛星の数は27個
海王星の衛星の数は14個

 

もし、Valueに興味はなく、Keyだけ列挙したいのであれば、DictionaryのKeyプロパティに対して列挙を記述します。

//惑星とその衛星の数。
var moons = new Dictionary<string, int>
{
    {"水星",0 },
    {"金星",0 },
    {"地球",1 }, //地球は月だけ。
    {"火星",2 }, //火星はフォボスとダイモス。
    {"木星",79 },//イオ、エウロパ、ガニメデ、カリストを筆頭に多数
    {"土星",82 },//タイタンなど多数
    {"天王星",27 },
    {"海王星", 14} //トリトンなど
};

//型推論により planet は string 型になります。
foreach (var planet in moons.Keys)
{
    System.Diagnostics.Debug.WriteLine(planet);
}

 

逆に Value だけ欲しいのであれば Valuesプロパティに対して列挙を記述します。

次のプログラムはすべての要素の値を合計して、太陽系の衛星の総数を算出します。

//惑星とその衛星の数。
var moons = new Dictionary<string, int>
{
    {"水星",0 },
    {"金星",0 },
    {"地球",1 }, //地球は月だけ。
    {"火星",2 }, //火星はフォボスとダイモス。
    {"木星",79 },//イオ、エウロパ、ガニメデ、カリストを筆頭に多数
    {"土星",82 },//タイタンなど多数
    {"天王星",27 },
    {"海王星", 14} //トリトンなど
};

int totalCount = 0;

//型推論させてもよいのですが、型を明示して int と記述してみました。
foreach (int count in moons.Values)
{
    totalCount += count;
}

System.Diagnostics.Debug.WriteLine($"太陽系の衛星の総数:{totalCount}");

 

foreachの構文は、ListであれDictionaryであれ、他の何かであれ共通です。ですから、必要であれば break と continue を使うこともできます。

 

3-2.要素の分解

ところで、上記の例では KeyValuePair<T1, T2> から値を取り出すときに、KeyプロパティとValueプロパティを使っていました。もう1度その部分だけを抜粋してみましょう。

//型推論により moon は KeyValuePair<string, int>型になります。
foreach (var moon in moons)
{
    string planetName = moon.Key;
    int moonCount = moon.Value;

    System.Diagnostics.Debug.WriteLine($"{planetName}の衛星の数は{moonCount}個");
}

変数 moon から、Keyプロパティで取り出した値を planetName に代入し、valueプロパティで取り出した値を moonCountプロパティに代入しています。

つまり、moon をキーと値に分解する処理を自分で書いているわけです。

 

Dictionaryの要素の分解は自動的に行うこともできます。それには次のように書き換えます。

//型推論により planetName は string, moonCount は intになります。
foreach (var (planetName, moonCount) in moons)
{
    System.Diagnostics.Debug.WriteLine($"{planetName}の衛星の数は{moonCount}個");
}

いきなりこの例を紹介すると少しわかりにくいかなと思って後だししました。この書き方の方が記述量が少なく断然楽なのでお勧めです。

 

なお、次のようKeyValuePair型の変数を使って、1行でキーと値を分解することもできます。

//型推論により moon は KeyValuePair<string, int>型になります。
foreach (var moon in moons)
{
    var (planetName, moonCount) = moon; //← 1行でキーと値を取り出せます。

    System.Diagnostics.Debug.WriteLine($"{planetName}の衛星の数は{moonCount}個");
}

このような値の分解は、 KeyValuePair以外でも複数の値を保持するものに対して使用できる場合があります。まだ初級講座第26回で説明する「タプル」がその代表です。

 

 

4.List との変換

Dictionaryでも ToList メソッドが使用できて、Dictionary を List に変換できます。

Dictionary<T1, T2> の要素の型は KeyValuePair<T1, T2> なので、List に変換すると、KeyValuePair<T1, T2> の List になります。

 

ForEach メソッドは List にはありますが、Dictionary には存在しません。このようなListの機能を使いたい場合は ToList で変換します。(※ foreach の構文のことではなく、メソッドとしての ForEach を指しています。)

ForEach メソッドを使って全要素のキーと値を出力する例を紹介しておきます。

//惑星とその衛星の数。
var moons = new Dictionary<string, int>
{
    {"水星",0 },
    {"金星",0 },
    {"地球",1 }, //地球は月だけ。
    {"火星",2 }, //火星はフォボスとダイモス。
    {"木星",79 },//イオ、エウロパ、ガニメデ、カリストを筆頭に多数
    {"土星",82 },//タイタンなど多数
    {"天王星",27 },
    {"海王星", 14} //トリトンなど
};

moons.ToList().ForEach(item => System.Diagnostics.Debug.WriteLine($"{item.Key}の衛星の数は{item.Value}個"));

前回 List の列挙でも説明しましたが、ForEach メソッドを使うと、各要素にそれぞれに対して指定した処理を実行することができます。

実行する「処理」はForEachメソッドの引数で指定します。ところが、通常の int や string などの型は「値」を表現するものであり、「処理」を表現することができません。そこで、C#では「処理」を表現するために「ラムダ式」という特別な機能があります。ラムダ式は次回説明する予定です。 => がラムダ式の目印です。

この例の場合、それぞれの要素を item という変数で表現するようにしています。だから、item は KeyValuePair型です。このitem に対して実行する処理として => の右側にDebug.WriteLine メソッドを記述しています。

 

5.演習 拡張子の統計

5-1.フォルダー選択

Dictionaryを使って、拡張子ごとのファイル数を数えるプログラムを作ってみましょう。

下記の設定で新規プロジェクトを作成してください。

プロジェクトテンプレート Windows フォーム アプリ
プロジェクト名 ExtensionCounter
ソリューション名 ExtensionCounter
フレームワーク .NET 8.0

 

 

フォームにはボタン(Button)とリストボックス(ListBox)を貼り付けて、プロパティは下記の通りにしてください。

 

コントロールの名前 種類 プロパティ/イベント 備考
イベント Form1 Form イベント Text 拡張子の統計  
イベント btnCount Button イベント (Name) btnCount  
    イベント Text フォルダー選択  
    イベント Click ハンドラーを生成してください。 ハンドラーを生成する方法
イベント lstResult ListBox イベント (Name) lstResult  
    イベント Font メイリオ、サイズ 14  
    イベント Anchor Top,Bottom,Left,Right フォームの大きさに応じてListBoxの大きさも変わるようになります。

btnCount の Clickイベントハンドラーを生成する最も簡単な方法はデザイン画面でそのボタンをダブルクリックすることです。

 

配置やその他のデザインはお好みでアレンジしてください。私は次のように配置しました。

 

ボタンをクリックしたら、ファイルが格納されているフォルダーをユーザーに選択させます。次のようなプログラムになります。

private void btnCount_Click(object sender, EventArgs e)
{
    using var dialog = new FolderBrowserDialog();
    dialog.Description = "拡張子を数えるフォルダーを選択してください。";

    //ここでダイアログが表示されます。ユーザーが選択を完了するまでプログラムはこれ以上進みません。
    DialogResult result = dialog.ShowDialog(); 

    if (result == DialogResult.Cancel)
    {
        //ユーザーがキャンセルを選択した場合何もしません。
        return;
    }

    MessageBox.Show(dialog.SelectedPath, "選択されたフォルダーのパス");
}

FolderBrowserDialogクラス(読み方:FolderBrowserDialog = フォルダーブラウザーダイアログ)はフォルダーを選択させるときにとても便利です。子画面を表示してユーザーにフォルダーを選択させる機能です。

このクラスは使い終わったらリソースを開放する必要があるので、宣言時に using を指定します。こうしておくことで自動的にリソースを開放してくれます。リソースを開放する必要があるクラスは必ず Disposeメソッドを持っているので、これで見分けられるというのは以前説明しています。

FolderBrowserDialogで、フォルダー選択の子画面を表示するには ShowDialogメソッド(読み方:ShowDialog=ショウダイアログ)を使います。表示される子画面はWindowsのバージョンによって違いますが、たとえば、私のWindows 10 では次のような子画面が出てきます。

このようなユーザーからの入力を受け取るための子画面をダイアログ(Dialog)と呼びます。英語で Dialog とは2人でする対話という意味です。英語では dialogue とも書きます。

このダイアログには「フォルダーの選択」や「キャンセル」というボタンがあります。面白いことにユーザーが何かボタンをクリックするか、ダイアログを閉じるまで元のフォームを操作することができません。余談ですがこのように閉じるまで親画面を操作できないウィンドウをモーダルウィンドウと呼びます。

ユーザーがどのボタンをクリックしたのかは、ShowDialogメソッドの戻り値でわかります。この戻り値はボタンの種類を表す DialogResult 型です(読み方:DialogResult=ダイアログリザルト)。

この型は列挙体で、キャンセルボタンをクリックした場合、DialogResult.Cancel という値になります。

そこで、上記のサンプルのような if 文でキャンセルされたか判断できます。ダイアログの右上の×ボタンをクリックした場合もキャンセルボタンと同じです。この例ではキャンセルされた場合 returnを実行します。returnを実行するとメソッドの処理はそこで終わり、メソッド内の後続の処理は実行されないのでした。要するにこの例ではプログラムはこれ以上実行されません。ここでいうメソッドとは、btnCount_Clickメソッド のことです。

ユーザーが選択したフォルダーのパスは SelectedPathプロパティ (読み方:SelectedPath = セレクテッドパス)で取得できます。

 

5-2.ファイル一覧の取得と表示

拡張子の集計をする前に、ちょっと寄り道して肩慣らしします。

選択したフォルダ内のファイル一覧を取得してみます。繰り返しになるので前に示した部分のプログラムの大部分は省略します。最後にプログラム全体を載せます。

private void btnCount_Click(object sender, EventArgs e)
{

    …省略…

    MessageBox.Show(dialog.SelectedPath, "選択されたフォルダーのパス");

    //ファイル名と拡張子の一覧を表示
    foreach(string fileFullPath in System.IO.Directory.EnumerateFiles(dialog.SelectedPath))
    {
        string fileName = System.IO.Path.GetFileName(fileFullPath); //ファイル名を取得
        string extension = System.IO.Path.GetExtension(fileFullPath); //拡張子を取得
        lstResult.Items.Add($"{fileName} - {extension}");
    }

}

まだ Dictionary は出てきません。この段階では単純にファイルと拡張子を並べて表示するだけです。

DirectoryクラスのEnumerateFilesメソッド(読み方:EnumerateFiles=イニューマレイトファイルズ)は、フォルダー内のファイルの一覧を簡単に取得できる便利なメソッドです。戻り値は文字列型の配列で、フォルダー内のファイルのフルパスが格納されています。

とりあえず、ここでは確認のためこれをリストボックスに表示するプログラムにしています。2回目に実行してもおかしくならないよう、Items.Clearを使って表示前にリストボックスの内容をクリアします。

配列の内容は foreach のループで取り出します。ループ内でフルパスからファイル名と拡張子を取得して、Items.Add でリストボックスに追加します。

フルパスを分解して情報を取り出すには、Pathクラス(読み方:Path=パス)が便利で、このサンプルのようにファイル名・拡張子はそれぞれ一撃で取得できます。

この状態で実行すると次のように表示されます。

 

5-3.拡張子の統計

さて、単純にファイル名と拡張子を表示するのではなく、拡張子ごとのファイル数を表示するにはこれをどう改造したらよいでしょうか?

できあがりからイメージすると、次のように表示したいわけです。

この画面を見るとイメージが湧いてくるかもしれません。このデータの構造は、拡張子をキー、カウントを値とする辞書、つまりDictionaryで表現できるのです。

ですから、プログラム内で情報を格納するとために Dictionary<string, int> 型の変数を宣言して、ファイルの拡張子を調べて集計していく形になります。

はじめての人には難しいのですぐにこの下の正解を見るのが良いでしょう。他の言語で似たようなプログラムを作ったことのある経験者なら少し自分でチャレンジしてみても良いかもしれません。

 

正解は次のようになります。

拡張子とカウントを格納する辞書は var extensions = new Dictionary<string, int>(); と宣言しています。

private void btnCount_Click(object sender, EventArgs e)
{
    using var dialog = new FolderBrowserDialog();
    dialog.Description = "拡張子を数えるフォルダーを選択してください。";

    //ここでダイアログが表示されます。ユーザーが選択を完了するまでプログラムはこれ以上進みません。
    DialogResult result = dialog.ShowDialog(); 

    if (result == DialogResult.Cancel)
    {
        //ユーザーがキャンセルを選択した場合何もしません。
        return;
    }

    MessageBox.Show(dialog.SelectedPath, "選択されたフォルダーのパス");

    //ListBoxの内容をすべてクリアします。
    lstResult.Items.Clear();

    //拡張子(文字列)をキーに、カウント(数値)を格納するDictionary
    var extensions = new Dictionary<string, int>();

    //拡張子を集計
    foreach (string fileFullPath in System.IO.Directory.EnumerateFiles(dialog.SelectedPath))
    {
        string extension = System.IO.Path.GetExtension(fileFullPath); //拡張子を取得

        //この拡張子の 値1 での Dictionary への登録を試みます。
        if (!extensions.TryAdd(extension, 1))
        {
            //もし、登録に失敗したら、それはこの拡張子が既に登録されているということです。
            //その場合は、単純に値を +1 します。
            extensions[extension] += 1;
        }
    }

    //拡張子とカウントを表示
    foreach (var summary in extensions)
    {
        string extension = summary.Key;
        int count = summary.Value;
        lstResult.Items.Add($"{extension} - {count} ファイル");
    }
}

最大のポイントはコメントで「拡張子を集計」と書いてあるところの foreach です。

ここでファイルのパスを1個ずつ調べて、拡張子を取り出し、TryAddメソッドを使ってその拡張子がまだextensionsにキーとして登録されていなければ登録します。その時、その拡張子の1つ目のファイルなので値は 1 で登録します。

既にこの拡張子がキーとして登録されている場合は、TryAdd が false を返すので if の中が実行されます。この if の条件には ! がついているので、false が反転して true になる点に注意してください。

if の中ではインデクサーを使ってその拡張子をキーとする要素の値を +1 します。

このようにすることで拡張子とカウントを格納した辞書(Dictionary)が完成するので、最後にもう1度 foreach でループを行ってこれをリストボックスに表示します。

ところで、この最後の foreach の変数 summary が何型になるかわかりますか? KeyValuePair<string, int>型です。既に説明したように Dictionary<T1, T2> の要素はすべて KeyValuePair<T1, T2>型です。

 

5-4.LINQ

以上で説明したプログラムは、はじめてDictionaryを使ってプログラムする説明用にわかりやすさを優先して少し冗長に書いています。

逆に積極的に行数を少なくしてみた例を紹介します。この例では Dictionary は登場しません。

private void btnCount_Click(object sender, EventArgs e)
{
    using FolderBrowserDialog dialog = new (){ Description = "拡張子を数えるフォルダーを選択してください。" };

    //ここでダイアログが表示されます。ユーザーが選択を完了するまでプログラムはこれ以上進みません。
    if (dialog.ShowDialog() == DialogResult.OK)
    {
        MessageBox.Show(dialog.SelectedPath, "選択されたフォルダーのパス");

        //ListBoxの内容をすべてクリアします。
        lstResult.Items.Clear();

        //拡張子を集計
        System.IO.Directory.EnumerateFiles(dialog.SelectedPath)
            .GroupBy(fileFullPath => System.IO.Path.GetExtension(fileFullPath))
            .Select(files => (extension : files.Key, count : files.Count()))
            .ToList()
            .ForEach(summary => lstResult.Items.Add($"{summary.extension} - {summary.count} ファイル"));
    }
}

これは処理自体を式で表す ラムダ式 を積極活用して、ラムダ式を引数にとるメソッド群を次々と呼び出すことで処理を実現しています。このような記述方法については初級講座の後の方で説明していくことになると思います。

この技を「LINQ」(リンク)と呼びます。LINQを使うと配列やコレクションをさまざまに集計したり、操作することができます。LINQはDictionaryなどのコレクションととても相性がよく、便利に使えるようにデザインされているので、いろいろなデータを集計・操作する必要がある場合は、参考にしてみてください。

参考 統合言語クエリ (LINQ) (C#) | Microsoft Learn

とはいえ、この短いプログラムがわかりやすいかといえば、どうでしょうか。人によって評価は変わると思いますが、私は最初に紹介したDictionaryやforeachが出てくる少し長いプログラムの方が、何をやっているかわかりやすいように思うので好みです。

 

 

6.練習問題

問1.次のプログラムで □ に当てはまるのは何でしょうか?
var store = new □<long, long>();

store.Add(1, 9999);
List
Dictionary
long[]

 

問2.変数 values で表される Dictionary の要素の型は何でしょうか?

var values = new Dictionary<string, int>();

string
int
KeyValuePair<string, int>

 

問3.次の変数 value は何型でしょうか?
var populations = new Dictionary<string, int>();

//国別人口。単位:千人
populations.Add("ベトナム", 96462);
populations.Add("ラオス", 7169);
populations.Add("ミャンマー", 54045);
populations.Add("カンボジア", 16486);
populations.Add("タイ", 69625);
populations.Add("マレーシア", 31949);
populations.Add("シンガポール", 5804);

var value = populations["ベトナム"];

System.Diagnostics.Debug.WriteLine($"ベトナムの人口は約{value}000人");
string
int
KeyValuePair<string, int>

 

次の回は、while と do を扱います。これで、C#で繰り返し実行の構文は全部です。

C#初級講座講座 次の回へ