はじめに
C#のSystem.Collections.Generic.List<T>(以後List<T>)型には、MSDNのクラスリファレンスを見る限り、引数のないGetEnumaratorという3個のメソッドがあります。それぞれの返り値の型は次の通りです。
- System.Collections.IEnumeratorインターフェース
- System.Collections.Generic.IEnumerator<T>インターフェース (IEnumerator、 IDisposableを継承)
- System.Collections.Generic.List<T>.Enumerator構造体 (IEnumerator<T>、IDisposable、IEnumeratorを実装)
Javaプログラマー、Javaの経験がある方はもしかしたら次のような2つの疑問が浮かぶのではないのでしょうか?
- List<T>.Enumerator構造体がIEnumerator<T>インターフェース、IEnumeratorインターフェースを実装しているのだから、List<T>.Enumerator構造体を返すメソッド一つでいいのでは?
- メソッド名と引数の型と順序が同じメソッドがクラス内になぜ複数定義できるのか?
先に、結論から言ってしまいます。
一つ目の疑問の答えは、「それはできない」です。C#はJavaと違い共変戻り値型が無いからです。
二つ目の疑問の答えは、「それはできる」です。インターフェースの明示的な実装を用いることで可能です。
先に結論だけ述べてしまいましたが、この投稿ではなぜList<T>型が3個のGetEnumeratorというメソッドを持ち、それが可能で、必要なのかをまとめました。
関連クラス、インターフェース、構造体とメソッドを整理
まず関連のある関連クラス、インターフェース、構造体とメソッドを整理を整理します。
List<T>クラスは、いくつかのインターフェースを実装しています。IEnumerable<T>インターフェイスとIEnumerableインターフェイスも、その中に含まれています。
[SerializableAttribute]
public class List<T> : IList<T>, ICollection<T>, IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T>, IEnumerable<T>, IEnumerable
IEnumerable<T>インターフェイスは、IEnumerableインターフェイスを継承しています。
public interface IEnumerable<out T> : IEnumerable
IEnumerable<T>インターフェイスは、引数なしで、返り値型がIEnumerator<T> のGetEnumeratorメソッドを持っています。
IEnumerator<T> GetEnumerator()
IEnumerableインターフェイスは、引数なしで、返り値型がIEnumeratorのGetEnumeratorメソッドを持っています。
IEnumerator GetEnumerator()
似ているインターフェース名、そしてジェネリクス版と非ジェネリクス版があり、混乱してしまいそうですね。
さて、ここまでのことを総合するとList<T>型は次の二つのメソッドを実装する必要がありますね。
-
IEnumeratorを返すGetEnumeratorメソッド(IEnumerableで定義)
-
IEnumerator<T>を返すGetEnumeratorメソッド(IEnumerable<T>定義)
List<T>型のMSDNのクラスリファレンスを見てみると、上記二つ加えて同じGetEnumeratorという名前でSystem.Collections.Generic.List<T>.Enumerator構造体を返すメソッドがあります。List<T>.Enumerator構造体は、IEnumerator<T>、IDisposable、IEnumeratorを実装しています。
[SerializableAttribute]
public struct Enumerator : IEnumerator<T>, IDisposable, IEnumerator
さて、List<T>型は3個の戻り値型が違う、引数なしの、GetEnumerator型があることが分かりました。またMSDNのクラスリファレンスにも、三個のGetEnumeratorメソッドが記載されています。
Javaプログラマー、Javaの経験がある方はもしかしたら次のような2つの疑問が浮かぶのではないのでしょうか?
List<T>.Enumerator構造体がIEnumerator<T>インターフェース、IEnumeratorインターフェースを実装しています。そのため三個メソッドを定義せずとも、List<T>.Enumerator構造体を返すメソッドを一つだけ定義すればいいのではないか?というのが一つ目の疑問です。これについては、次の節でこれができないということを説明します。
二つ目の疑問は同じGetEnumeratorというメソッド名(引数の型と順序も同じ)のメソッドがクラス内になぜ複数定義できるのか、というものです。これは、次の次の節でインターフェースの明示的な実装を使えば可能であることを説明します。
Javaにはあって、C#にはない、メソッドの共変戻り値型
共変戻り値型とは、スーパークラスのメソッドをオーバーライドする際、戻り型をもとの戻り値型のサブクラスにできる言語特性です。
public class Shape { /*略*/ }
public class Circle extends Shape { /*略*/ }
public class ShapeBuilder {
/*略*/
public Shape create() { /*略*/ } //<-戻り値型 Shape!
}
public class CircleBuilder extends ShapeBuilder {
@Override
public Circle create() { /*略*/ } //<-戻り値型 Circle!
}
上記のコードは、共変返り値型を説明するためのJavaのコードです。(本来、各クラスは別々のファイルに分割すべきです。)
CircleBuilderのcreateメソッドの返り値型に注目してください。ShapeBuilder(CircleBuilderのスーパークラス)のcreateメソッドの返り値はShapeでしたが、CircleBuilderのcreateメソッドはCircle(Shapeのサブクラス)になっています。
これは当たり前ではありあせん。Javaが共変戻り値型という言語特性をもっているから可能なコードなのです。
さて、C#は共変戻り値型という言語特性を持っていません。もし仮に共変戻り値型があったのならばList<T>型はList<T>.Enumerator構造体を返すメソッドのみ実装すれば良かったでしょう。
C#には共変戻り値型がないので、List<T>型はそれが実装するインターフェースIEnumerableとIEnumerable<T>が持つそれぞれのGetEnumeratorメソッドを、それぞれ実装する必要があるのです。一個ではダメなのです。
さて、それぞれのメソッドを実装する必要があるのは分かりましたが、それは可能なのでしょうか?同じメソッド名で、引数の型と順序も同じメソッドがクラス内に複数定義できるのできるのでしょうか?C#ではインターフェースの明示的な実装を利用することで可能なのです。
C#にはある、インターフェースの明示的な実装
下記のコードは普通のC#のインターフェースと、それを実装するクラスのコードです。
public interface IHoge
{
void Hello ();
}
public class Impl : IHoge
{
public void Hello ()
{
Console.WriteLine ("Hello");
}
}
IHoge、Implの利用したコードと、その実行結果は次のようになります。
public static void Main (string[] args)
{
IHoge hoge = new Impl ();
hoge.Hello (); // Helloと出力
Impl impl = new Impl ();
impl.Hello (); // Helloと出力
}
インターフェースの明示的な実装をしてみます。IHogeのHelloメソッドを明示的な実装で実装した、Implは次のようになります。
public class Impl : IHoge
{
void IHoge.Hello ()
{
Console.WriteLine ("IHoge#Hello");
}
}
Helloメソッドが、IHoge.Helloというメソッドになっていること、その実装にアクセス修飾子がついていないことに注目です。実行してみます。
public static void Main (string[] args)
{
IHoge hoge = new Impl ();
hoge.Hello (); // IHoge#Helloと出力
Impl impl = new Impl ();
//impl.Hello (); // コンパイルエラー
}
Impl型のインスタンスがIHogeインターフェースの変数に入っている場合は、Helloメソッドを呼び出すことができます。しかし、Impl型の変数に入っている場合は、Helloメソッドを呼び出そうとするとコンパイルエラーになってしまいます。
さて、次のようなHelloメソッドを追加します。
public class Impl : IHoge
{
public void Hello ()
{
Console.WriteLine ("Hello");
}
void IHoge.Hello ()
{
Console.WriteLine ("IHoge#Hello");
}
}
実行結果を見てください。Impl型のインスタンスが、IHoge型の変数に入っている時は、IHoge#Helloと出力されているので明示的な実装をした方のHelloメソッドがよばれていますね。一方で、Impl型の変数に入っている時は、Helloと出力されていますね。
public static void Main (string[] args)
{
IHoge hoge = new Impl ();
hoge.Hello (); // IHoge#Helloと出力
Impl impl = new Impl ();
impl.Hello (); // Helloと出力
}
ちなみに、ImplクラスのHelloメソッドの返り値は、IHogeメソッドのHelloメソッドの返り値と同じにする必要はありません。
public class Impl : IHoge
{
public string Hello ()
{
return "Hello";
}
void IHoge.Hello ()
{
Console.WriteLine ("IHoge#Hello");
}
}
ImplクラスのHello型の返り値型をstringにしてみました。
public static void Main (string[] args)
{
IHoge hoge = new Impl ();
hoge.Hello (); // IHoge#Helloと出力
Impl impl = new Impl ();
Console.WriteLine (impl.Hello ()); // Helloと出力
}
新たに、IFugaというインターフェースを導入します。IHogeと同様に、Helloというメソッド名を持っています。メソッド名は同じですが、返り値型はintです。
public interface IFuga
{
int Hello ();
}
IFugaのHelloメソッドも明示的に実装してみます。
public class Impl : IHoge, IFuga
{
public string Hello ()
{
return "Hello";
}
void IHoge.Hello ()
{
Console.WriteLine ("IHoge#Hello");
}
int IFuga.Hello ()
{
Console.WriteLine ("IFuga#Hello");
return 0;
}
}
上記のコードを見て分かるように、同じメソッド名と引数の型と順序でも、インターフェースの明示的な実装であれば複数クラスで実装することが可能です。
public static void Main (string[] args)
{
IHoge hoge = new Impl ();
hoge.Hello (); // IHoge#Helloと出力
IFuga fuga = new Impl ();
fuga.Hello (); // IFuga#Helloと出力
Impl impl = new Impl ();
Console.WriteLine (impl.Hello ()); // Helloと出力
}
さてList<T>型の話に戻ります。
List<T>型はドキュメントには三個のGetEnumeratorメソッドがあります。その内の二つは実装しているインターフェースのメソッドです。
-
IEnumeratorを返すGetEnumeratorメソッド(IEnumerableで定義)
-
IEnumerator<T>を返すGetEnumeratorメソッド(IEnumerable<T>定義)
上記の例で見た通り明示的な実装では、同じメソッド名で同じ引数の型・順序のメソッドも複数メソッドを定義できます。List<T>型はこの二つのメソッドを明示的な実装で実装しています。これにより上記の実装する必要のある、IEnumeratorを返すGetEnumeratorメソッド(IEnumerableで定義)とIEnumerator<T>を返すGetEnumeratorメソッド(IEnumerable<T>定義)を実装しています。
List<T>型はニ個のメソッドに加えて、List<T>.Enumerator構造体を返すGetEnumeratorメソッドと合わせて、三個のGetEnumeratorメソッドを持ちます。
まとめ
C#のList<T>型は、GetEnumaratorメソッドが3個あります。
これは、C#はJavaと異なり共変返り値型という特性が無いこと、そしてC#はインターフェースの明示的な実装ができることと関係しています。
またList<T>に限らず、Dictionary<TKey, TValue>などIEnumerable<T>インターフェースを実装した他のクラスでも同様に複数のGetEnumeratorを実装しています。