foreach でインデックスを使いたい

唐突ですが、プログラミングにおいて重要な制御構文は次の3つです。かしこまったプログラミングの本や、Wikipedia にはこう記述されています。

  • 順次 ( Sequence ) 前から(上から)順に実行
  • 選択 ( Selection ) 条件分岐.
  • 反復 ( Repetition ) 繰り返し.

「順次」?そんなことの当然でしょ、わざわざ3要素に入れる必要あるの??と思うのは現代のプログラマだったら抱く感情です。

以前は、変な場所にジャンプする goto などが普通に使われている時代がありまして、それによる弊害をなくすには「順次」という概念をプログラマ社会に浸透させることは非常に重要だったのです。

さて、前置きが長くなりました、本記事ではその3つのうち「反復」とインデックスについて語りたいと思います。

反復を使うというときは、配列やリストなどのデータ構造を順次スキャンしていくという場合が多く、同時に要素インデックスが必要となる場合が多いです。for 構文ならば特に何も考える必要はありませんが、foreach 構文の場合はどのようにしてインデックスを取得すればよいでしょうか。

for ループのループ変数をそのまま使う方法(なんの工夫もありません)

まずは、for ループです。下記にコードを示します。この場合はループ変数の k がインデックスそのものなので 問題ありません。

		private void menuDebugExec001_Click( object sender, RoutedEventArgs e )
		{

			String CAP = "単純なforループ";

			StringBuilder sb = new StringBuilder();

			List<int> data = new List<int>();
			data.Add(    1 );
			data.Add(   11 );
			data.Add(  111 );
			data.Add( 1111 );

			for ( int k = 0; k < data.Count; k++ )
			{
				String s = String.Format( "data[{0}] is {1}", k, data[k] );
				sb.AppendLine( s );
			}

			String msg = sb.ToString().Trim();
			MessageBox.Show( msg, CAP );

		}

foreach を使いつつユーザが宣言した変数をインクリメントする方法

さて、次に foreach です。foreach にはインデックスがありません。それが利点です。

確保済みメモリ配列やリストに不注意なインデックスの値でアクセスをするとメモリ破壊を起こします。何十億円、何百億円もする飛行機やロケットの制御プログラムだったら最高にマズいですね。

もしそんな不安があるときは foreach を使えばいいのです。

インデックスがないのならば、作ればいいということで foreach 突入前に counter というローカル変数を宣言して、それをインクリメントしてしのぎます。

		private void menuDebugExec002_Click( object sender, RoutedEventArgs e )
		{

			String CAP = "foreachにカウンタ変数を併用";

			StringBuilder sb = new StringBuilder();

			List<int> data = new List<int>();
			data.Add(    2 );
			data.Add(   22 );
			data.Add(  222 );
			data.Add( 2222 );

			int counter = 0;
			foreach ( var v in data )
			{
				String s = String.Format( "data[{0}] is {1}", counter, v );
				sb.AppendLine( s );

				counter++;
			}

			String msg = sb.ToString().Trim();
			MessageBox.Show( msg, CAP );

		}

しかし、いちいち変数を宣言しなければならないというのが面倒です。ループの中で counter++ するのを忘れたらどうなるでしょう。メモリ破壊を引き起こすことはないでしょうが、意図しない動作に悩まされることになります。

foreach と Select を併用してインデックスを参照する方法

foreach と Select を併用すれば余計な変数を宣言することもないし、ループの中でインクリメントするコードを書く必要もありません。その実例が下記のコードになります。

		private void menuDebugExec003_Click( object sender, RoutedEventArgs e )
		{

			const String CAP = "foreachにSelect併用";

			StringBuilder sb = new StringBuilder();

			List<int> data = new List<int>();
			data.Add(    3 );
			data.Add(   33 );
			data.Add(  333 );
			data.Add( 3333 );

			foreach( var v in data.Select(( value, index ) => new { value, index })) 
			{
				int idx = v.index;
				int tmp = v.value;
				String s = String.Format( "data[{0}] is {1}", idx, tmp );
				sb.AppendLine( s );
			}

			String msg = sb.ToString().Trim();
			MessageBox.Show( msg, CAP );

		}

と、こんな記事を書いておきながら、自分は foreach で反復処理をするのはいまいちしっくりこないのです。

for でループ変数が明示して増減していて、それに連動したインデックスがあることに安心感を覚えるのです!