IEnumerableとListが別物であることは見りゃ分かるんだけど、実際にC#でLINQを使ってみたらIEnumerableとListの違いがいまいちよく分からず混乱したので復習してみた。
背景
C#でIEnumerableやListを使うとき、
- IEnumerable
- DapperなどのORMでデータベースから取得したデータをLINQで加工する際に使用する。遅延評価が有効なので、抽出やソートなどの加工を効率よく処理できる。
- List
- Dapperにクエリを投げる際のパラメータの作成や、要素のAdd/Remove、インデクサを使ってのアクセスなど、LINQを使わず手動でデータを加工するのに使用する。またIEnumerableを直接加工したいとき.ToList()メソッドでListに変換してから加工する。.ToList()で変換する時に実際の評価が走る。
このような認識、使い分けをしている。
IEnumerableでは遅延評価をめいっぱい活用してLINQでデータをガシガシ加工できるが、その代わりにListのようにAdd/Removeといった変更はできないし、hoge[1]といった形でインデクサによるアクセスもできない。ここがIEnumerableとListの違いで、それぞれ一長一短だと思っている。ここまでが知っていたこと。
IEnumerable、ICollection、IList
大抵はIEnumerableとIListインターフェイスがベースのものしか使わないが、実はICollectionというものもある。
この3つのインターフェイスの違いは以下のとおりである。
- IEnumerableインターフェイスは、「列挙可能」である事を示します。
- ICollectionインターフェイスを実装しているクラスは、要素数を把握可能である事を示します。
- ICollectionはIEnumerableを継承しています。そのため、ICollectionにキャスト出来た場合は、勿論「列挙が可能」と言う事になります。
- IList インターフェイスは、リスト形状のコレクションに対する、追加・変更・削除も含めた、全ての操作が可能である事を示します。
- このインターフェイスは、ICollectionを継承しているので、要素数の把握も可能(つまり、Countプロパティが使える)で、更にIEnumerableも継承していることになるので、列挙も可能です。
列挙可能から完全なるモノまで - IEnumerableの探索 - C# Advent Calendar 2014 - kekyoの丼
上記引用内容から、
こういう違いがある。継承関係についてはこちらの記事も詳しい。
機能的に(というか継承の流れ)はIList>ICollection>IEnumerableだが、遅延評価の恩恵を得るならIEnumerable。
ICollectionは...Countを取るだけってことは稀で、大抵の場合はIListで事足りるからイラネ。
というのが今回学んだこと。以下はそれに至るまでの蛇足。
IEnumerable<T>は受け取るがList<T>だとエラーを吐く困った子
今触ってる客先のシステムだが...なんとこのシステム、「Dapperにクエリを投げる為のExecuteメソッド」にList<T>シーケンスを投げたら例外で落ちてしまった。
string sql = "INSERT INTO Customers (Name, Email) VALUES (@Name, @Email);"
object[] parameters = { new { Name = "John Doe", Email = "jdoe@example.com" } };
using (var connection = new SqlConnection(connectionString))
{
conn.Execute(sql, parameters);
}
↑DapperのExecuteメソッドのサンプル。本来パラメータに配列やListシーケンスを渡すことで複数データを処理できる筈なのだが...なぜエラーに?
コードを追ってみると、どうやらDapperのExcuteメソッド直接呼んでいるのではなく、Excuteメソッドをラップする独自メソッドが実装されており、この中に書かれたログ出力処理で落ちている様子。
じゃあ複数件のデータをどうやって処理してたの...と思って他の処理を洗い出してみると、複数件をInsert/Updateする場合は全て手前でforeachしており、Executeをラップするメソッドには1件ずつしかパラメータを渡していなかった。なぜ機能を退化させるんだ...。
Reflectionでプロパティから値を取得する処理が曲者
問題の箇所はInsertのSQL文とそのパラメータ(VALUES句)を丸々ログに吐いている処理。SQLで何を投げたかを後から確認できるようにする為だろう。
具体的には、Executeメソッドにパラメータとして渡されたオブジェクトに対してリフレクション機能を用いてプロパティへアクセスし、全てを文字列化してログに吐いている。
ポイントはPropertyInfo.GetValue
メソッド。
GetValue(Object)
指定されたオブジェクトのプロパティの値を返します。
注釈:インデックス付きプロパティの値を取得しようとすると、メソッドは TargetParameterCountException 例外をスローします。
GetValue(Object, Object[])
指定したオブジェクトのプロパティの値を返します。インデックス付きプロパティの場合は、オプションでインデックス値を設定できます。
ポイントは「プロパティがインデックス付きの場合はGetValue(Object, Object[])
を使わなければTargetParameterCountException が発生する」点。今回吐いているエラーもまさにこれだ。
しかしながら実装を見るとGetValue(Object, null)
とちゃんと第二引数が設定してある。でもnull
だ。第二引数をnull
にするとどうなるのか。
Object[]
インデックス付きプロパティのインデックス値 (省略可能)。 インデックス付きプロパティのインデックスは 0 から始まります。インデックス付きでないプロパティの場合は、この値をnull
にする必要があります。
第二引数をnull
にするのはインデックス付きでないプロパティの場合、つまりGetValue(Object)
と同じになってしまう。
つまりインデックスのないプロパティ専用のメソッドなってしまっており、そこにインデックス付きのプロパティ(Listシーケンス)を投げたことで例外エラーが出たということが分かった。
インデックス付きプロパティ(インデクサ)とは
「インデックス付きプロパティ」というのはVB.NETの言葉らしい。C#ではインデクサと言うそうだ。
インデクサを持つプロパティはhoge[0]
のようにあたかも配列にアクセスするかのようにオブジェクトにアクセスできる。
一番簡単な例はstringで、str[0]
と書けばstr変数が持つ文字列の1文字目にアクセスできるわけだが、C#のstringはC言語の文字列のようにchar型の配列として保持しているわけではない。
C#のstringはインデクサを持っており、一文字ずつインデックス番号が振られている為、str[0]
と書くことでインデックス番号0番の文字を取得することができる。この振る舞いはまるで配列のようだが、実際は配列でないんですよ、ということらしい。
見た目はIEnumerable<T>、中身は...?
ようやく話のオチに近づいてきた。
先ほどList<T>シーケンスを渡したらエラーになったのは、List<T>がインデクサを持つからだ。
例えば関数の引数型に「IEnumerable<string>」を指定した場合、全く同じ型である「IEnumerable<string>」が渡せるのは言うまでもないが、「IEnumerableを継承しているList<string>」も渡せてしまうし、「IEnumerableを継承しているICollection」を渡すことができてしまう。これらにはインデクサの有無など、色々と違いがあるにもかかわらず。
「継承しているんだからそんなの当たり前だろう」と言ってしまえばその通りなのだが、「継承されていればそのまま渡せてしまう」というのは案外と見落とされがちだ。
確かにListは何でもできる型なので気軽に使ってしまいがちだが、IEnumerableやICollectionとの違いを理解した上で使い分けないと今回のようなバグを作ってしまう事になる。
引数の型がIEnumerableであれば、IEnumerableを継承している型も渡せてしまうのは意味当然ではあるのだが、渡せたからといってそれが「素のIEnumerableである」とは言い切れない、というのが今回の教訓だ。
もしかしたらこのメソッド製作当時は「インデクサ付きのデータ(例えばListシーケンス)を渡さない」という暗黙のルールがあったのかもしれないが、それならそれで明示的に「素のIEnumerableしか渡せない」作りにしなければらない(一万歩譲って最低でもコメントに書いておいてほしい)。
コード上渡せてしまう物は渡ってくる前提でロジックを作らないといけない。もしかしたら製作当時はそういうケースがあるなんて想定していなかったのかもしれないが...。
自分が作る側に回った時に同じ過ちを犯さない為にも、備忘録として残しておくことにする。
コメントする