foreachループで現在の繰り返し回数を使うには?[C#/VB]:.NET TIPS
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ループの中で繰り返し回数が必要になることがある。ループ外でカウンター変数を定義しておいてループ内でインクリメントすれば実現できるとはいえ、もっとスマートに書けないものだろうか?
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
Select拡張メソッドに与えるラムダ式には、このように2つの入力パラメーターを持つものもある。最初の入力パラメーター(この例では「Value」)はコレクションの各要素であり、2つ目の入力パラメーター(この例では「Index」)はコレクションのインデックス(ゼロ始まり)である。この2つの入力パラメーターを、そのまま匿名型のオブジェクトに格納してやり、それをループ変数(この例では「item」)としている。ループ内では、ループ変数のプロパティとして、コレクションの要素とインデックスを得られる。
なお、ここでは説明のためにループ内で一時変数(「value」と「index」)にコレクションの要素とインデックスを代入している。簡単な処理ならば、一時変数に代入せずそのまま(「item.Value」や「item.Index」を)使えばよい。
実際の例
コンソールアプリの例を示そう(次のコード)。
このコードは、文字列のコレクションの各要素の先頭にインデックスを付けてコンソールに出力するものだ(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
C#コードの冒頭から3行目にある「using static System.Console;」という書き方は、Visual Studio 2015からのものだ。詳しくは、「.NET TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]」をご覧いただきたい。同様な機能がVBには以前から備わっており、「.NET TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?」で解説している。
WriteLineメソッドの引数に出てくる先頭に「$」記号が付いた文字列については、「.NET TIPS:数値を右詰めや0埋めで文字列化するには?[C#、VB]」の後半(「C# 6.0/VB 14で追加された補間文字列機能を使用する」)を見てほしい。
また、「Main」メソッド末尾にReadKeyメソッドを置く意味は、「.NET TIPS:Visual Studioでコンソール・アプリケーションのデバッグ実行時にコマンド・プロンプトを閉じないようにするには?」をご覧いただきたい。
従来の書き方
参考までに、従来の書き方も示しておこう(次のコード)。
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
カウンター変数を間違いなくカウントアップするために、ループの冒頭でインクリメントしている。ループ末尾でインクリメントしていると、(特にループ内の処理が長いと)うっかり途中でcontinue文を使ってしまって正しくカウントされなくなるというバグを作り込んでしまう可能性があるからだ。
先のSelect拡張メソッドを使うコードでは、そんな心配はしなくてよいのだ。
C# 7.0/VB 15の新機能「値タプル」を利用する方法
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
Visual Studio 2017が必要。
また、NuGetから「System.ValueTuple」パッケージをプロジェクトにインストールしておく必要がある。
C#の値タプルについては「C# Tuple types」を参照してもらいたい(本稿執筆時点では英文のみ)。VBの解説は、本稿執筆時点ではまだないようだった。
Select拡張メソッドに与えているラムダ式の意味は、「コレクションの要素vとインデックスiを受け取り、vとiを持つタプルにして返す」というものだ。C#のコードのラムダ式の左側にある「(v, i)」はラムダ式への入力であり、右側の「(v, i)」はタプルの生成である。VBのコードは、タプルを生成するときにそれぞれの値に名前を付けている(C#のコードは、生成したタプルの持つ値は匿名)。
タプルを受け取るとき、C#はその場で分解できる(タプルそのものを変数に受け取ることも可能)。C#コードのforeachキーワード直後にある「var (value, index)」は、受け取ったタプルを分解して2つの変数「value」と「index」に代入している。VBでは、タプルの受け取りと分解をいっぺんに記述できず、タプルのままいったん変数に代入しなければならないようだ。
上のコードの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
C#のforeach文の記述は、理想に近づいたのではないだろうか。foreach文のループ変数としてvalueとindexの2つがあるかのように書けている。
とはいえ実際には、本稿冒頭で示したコードの匿名型がタプルに変わっただけであり、ループごとにオブジェクト(匿名型または値タプル)を生成することには変わりがない。
まとめ
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.