LINQのSelect拡張メソッドを使用して、foreachループの中で現在が何回目の繰り返しなのか、そのインデックスを得る方法を紹介する(C# 7/VB 15対応)。
■更新履歴【2017/03/23】Visual Studio 2017の新機能を使う方法を追加しました。
【2017/02/22】初版公開。
本稿は2017/02/22に初版公開された記事を改訂し、C# 7.0/Visual Basic 15(以下、VB 15)の値タプルを利用する記述を追加したものです。
コレクションを処理するforeachループの中で繰り返し回数が必要になることがある。ループ外でカウンター変数を定義しておいてループ内でインクリメントすれば実現できるとはいえ、もっとスマートに書けないものだろうか?
LINQのSelect拡張メソッド(System.Linq名前空間のEnumerableクラス)を使うとスマートに書ける。
次のコードのようにSelect拡張メソッドを使って、コレクションの要素とインデックスを持つ匿名型のオブジェクトをループするごとに作ればよいのだ。
using System.Collections.Generic;
using System.Linq;
……省略……
IEnumerable<string> collection = ……省略……
foreach (var item in collection.Select((Value, Index) => new { Value, Index }))
{
string value = item.Value; // コレクションの要素
int index = item.Index; // ループのインデックス
// ……valueとindexを使った処理を書く……
}
Dim collection As IEnumerable(Of String) = ……省略……
For Each item In collection.Select(Function(Value, Index) New With {Value, Index})
Dim value As String = item.Value ' コレクションの要素
Dim index As Integer = item.Index ' ループのインデックス
' ……valueとindexを使った処理を書く……
Next
コンソールアプリの例を示そう(次のコード)。
このコードは、文字列のコレクションの各要素の先頭にインデックスを付けてコンソールに出力するものだ(Visual Studio 2015またはそれ以降)。
using System.Collections.Generic;
using System.Linq;
using static System.Console;
class Program
{
static void Main(string[] args)
{
IEnumerable<string> collection = new List<string>
{
"後漢の建寧元年のころ。",
"一人の旅人があった。",
"年の頃は二十四、五。",
};
foreach (var item
in collection.Select((Value, Index) => new { Value, Index }))
{
WriteLine($"{item.Index}:{item.Value}");
}
// 出力:
// 0:後漢の建寧元年のころ。
// 1:一人の旅人があった。
// 2:年の頃は二十四、五。
#if DEBUG
ReadKey();
#endif
}
}
Imports System.Console
Module Module1
Sub Main()
Dim collection As IEnumerable(Of String) = New List(Of String) _
From {
"後漢の建寧元年のころ。",
"一人の旅人があった。",
"年の頃は二十四、五。"
}
For Each item _
In collection.Select(Function(Value, Index) New With {Value, Index})
WriteLine($"{item.Index}:{item.Value}")
Next
' 出力:
' 0:後漢の建寧元年のころ。
' 1:一人の旅人があった。
' 2:年の頃は二十四、五。
#If DEBUG Then
ReadKey()
#End If
End Sub
End Module
参考までに、従来の書き方も示しておこう(次のコード)。
Select拡張メソッドを使ったコードより、2行多くなり、「カウンター変数を間違いなくカウントアップする」ことに注意する必要もある。ただし、実行速度はこちらの方が数倍ほど速いので、処理速度がシビアな場面ではまだ有用だ(なお、速いといっても、1万回のループをCore i7で回して1ミリ秒違うかどうかという程度である)。
IEnumerable<string> collection = ……省略……
int index = -1; // カウンター変数
foreach (var value in collection)
{
index++;
WriteLine($"{index}:{value}");
}
Dim collection As IEnumerable(Of String) = ……省略……
Dim index As Integer = -1 ' カウンター変数
For Each value In collection
index += 1
WriteLine($"{index}:{value}")
Next
Visual Studio 2017のC# 7.0/VB 15で導入された「値タプル」(value tuples)を使うと、特にC#ではとてもスマートに記述できる(VBでは従来とそれほど変わらない)。
本稿改訂時点で値タプルの機能を利用するには、NuGetから「System.ValueTuple」パッケージをプロジェクトごとに導入する必要がある(導入方法はVisual Studio 2015と同様)。なお、.NET Framework 4.7には既定で組み込まれるようだ(Windows 10 SDK Preview Build 15042をインストールしたところ、.NET Framework 4.7のプレビュー版が利用可能になり、それを選択することでパッケージを追加することなく値タプルが利用できた)。
値タプルを使うと、次のコードのように書ける。
IEnumerable<string> collection = ……省略……
// C# 7の新機能を使う
foreach (var (value, index) in collection.Select((v, i) => (v, i)))
WriteLine($"{index}:{value}");
Dim collection As IEnumerable(Of String) = ……省略……
' VB 15の新機能を使う
For Each t In collection.Select(Function(v, i) (Value:=v, Index:=i))
WriteLine($"{t.Index}:{t.Value}")
Next
上のコードのSelect拡張メソッドの中身は、何をやっているのかとっさには分からないだろう。拡張メソッドに切り出して名前を付けておくと、分かりやすくなる(次のコード)。
static class LinqExtensions
{
// コレクションの要素を、要素とインデックスのタプルに変換する拡張メソッド
public static IEnumerable<(T value, int index)>
ToTuples<T>(this IEnumerable<T> collection)
=> collection.Select((v, i) => (v, i));
}
……省略……
IEnumerable<string> collection = ……省略……
foreach (var (value, index) in collection.ToTuples())
WriteLine($"{index}:{value}");
Module LinqExtensions
' コレクションの要素を、要素とインデックスのタプルに変換する拡張メソッド
<System.Runtime.CompilerServices.Extension()>
Public Function ToTuples(Of T)(collection As IEnumerable(Of T)) _
As IEnumerable(Of (Value As T, Index As Integer))
Return collection.Select(Function(v, i) (v, i))
End Function
End Module
……省略……
Dim collection As IEnumerable(Of String) = ……省略……
For Each t In collection.ToTuples()
WriteLine($"{t.Index}:{t.Value}")
Next
Select拡張メソッドを使うと、foreachループの中で現在の繰り返し回数をスマートに得られる。カウンター変数の管理をしなくて済むというメリットがある。実行速度は従来の書き方よりも少し遅くなる。また、C# 7/VB 15では値タプルを利用することで、より簡潔な記述が可能になる。
利用可能バージョン:.NET Framework 3.5以降(サンプルコードにはそれ以降の構文も含む)
カテゴリ:クラス・ライブラリ 処理対象:コレクション
カテゴリ:クラス・ライブラリ 処理対象:LINQ
使用ライブラリ:Enumerableクラス(System.Linq名前空間)
関連TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]
関連TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?
関連TIPS:数値を右詰めや0埋めで文字列化するには?[C#、VB]
関連TIPS:Visual Studioでコンソール・アプリケーションのデバッグ実行時にコマンド・プロンプトを閉じないようにするには?
■更新履歴【2017/03/23】C# 7.0/VB 15の新機能を使う方法を追加しました。
【2017/02/22】初版公開。
Copyright© Digital Advantage Corp. All Rights Reserved.