【C#】自作LINQオペレータの作り方 

# はじめに

C#erなら誰しもがLINQを使っていますよね？
`Where`とか`Select`とかよく使うと思います。あれって魔法みたいに見えますが、__その実ただの`IEnumerable<T>`の拡張メソッドでしかない__んですよね。

てことは自分で作れます。しかも、幸いにもC#には`yield return`があるのでものすごく簡単に実装できます。
試しに`Where`と`Select`を自前で実装してみましょう。


## Whereを自前実装する

`Where`は受け取ったソースシーケンスの中から、条件に合致したものだけフィルタリングするオペレータです。
これを簡易的に自前で実装したコードは以下になります。

```csharp
public static IEnumerable<T> MyWhere<T>(this IEnumerable<T> source, Func<T, bool> filter)
{
    foreach (var item in source)
    {
        if (filter(item)) yield return item;
    }
}
```

たったのこれだけで、`Where`オペレータを再現することができます。

中身を見れば非常に単純ですよね。ソースを`foreach`して、フィルターが通ったものだけ`yield return`してるだけです。

`yield return`によって遅延評価な`IEnumerable<T>`がかんたんに生成できるので、たったのこれだけのコードで`Where`オペレータが実装できてしまいました。

## Selectを自前で実装する

同様に`Select`も自前で実装してみましょう。
`Where`と同様に`yield return`を使うと、以下のようにかんたんに実装することができます。

```csharp
public static IEnumerable<TResult> MySelect<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector)
{
    foreach (var item in source)
    {
        yield return selector(item);
    }
}
```

`Select`の場合、コレクションの型を変換することになるので、型引数が2つ必要になります。
変換前のコレクションの型を表す`TSource`と、変換後のコレクションの型を表す`TResult`ですね。
そのため、受け取る第一引数は`IEnumerable<TSource>`、戻り値は`IEnumerable<TResult>`になっていることがわかると思います。

第二引数は`Func<TSource, TResult>`になっています。`TSource`から`TResult`に変換する処理を受けるためですね。

そして中身の実装はこれまた非常に単純で、ただ単に`foreach`ですべての要素を変換処理かけて`yield return`するだけです。とてもかんたんです。

## 使ってみる

自前で用意した`MyWhere`と`MySelect`を使ってみます。

```csharp
class Program
{
    static void Main(string[] args)
    {
        IEnumerable<int> source = Enumerable.Range(1, 10);

        source.MyWhere(i => i > 5).Print();
        source.MySelect(i => $"{i}です").Print();
    }
}
```

ちなみに`Print`は受け取ったコレクションの要素をすべて`Console.WriteLine`するメソッドです。
これも拡張メソッドで書きました。

```csharp
public static void Print<T>(this IEnumerable<T> source)
{
    foreach (var item in source)
    {
        Console.WriteLine(item);
    }
}
```

結果

```
6
7
8
9
10
1です
2です
3です
4です
5です
6です
7です
8です
9です
10です
```

正常に動作していることがわかります。

# LINQオペレータの自作は簡単にできる

このように、`yield return`があるおかげでLINQオペレータはかんたんに自作できます。

LINQオペレータ自作のポイントをもう一度整理すると次のようになります。

- `IEnumerable<T>`の拡張メソッドとして定義する
- 戻り値も`IEnumerable<T>`にする[^1]
- 拡張メソッドの実装は`foreach`して`yield return`が基本

[^1]: もちろん`T`は必要に応じて変えて下さい

特にこの__`yield return`を使う部分はとても重要で、これを使わないと実装が非常に面倒になる or 遅延評価じゃなくなります__。これについては記事の最後で触れたいと思います。

## 完全自作オペレータ`Combine`を作ってみる

LINQオペレータの作り方がわかったので、以下のような挙動の完全自作オペレータ`Combine`を作ってみます。

- 2つのシーケンスを合成して1つのシーケンスにして返す。
- 合成の仕方を指定できる。
- 合成元の2つのシーケンスの要素数が異なる場合は、要素数が大きい方に合わせる。

シーケンスの合成というと`Zip`が思い浮かびますが、`Zip`は合成元シーケンスの要素数が異なる場合には要素数が小さい方に合わせ、要素数が大きい方の溢れた要素は破棄されます。
それに対して今回新しく作る`Combine`は、要素数の大きい方に合わせ、不足分は`default`で補完されるようにします。

動作イメージとしては、次のようなものを想定します。

```csharp
//1~10の10個の要素
IEnumerable<int> ints = Enumerable.Range(1, 10);
//0.5~2.5の5個の要素
IEnumerable<double> doubles = Enumerable.Range(1, 5).Select(n => n * .5);
//intsとdoublesをそれぞれ掛け合わせた式と答えを表示
IEnumerable<string> strings =
    ints.Combine(doubles, (i, d) => $"{i}×{d}={i * d}です");
```

```
1×0.5=0.5です
2×1=2です
3×1.5=4.5です
4×2=8です
5×2.5=12.5です
6×0=0です
7×0=0です
8×0=0です
9×0=0です
10×0=0です
```

### シグネチャの決定

まず最初に、新しく作る`Combine`オペレータのシグネチャを決定します。

第一引数はひとつめの合成元シーケンスとして`this IEnumerable<TSource1>`とします。
第二引数はふたつめの合成元シーケンスとして`IEnumerable<TSource2>`ですね。
合成する2つのシーケンスは型が異なっても良いので、型引数名を分けておきます。

第三引数には合成の仕方を指定できるようにするため、型は`Func<TSource1, TSource2, TResult>`とします。
`TResult`は合成後のシーケンスの型です。したがって戻り値は`IEnumerable<TResult>`となります。

以上から`Combine`オペレータのシグネチャは以下のように決定します。

```csharp
public static IEnumerable<TResult> Combine<TSource1, TSource2, TResult>(
    this IEnumerable<TSource1> source1,
    IEnumerable<TSource2> source2,
    Func<TSource1, TSource2, TResult> combiner);
```

### 中身の実装

シグネチャが決まれば、あとは`yield return`を活用して中身をゴリゴリ実装するだけですね。
以下のような実装になりました。

```csharp
public static IEnumerable<TResult> Combine<TSource1, TSource2, TResult>(
    this IEnumerable<TSource1> source1,
    IEnumerable<TSource2> source2,
    Func<TSource1, TSource2, TResult> combiner)
{
    if (source1 is null) throw new ArgumentNullException(nameof(source1));
    if (source2 is null) throw new ArgumentNullException(nameof(source2));
    if (combiner is null) throw new ArgumentNullException(nameof(combiner));

    var enumerator1 = source1.GetEnumerator();
    var enumerator2 = source2.GetEnumerator();
    while (true)
    {
        TSource1 element1;
        TSource2 element2;
        bool still1 = enumerator1.MoveNext();
        bool still2 = enumerator2.MoveNext();
        if (!still1 && !still2) yield break;
        element1 = still1 ? enumerator1.Current : default;
        element2 = still2 ? enumerator2.Current : default;

        yield return combiner(element1, element2);
    }
}
```

このようにして、`System.Linq`名前空間には存在しない自作のLINQオペレータを実装することができます。

### 追記:`ArgumentNullException`を即時スローする

<details><summary>追記：上記コードではnullチェックも遅延評価されるため、標準のLINQオペレータの挙動とは若干異なる模様。詳細はここをクリックして表示してください。</summary><div>

@laughter さんにコメント頂きました。
上記実装の`ArgumentNullException`のスローの仕方では、例えば以下のようなコードを実行しても例外がスローされないようです。


```csharp
var sq1 = Enumerable.Range(1, 10);
var sq2 = Enumerable.Range(1, 10).Select(n => n.ToString());
Func<int, string, string> nullFunc = null;

//第二引数はnullだが、例外がスローされない 
var sq3 = sq1.Combine(sq2, nullFunc);

Console.WriteLine("ここまできたよ");
```

メソッド自体が遅延評価なゆえ、上記のように最後まで評価されないケースでは、引数が`null`だとしても判定が発生しない。
（もちろん`foreach`等で評価すればきちんと例外はスローされます。）

引数に`null`が指定された時点で即例外をスローするには、オペレータの拡張メソッド自体は__nullチェックのみに特化して即時評価__されるようにし、__別途遅延評価されるオペレータのアルゴリズムが実装されたメソッドを呼び出して__`return`する必要があるようです。


```csharp
// オペレータの拡張メソッド本体。nullチェックのみに特化することで即時評価される
public static IEnumerable<TResult> Combine<TSource1, TSource2, TResult>(
    this IEnumerable<TSource1> source1,
    IEnumerable<TSource2> source2,
    Func<TSource1, TSource2, TResult> combiner)
{
    if (source1 is null) throw new ArgumentNullException(nameof(source1));
    if (source2 is null) throw new ArgumentNullException(nameof(source2));
    if (combiner is null) throw new ArgumentNullException(nameof(combiner));

    // 遅延評価されるオペレータのアルゴリズム本体
    return _Combine(source1, source2, combiner);
}

// 遅延評価されるオペレータの実装
private static IEnumerable<TResult> _Combine<TSource1, TSource2, TResult>(
    IEnumerable<TSource1> source1,
    IEnumerable<TSource2> source2,
    Func<TSource1, TSource2, TResult> combiner)
{
    var enumerator1 = source1.GetEnumerator();
    var enumerator2 = source2.GetEnumerator();
    while (true)
    {
        TSource1 element1;
        TSource2 element2;
        bool still1 = enumerator1.MoveNext();
        bool still2 = enumerator2.MoveNext();
        if (!still1 && !still2) yield break;
        element1 = still1 ? enumerator1.Current : default;
        element2 = still2 ? enumerator2.Current : default;

        yield return combiner(element1, element2);
    }
}
```

こうすれば無事に`IEnumerable<T>`が評価されなくても引数に`null`を指定した時点で`ArgumentNullException`がスローされました。

</div></details>

# `yield return`の重要性

最後に`yield return`の重要性について触れたいと思います。

この記事では当然のように`yield return`を使ってきましたが、仮に`yield return`を使わずにLINQオペレータを実装しようとするとかなり大変 or 遅延評価じゃなくなります。

## 具象クラスにすると遅延評価じゃなくなる

当然ですがオペレータ内部で`IEnumerable<T>`を具象クラスにするとその時点で即時評価となるのでNGです。

```csharp
public static IEnumerable<T> ImmediateMyWhere<T>(this IEnumerable<T> source, Func<T, bool> filter)
{
    List<T> list = new List<T>();
    foreach (var item in source)
    { //この時点で即時評価される
        if (filter(item)) list.Add(item);
    }
    return list;
}
```

## `yield return`を使わずに遅延評価な`Where`を実装する

遅延評価な`Where`を`yield return`を使わずに実装するには、下記のように専用の列挙子を実装する必要があり、かなり大変です。

```csharp
public class WhereEnumerable<T> : IEnumerable<T>
{
    private readonly Func<T, bool> filter;
    private readonly IEnumerator<T> source;

    public WhereEnumerable(IEnumerator<T> source, Func<T, bool> filter)
    {
        (this.filter, this.source) = (filter, source);
    }

    public IEnumerator<T> GetEnumerator() => new WhereEnumerator(source, filter);
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    public class WhereEnumerator : IEnumerator<T>
    {
        private readonly Func<T, bool> filter;
        private readonly IEnumerator<T> source;

        public WhereEnumerator(IEnumerator<T> source, Func<T, bool> filter)
        {
            (this.filter, this.source) = (filter, source);
        }

        public T Current => source.Current;

        object IEnumerator.Current => Current;

        public void Dispose()
        {
        }

        public bool MoveNext()
        {
            //filterの結果がtrueになるまでMoveNextを繰り返す
            do
            {
                if (!source.MoveNext()) return false;
            } while (!filter(source.Current));
            return true;
        }

        public void Reset()
        {
            throw new NotImplementedException();
        }
    }
}

```

そしてオペレータの拡張メソッドで専用の列挙子を持った`IEnumerable<T>`なクラスを`new`して返す必要があります。

```csharp
public static IEnumerable<T> NonYieldMyWhere<T>(this IEnumerable<T> source, Func<T, bool> filter)
{
    return new WhereEnumerable<T>(source.GetEnumerator(), filter);
}
```

かなーり大変です。
言い換えれば、これだけのことを`yield return`はやってくれているということがわかります。

# さいごに

Unity用ですが自作LINQオペレータいろいろ作って下記にアップしてます。
いわゆるオレオレライブラリなので使用は自己責任ですがなにかの参考になればと

https://github.com/yutorisan/UnityUtility


自作LINQメソッドの実装は楽しいですがハマりすぎると本来の作業が疎かになるので注意。。

追記：[ArgumentNullExceptionが即時スローされない件](#追記argumentnullexceptionを即時スローする)、時間を見つけて上記ライブラリも修正対応していきます。