[C#]IEnumerableはListではない(当たり前だが...)

  • 投稿日:
  • by
  • Category:

IEnumerableとListが別物であることは至極当然なんだけど、C#でLINQを使う中でIEnumerableとListの違いがいまいちよく分かっていなかったので復習。

 

背景

客先でC#を触るとき、大抵は

  • IEnumerable:Dapperから取ってきたデータやそれをLINQで加工する際に使用→遅延評価で効率よく処理できる。
  • List:Dapperにクエリを投げる際のパラメータ用クラスの作成やListのAdd、Remove、インデクサを使ってのアクセスなどLINQを使わないデータ加工に使用。→前もってIEnumerableをToList()で変換から操作してやらないといけない(ToListで変換する時に実際の評価が走る)

このような認識、使い分けをしている。

IEnumerableでは遅延評価をめいっぱい活用してLINQでデータをガシガシ加工できるが、その代わりにListのようにAdd/Removetといった変更はできないし、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の丼

上記引用内容から、

  • IEnumerableは列挙(foreach)が可能。
  • ICollectionは要素数を把握(Count)が可能で列挙も可能。
  • IListは追加・変更・削除を含めた全てが可能で、列挙(foreach)可能、要素数を把握(Count)可能。

こういう違いがある。

機能的に(というか継承の流れ)はIList>ICollection>IEnumerableだが、遅延評価の恩恵を得るならIEnumerable。

ICollectionは...Countを取るだけってことは稀で、大抵の場合はIListで事足りるからイラネ。

というのが今回学んだこと。以下はそれに至るまでの蛇足。

 

IEnumerableは受け取るけどListはエラーを吐く困った子

さて話戻って今触ってる客先のシステムだが...なんとこのシステム、「Dapperにクエリを投げる為のExecuteメソッド」にListシーケンスを投げたら落ちてしまった。

調べてみると、Dapper自体は複数件のデータを問題なく取り扱えるのだが(仕様通り)、そのwrapperの中で独自実装されたログ出力処理がListに対応しておらず(1件しか受け付けない前提)そこで落ちている様子。

これまではどうやって処理してたんや...と思い他の処理を洗い出してみると、複数件を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)と同じというわけだ。

結局インデックスのないプロパティ専用のメソッドにインデックス付きのプロパティを投げたのが例外エラーの原因ということが分かった。

 

インデックス付きプロパティとは

インデックス付きプロパティはインデクサとも呼ばれる。インデクサを持つプロパティはhoge[0]のようにあたかも配列にアクセスするかのようにオブジェクトにアクセスできる。

配列のように書けるが実際は配列ではない。一番簡単な例はstringだろう

str[0]と書けばstr変数が持つ文字列の1文字目にアクセスできるが、しかしC#のstringはC言語のようにchar型の配列として保持しているわけではない。

stringはインデクサを持っており、一文字ずつインデックス番号が振られている為、str[0]と書くとインデックス番号0番の文字を取得することができる(これが配列っぽい動作をする、が配列でない)ということなのだ。

 

見た目はIEnumerable、中身は...?

ようやく話のオチに近づいてきた。

先ほどIEnumerableなら通ったのにListはエラーになったのは、Listがインデクサを持つからだ。

ややこしいことに、例えば関数の引数型に「IEnumerable<string>」を指定したとしても、「IEnumerableを継承しているList<string>」も渡せてしまうし、当然「Listではない純粋のIEnumerable」も通ってしまう。もしかしたら「IEnumerableを継承しているICollection」が渡されるかもしれない。これも通ってしまう。

 

例えばListをSelectやWhereで加工した場合、得られるのは<WhereSelectListIterator>であり、この実体は「IEnumerableを継承しているList」だ。ここまではいい。

複数のListを更にIEnumerableでシーケンスとして持っているデータがあり、それをSelectManyで平坦化したら得られるのはIEnumerable<T>オブジェクトである<SelectManyIterator>だ。これはListではないのでインデクサは持たないらしい。なんとも面倒くさい。

IEnumerableを継承している型であればIEnumerableとして引数に渡すことができてしまうのは当然だが、渡せたからといってそれが「素のIEnumerableである」とは言えない、というのが今回の教訓だ。

 

もしかしたら製作当時は「インデクサ付きのリストを渡さない」というルールがあったのかもしれないが、それならそれで「素のIEnumerableしか渡せない」作りにしなければらない(それが実現可能かどうかは分からないが)。

コード上渡せてしまう以上はそれを意識したロジックにしなければいけないのだが、そもそも製作当時にそんなケースがあるなんて想定していなかったのであれば対応のしようがない。

自分が作る側に回った時に同じ過ちを犯さない為にも、備忘録として残しておくことにする。

コメントする