はじめに
前回 は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>IListICollection<T>ICollectionIEnumerable<T>IEnumerableIReadOnlyList<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>IEnumeratorIDisposable
となります。
説明し終わったところで、改めて(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です。)
