ElementAtOrDefaultは勝手にインデクサになるよ。
日々レガシーコードと戦いつつ、やっとLINQが普及しはじめたかという弊社。
先日レビューでこのようなコードに出くわしました。
For Each使えとかは言わないでください。
' stringsは適当な文字列コレクション。
Dim sorted = strings.OrderBy(Function(s) s.Length)
For i = 0 To sorted.Count - 1
Dim item = sorted(i)
' 略
Next
初見で「HAHAHA、IOrderdEnumerableにDefaultプロパティは無いよ。せめてコンパイルの通るコードを出してくれたまえ。」と突き返そうとしました。
ところが聞くと、コンパイルどころかテストも通っているというではないですか。
この手の「なぜか動いてしまうVBコード」と戦うのも慣れたもので、まさかと思いつつ言語仕様に当たったところ、ありました。
Visual Basic 言語仕様 | 式 | クエリ式
既定のクエリインデクサー
要素型が T で、既定のプロパティを持たないすべてのクエリ可能なコレクション型は、次の一般的な形式の既定のプロパティを持つと見なされます。
Public ReadOnly Default Property Item(index As Integer) As T
Get
Return Me.ElementAtOrDefault(index)
End Get
End Property
この仕様によって、すべてのIEnumerable(Of T)はElementAtOrDefaultをインデクサとみなしてアクセスできます。やったね!
いや$O(n)$のインデクサって。[^1] 場合によって副作用もありって。
LINQ慣れした皆さんならお察しの通り、先のコードはOrderByの遅延実行のため$n$によって指数的に重くなります。
いっそコンパイルエラーを出してくれた方がよい気もするのですが、既にリリースされてしまっている仕様なので諦めて注意しましょう。
# 余談
この仕様はいわゆるダックタイピングとなっており、特定のインターフェースを要求しません。
要求されているのは以下の条件です。
- クエリ可能な型であること。(詳細は省略。)
- 既定の(Default)プロパティを持たないこと。
- Int32を引数とするElementAtOrDefaultメソッドを持つこと。
最小実装としてはこんな感じです。
```vb
Class Indexable(Of T)
Function [Select](Of T, TResult)(selector As Func(Of T, TResult)) As Indexable(Of TResult)
End Function
Function ElementAtOrDefault(index As Int32) As T
End Function
End Class
面白いのは拡張メソッドでもインデクサにできることでしょうか。おかげでIEnumerable(Of T)にも使えますし。
これで任意の型にインデクサが生やせます。やったね!
……この仕様、要る?