はじめに
前回 はList<T>について、色々と書いて見ました。
今回は前回割愛したもののうち、foreach
ついて色々書きたいと思います。
おまけ程度にVisual Studioのおかしな挙動についても少し書いてます。
C#が用意している構文の中身について知りたいちょっと変わった人向けです。
今回も独学で書いている部分が多いので、間違ってたらガンガン指摘してください。
タイトルは特に思いつかなかったので前回と同じなのは秘密。
foreachとは
List
や配列等の要素を1つずつ取り出すための構文。
「C#入門」とか「初心者向けC#」とか読んでも必ず出てくる。
実は内部で結構複雑なことをやってるけど、それをおくびにも出さない凄いお方。
最近はLINQに出番を奪われることもあるが、まだまだ現役バリバリ。
foreachのあれやこれや
foreachってどうなってるの?
まずはソースコードから。
var listStr = new List<string>() { "aaa", "abc", "bbc", "aba", "cbc", "bcc" };
foreach (string item in listStr)
{
if (item.Contains("a"))
{
Console.Write(item);
}
}
List
を使ったことがあるのに、foreach
を使った事が無い人はいないと思います。
for
で書いていた、
- 初期化条件
- 終了条件
- インクリメント条件
がいらないので、非常に便利です。勝手に最初から最後までやってくれます。
でも、いらないと言っても誰かが条件を設定しなければならないはずです。誰がやってくれてるんでしょうかね?
その秘密はforeach
の真の姿に隠されています。
2019/01/17
大嘘でした。(2回目)
インターフェース越しに展開すると仮想呼び出しのコストが掛かるので、List<T>
内部の構造体に展開されます。(情報ありがとうございます。)
var listStr = new List<string>() { "aaa", "abc", "bbc", "aba", "cbc", "bcc" };
//(1)
//IEnumerator<string> e = listStr.GetEnumerator();
List<string>.Enumerator e = listStr.GetEnumerator();
//(2)
while (e.MoveNext())
{
//(3)
string item = e.Current;
if (item.Contains("a"))
{
Console.Write(item);
}
}
foreach
はコンパイルするとこんな感じに展開されます。
突然大量の見たこともないメソッドやらプロパティやらが溢れてきて面食らいますが、一つ一つ解説していきますので安心(?)してください。
話がそれますが、今回のように「実際に利用者が書いたコード」と「コンパイラの展開結果」が一致しないことは多々あります。
これはシンタックスシュガーと呼ばれる機能で、糖衣構文なんて呼んだりもします。
話を戻して、まずは(1)を見てください。
//(1)
List<string>.Enumerator e = listStr.GetEnumerator();
GetEnumerator
は、List
クラスのメソッドです。
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return new Enumerator(this);
}
突然出てきました。IEnumerable<T>
インターフェースとIEnumerator<T>
インターフェース、そしてEnumerator
構造体です。
順番に解説してきましょう。
IEnumerable<T>インターフェースってなに?
実は、foreach
が受け入れる事が出来る変数は、必ずIEnumerable<T>
を継承している必要があります。
2019/01/16
大嘘でした。正しくは、
-
foreach
に投げる型が、GetEnumerator
を実装している。 -
GetEnumerator
の戻り値の型が、MoveNext
メソッドとパブリックなCurent
プロパティを実装している。
上記2つを満たしている必要があります。(情報ありがとうございます。)
I(インターフェース) + Enumerate(...を数え上げる) + able(出来る)なので、「数え上げる事が出来る(ことを規約する)インターフェース」といった感じの意味になります。(英語は苦手なので間違ってるかもしれません)
public interface IEnumerable<out T> : IEnumerable
{
new IEnumerator<T> GetEnumerator();
}
メソッドは1つのみです。ジェネリクスで指定された型のIEnumerator<T>
を返すメソッドです。
ここで、改めてList
クラスの定義を見てみます。
public class List<T> : IList<T>, System.Collections.IList, IReadOnlyList<T>
List
クラスが直接継承しているインターフェースは、IList<T>
, IList
, IReadOnlyList<T>
の3つです。
そして、IList<T>
はICollection<T>
を、ICollection<T>
はIEnumerable<T>
を、IEnumerable<T>
はIEnumerable
を継承しています。
さらに、IList
はICollection
を、ICollection
はIEnumerable
を継承しています。
最後に、IReadOnlyList<T>
はIReadOnlyCollection<T>
を、IReadOnlyCollection<T>
はIEnumerable<T>
を、継承しています。
継承祭りですね。てんやわんやなので少し整理すると、Listクラスが継承しているインターフェースは、
IList<T>
IList
ICollection<T>
ICollection
IEnumerable<T>
IEnumerable
IReadOnlyList<T>
IReadOnlyCollection<T>
となります。
先述の通り、IEnumerable<T>
を継承してるので、foreach
はList
を受け入れる事が可能です。
なんで同じ名前でジェネリクスしてたり、してなかったりするの?
また少し話がそれますが、<T>
はジェネリクスだよ。って事は前回お話しました。
「IList<T>
とIList
って何がちゃうねん、コラ」と思う方もいると思います。
C#にジェネリクスの機能が追加されたのは、2005年、C#の2.0からです。
(僕がまだギリギリランドセルを背負ってたくらい昔の話です。)
IList
、ICollection
、IEnumerable
はそれ以前からC#に存在している方々です。
ジェネリクスが存在しないので、当然List<T>
も存在しません。
このころはList
的な物を使いたい場合、取り扱う型ごとに違うクラス、もしくはobject
で全てを受け入れるクラスを作っていたんだと思います。(**推測です。**このころの事は全然知らないので詳しい方、教えてください。)
これでは不便だろうと言うことで、ジェネリクスが誕生しました。そして、IList<T>
、ICollection<T>
、IEnumerable<T>
、そしてList<T>
も合わせて生まれます。
誕生した所までは良かったんですが、「じゃあ今まで使ってたジェネリクスしてない(非ジェネリクス)コードはどうするんだ?」問題が発生します。
過去のコードが使用できなくなるような変更を破壊的変更と言いますが、C#は破壊的変更を避けるようにかなり慎重に機能の追加・修正を行っています。(破壊的変更を避けるのは、C#に限った話ではありませんけども)
そんな経緯があるので、非ジェネリクスインターフェースは残っています。
IReadOnlyList<T>
は、.Net Framework 4.5で追加された機能です。2012年、C#は5.0のなのでこんな話とは無縁です。なので、非ジェネリクスなインターフェースはいません。
(僕が社会人になる3年くらい前です。ずいぶんと最近な気がしますね。)
IEnumerator<T>インターフェースってなに?
IEnumerable<T>
はList
が継承していました。では、IEnumerator<T>
はどこにあるんでしょうか?
こいつはEnumerator
構造体が継承しています。まず先に、IEnumerator<T>
の定義から。
I(インターフェース) + Enumerate(...を数え上げる) + or(もの)なので、「数え上げる処理をする(ことを規約する)インターフェース」といった感じの意味になります。(英語は苦手なので以下略)
IEnumerator
には2つのメソッドと1つのプロパティが、IEnumerator<T>
には1つのプロパティがあります。
public interface IEnumerator
{
bool MoveNext();
Object Current
{
get;
}
void Reset();
}
public interface IEnumerator<out T> : IDisposable, IEnumerator
{
new T Current
{
get;
}
}
foreach
はIEnumerator
のMoveNext
メソッドとIEnumerator<T>
のCurrent
プロパティを使用しています。
(本当はIDisposable
のDispose
も使用しているのですが、List
の場合、処理なしで実装されているので割愛)
Enumerator構造体ってなに?
このEnumerator
構造体ですが、List
クラスの内部に定義されているローカル構造体です。
public struct Enumerator : IEnumerator<T>, System.Collections.IEnumerator
Enumerator
は、IEnumerator<T>
とIEnumerator
を継承しています。
そして、IEnumerator<T>
は、IDisposable
とIEnumerator
を継承しています。
なのでEnumerator
構造体が継承しているインターフェースは、
IEnumerator<T>
IEnumerator
IDisposable
となります。
説明し終わったところで、改めて(1)に戻ります。
//(1)
List<string>.Enumerator e = listStr.GetEnumerator();
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return new Enumerator(this);
}
GetEnumerator
が呼び出された時、Enumerator
のコンストラクタを実行しています。
引数はthis
なので、List
自身ですね。
Enumerator
のコンストラクタとフィールドはこうなっています。
private List<T> list;
private int index;
private int version;
private T current;
internal Enumerator(List<T> list) {
this.list = list;
index = 0;
version = list._version;
current = default(T);
}
順を追ってみると、
-
GetEnumerator
が呼び出された時、コンストラクタの引数にList
自身を渡す。 -
Enumerator
内のフィールドにて、渡されたList
、初期index
、渡された時点での_version
、current
を設定。
これで終わりです。とてもシンプルですね。
次に(2)です。
//(2)
while (e.MoveNext())
ループ処理の終了条件です。Enumerator
内の実装はこうなっています。
public bool MoveNext()
{
List<T> localList = list;
if (version == localList._version && ((uint)index < (uint)localList._size))
{
current = localList._items[index];
index++;
return true;
}
return MoveNextRare();
}
private bool MoveNextRare()
{
if (version != list._version)
{
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
}
index = list._size + 1;
current = default(T);
return false;
}
まず、コンストラクタで保存したversion
とlocalList._version
が一致していて、index
が0以上かつ、localList._size
未満であるか確認しています。
条件を満たしていた場合は、current
フィールドの値を書き換えて、index
をインクリメント。次に進むことが出来たのでtrue
を返して終了です。
条件を満たしていない場合は、version
とlocalList._version
を確認し、一致していなければInvalidOperationException
を発行。そうでない場合は、``currentを初期化。次に進めなかったので、
false`を返して終了です。
つまり、
-
version
とlocalList._version
が一致していない。→InvalidOperationException
を発行。 -
index
が0以上かつ、localList._size
未満。→current
に値を設定してtrue
を返す。 - それ以外の場合。 →
current
を初期化してしてfalse
を返す。
こうやってループの終了を判定しているんですね。(優先順位は上から)
前回の記事の「コレクションの変更ってどうなってるの?」で割愛した「foerach
も似たようなことをやっています。」がこの部分になります。
最後に(3)です。
//(3)
string item = e.Current;
public T Current
{
get
{
return current;
}
}
ただのゲッターなので、特に言うこともありません。(2)で設定した値を取得しています。
これを知ってると何が出来るの?
自作のクラスをforeach
に渡す事が出来るようになります!おそらく意味はない
正直面倒くさいだけで実用性は皆無ですが、勉強としては意味がある...んじゃないですかね。多分。
実際に作ってみました。
満たすべき要件
- 要素を
Add
で追加できる。(メソッドの作成) - 要素を
foreach
で取り出せる。(インターフェースの実装)
この2つだけです。なので、取り出し時は入れた時とは逆順で出てきます。削除もインデクサでの取り出しも出来ません。それでいいんです、自作なので。
まずはListModoki
の作成です。
partial class ListModoki<T> : IEnumerable<T>
{
private ListValue<T> head;
public void Add(T v)
{
head = new ListValue<T>(v, head);
}
public IEnumerator<T> GetEnumerator()
{
return new ListNakami<T>(head);
}
IEnumerator IEnumerable.GetEnumerator()
{
return new ListNakami<T>(head);
}
}
IEnumerable<T>
の部分です。(説明用にクラスをpartialで分割してますが、本来は不要です。)
先頭位置を記憶するためのhead
を持っています。ListValue<T>
は、値と次の要素を持ったクラスです(詳細は後述)。
partial class ListModoki<T>
{
private class ListNakami<T> : IEnumerator<T>
{
private ListValue<T> current;
public ListNakami(ListValue<T> current)
{
this.current = current;
}
public T Current
{
get
{
ListValue<T> c = current;
current = c.Next;
return c.Value;
}
}
object IEnumerator.Current
{
get
{
return Current;
}
}
public bool MoveNext()
{
if (current == null)
{
return false;
}
return true;
}
public void Dispose() { }
public void Reset() { }
}
}
IEnumerator<T>
の部分です。内部クラスとして定義してます。
Current
で値を取得する時についでに、次に進めています。
今回は面倒なのでここでしか使わないのでCurrent
に副作用を持たせていますが、かなり危険な行為です。
理由は一番最後におまけで書いておくので、読んでください。
class ListValue<T>
{
public T Value;
public ListValue<T> Next;
public ListValue(T value, ListValue<T> next)
{
Value = value;
Next = next;
}
}
最後にListModoki
の中身、ListValue
です。自分の値と次の中身を持っています。
static void Main()
{
ListModoki<int> modoki = new ListModoki<int>();
for (int i = 0; i < 10; i++)
{
modoki.Add(i);
}
foreach (int item in modoki)
{
Console.WriteLine(item);
}
}
実際に0から9まで値を追加した後、順番に取り出していきます。
9
8
7
6
5
4
3
2
1
0
最初に言い訳したとおり、入れた時とは逆の順番で出てきます。さらに、インデクサでのアクセスが出来ないので、for
は使用できません。(超不便)
既存のクラスでは実現できない要求が来た時に、覚えておくと役に立つ可能性は否定出来ないんじゃないかと思います。(超弱気)
おわりに
前回書けなかった部分のうちIEnumerable<T>
について書こうと思ってたらforeach
の話になってました。
自作のListModoki
を要求されることはまず無いと思います。どう考えてもList
の方が万能です。
次の内容は未定です。次があるかどうかも未定です。
おまけ:Visual Studioのバグ...?
ゲッターに副作用を持たせた状態で、Visual Studioでデバックを行うとバグります。
ウォッチ式やマウスオーバーした時に出るアレ(名前を知らない)で値を確認するたびに、**その副作用が実行されます。**今回の場合、勝手にcurrent
が進んでいきます。
結構深刻なバグの元になりかねないので、ゲッターに副作用を持たせるのは止めましょう。
簡単なテストを行なった画像を下に貼っておきます。(Visual Studio 2017です。)