連載
» 2018年02月21日 05時00分 公開

.NET TIPS:Dictionaryのキー/値をforeachで簡単に扱うには?[C#/VB]

キー/値をまとめて保持するDictionary<T, T>クラスをforeachループで扱う際の基本と注意すべき点、タプルを使ったより簡便な記述の仕方を紹介する。

[山本康彦,BluewaterSoft/Microsoft MVP for Windows Development]
「.NET TIPS」のインデックス

連載「.NET TIPS」

 Dictionary<T, T>コレクション(System.Collections.Generic名前空間)は、キー(key)と値(value)のペアを保持しているコレクションであり、ハッシュテーブルや連想配列と呼ばれることもある。本稿では、Dictionary<T, T>コレクションに格納されているキー/値をforeachループ(C#)/For Eachループ(VB、以降はC#に合わせて「foreachループ」とだけ記述する)で扱う方法を解説する。

POINT Dictionaryのキー/値をforeachで扱う方法

Dictionaryのキー/値をforeachで扱う方法まとめ Dictionaryのキー/値をforeachで扱う方法まとめ
変数dicは、ここでは整数のキーと文字列型の値を格納しているDictionary<int, string>型のコレクションとしている。


 特定のトピックをすぐに知りたいという方は以下のリンクを活用してほしい。

 なお、本稿に掲載したサンプルコードをそのまま試すにはVisual Studio 2017以降が必要である。サンプルコードはコンソールアプリの一部であり、コードの冒頭に以下の宣言が必要となる。

using System.Collections.Generic;
using System.Linq;
using static System.Console;

Imports System.Console

本稿のサンプルコードに必要な宣言(上:C#、下:VB)

Dictionaryの基本的な使い方

 Addメソッドインデクサを使ってデータを追加し、TryGetValueメソッドか(ContainsKeyメソッドを使うなどしてキーの存在が確実に分かっているならば)インデクサを使ってデータを取得するのが基本的な使い方だ。

 詳しくは次の.NET TIPSをご覧いただきたい。

KeyValuePairで受け取る

 foreachループでキーと値を取り出して使うだけのときは、KeyValuePair<T, T>構造体(System.Collections.Generic名前空間)をループ変数にするとよい(次のコード)。キーと値をまとめて受け取れる。

var dic = new Dictionary<int, string> {
                { 1, "Eureka"}, { 2, "Nadia"}, { 3, "002"},
              };
WriteLine($"データ数:{dic.Count}");
// 出力:データ数:3
WriteLine(string.Join(", ", dic.Select(kvp => $"{kvp.Key}:{kvp.Value}")));
// 出力:1:Eureka, 2:Nadia, 3:002

// ループ変数にKeyValuePairを使う
foreach (KeyValuePair<int, string> kvp in dic)
{
  int id = kvp.Key;
  string name = kvp.Value;
  WriteLine($"{id}:{name}");
}
// 出力:
// 1:Eureka
// 2:Nadia
// 3:002

Dim dic = New Dictionary(Of Integer, String) From {
                {1, "Eureka"}, {2, "Nadia"}, {3, "002"}
              }
WriteLine($"データ数:{dic.Count}")
' 出力:データ数:3
WriteLine(String.Join(", ", dic.Select(Function(kvp) $"{kvp.Key}:{kvp.Value}")))
' 出力:1:Eureka, 2:Nadia, 3:002

' ループ変数にKeyValuePairを使う
For Each kvp As KeyValuePair(Of Integer, String) In dic
  Dim id As Integer = kvp.Key
  Dim name As String = kvp.Value
  WriteLine($"{id}:{name}")
Next
' 出力:
' 1:Eureka
' 2:Nadia
' 3:002

KeyValuePairで受け取るforeachループの例(上:C#、下:VB)
ループ内でキー/値を取り出して使うだけなら、このようにループ変数としてKeyValuePair<T, T>構造体を使うと効率がよい。
なお、ここではサンプルということでループ変数kvpの型を明示しているが、通常は省略して「foreach (var kvp in dic)」(C#)/「For Each kvp In dic」(VB)のように書くことが多い。正しくKeyValuePair<T, T>型であるとコンパイラが推論してくれる。

 次のコードのような書き方を見かけることもあるが、これはループごとにキーを使った検索が発生するので速度的に不利である。

foreach (int id in dic.Keys)
{
  string name = dic[id]; // ←ハッシュテーブルの検索を行っている
  WriteLine($"{id}:{name}");
}

For Each id As Integer In dic.Keys
  Dim name As String = dic(id) ' ←ハッシュテーブルの検索を行っている
  WriteLine($"{id}:{name}")
Next

無駄な検索が発生するのでよくないforeachループの例(上:C#、下:VB)
変数dicの生成と初期データの投入コードは、前のコードと同じである。

 ただし、ループ内で更新や削除をする場合は、上のようなループの書き方では例外が発生してしまう(どちらの書き方でも)。更新/削除には、あらかじめ作っておいたキーのコレクションを使ってforeachループを回す(次のコード)。

// Dictionaryの内容を表示
WriteLine(string.Join(", ", dic.Select(kvp => $"{kvp.Key}:{kvp.Value}")));
// 出力:1:Eureka, 2:Nadia, 3:002

// キーを取り出して別のコレクションにする
List<int> idList = dic.Keys.ToList();

// あらかじめ作っておいたキーのコレクションでforeachループを回す
foreach (int id in idList)
{
  string name = dic[id];
  if (name.StartsWith("N"))
    dic.Remove(id); // データを削除
  else
    dic[id] = $"{name}[{id}]"; // 値を更新
}
WriteLine(string.Join(", ", dic.Select(kvp => $"{kvp.Key}:{kvp.Value}")));
// 出力:1:Eureka[1], 3:002[3]

' Dictionaryの内容を表示
WriteLine(String.Join(", ", dic.Select(Function(kvp) $"{kvp.Key}:{kvp.Value}")))
' 出力:1:Eureka, 2:Nadia, 3:002

' キーを取り出して別のコレクションにする
Dim idList As List(Of Integer) = dic.Keys.ToList() 

' あらかじめ作っておいたキーのコレクションでforeachループを回す
For Each id As Integer In idList
  Dim name As String = dic(id)
  If (name.StartsWith("N")) Then
    dic.Remove(id) ' データを削除
  Else
    dic(id) = $"{name}[{id}]" ' 値を更新
  End If
Next
WriteLine(String.Join(", ", dic.Select(Function(kvp) $"{kvp.Key}:{kvp.Value}")))
' 出力:1:Eureka[1], 3:002[3]

foreachループ内で更新/削除する例(上:C#、下:VB)
変数dicの生成と初期データの投入コードは、冒頭のコードと同じである。
キーを取り出して別のコレクションに固定してしまえば、foreachループの中でデータを変更してもそのコレクションには影響が及ばないので、このようにうまく更新/削除ができる。ただし、別のコレクションとしては、List<T>コレクションか配列を使う(IEnumerable<T>コレクションでは元のKeysコレクションと連動したままなので、やはり例外が発生してしまう)。
なお、速度を優先するために、冒頭のコードのようなKeyValuePair<T, T>構造体を使ったループで対象となるデータのキーだけを別のコレクションに取り出しておいて、後からそのコレクションを使ったループを回して実際の更新/削除を行うという方法もある。

タプルでも受け取れる.NET Core 2.0(C#のみ)

 KeyValuePair<T, T>構造体で受け取り、そこからいちいちキーと値を取り出すのは面倒だ。もしもタプルで受け取れるとしたら、キーと値を取り出す手間もないし、foreachステートメントの中でキーと値を受け取る変数も明示できる。実は、.NET Core 2.0ではそのような改良が施されている(次のコード)。

var dic = new Dictionary<int, string> {
                { 1, "Eureka"}, { 2, "Nadia"}, { 3, "002"},
              };

foreach (var (id, name) in dic)
{
  WriteLine($"{id}:{name}");
}
// 出力:
// 1:Eureka
// 2:Nadia
// 3:002

.NET Core 2.0ではタプルで受け取れる(C#)
このタプルを使った構文はVisual Studio 2017(C# 7)で導入されたものである。VBでは、このような書き方はまだサポートされていない。詳細は.NET TIPS「複数のオブジェクトを一時的に1つにまとめるには?[4以降、C#、VB]」をご覧いただきたい。
なお、Visual Studio 2017で.NET Core 2.0のコンソールアプリを開発するには、「.NET Core クロスプラットフォームの開発」ワークロードをインストールしておく。
ちなみに.NET Core 2.0のAPIは、Windows 10 バージョン1709(build 16299)以降を対象にしたUWPアプリでも利用できる(Microsoft.NETCore.UniversalWindowsPlatform 6.0以降)。Visual Studio 2017でターゲットの最小ビルドを16299(またはそれ以降)にしてUWPアプリのプロジェクトを作るだけで、このタプルを使った構文が使える。

.NET Frameworkでもタプルで受け取れるようにするには?(C#のみ)

 残念ながら、.NET Framework(少なくとも4.7まで)では、上記の.NET Core 2.0のようにタプルで受け取ることはできない。

 .NET Core 2.0のKeyValuePair<T, T>構造体には、キー/値をタプルの分解した要素として返すDeconstructメソッドが実装されている。それで、前述のようにforeachステートメント内でタプルとして受け取れるのだ。.NET Frameworkにはそのような実装がないのであるが、拡張メソッドを使って実装を追加してしまえばよい(次のコード)。

// KeyValuePair<T, T>構造体にDeconstructメソッドを付加する拡張メソッド
public static class KeyValuePairExtension
{
  public static void Deconstruct<TKey, TValue>(this KeyValuePair<TKey, TValue> kvp,
                                               out TKey key, out TValue value)
  {
    key = kvp.Key;
    value = kvp.Value;
  }
}

……省略……

// 上で定義した拡張メソッドを使うforeachループ
foreach (var (id, name) in dic)
{
  WriteLine($"{id}:{name}");
}

拡張メソッドを作って.NET Frameworkでもタプルで受け取る(上:C#)
このタプルを使った構文はVisual Studio 2017(C# 7)で導入されたものである。VBでは、このような書き方はまだサポートされていない。

まとめ

 Dictionaryのキー/値をforeachループ内で使うには、ループ変数をKeyValuePairにするのが基本となる。C#の場合は、Visual Studio 2017以降なら拡張メソッドを作ることでタプルで受け取れるようになる(.NET Core 2.0以降では標準で実装されている)。

利用可能バージョン:.NET Framework 2.0以降(一部、.NET Core 2.0以降)
カテゴリ:クラス・ライブラリ 処理対象:コレクション
使用ライブラリ:Dictionaryクラス(System.Collections.Generic名前空間)
使用ライブラリ:KeyValuePair構造体(System.Collections.Generic名前空間)
関連TIPS:複数のオブジェクトを一時的に1つにまとめるには?[4以降、C#、VB]
関連TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]
関連TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?
関連TIPS:数値を右詰めや0埋めで文字列化するには?[C#、VB]


「.NET TIPS」のインデックス

.NET TIPS

Copyright© 1999-2018 Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。