C# の LINQ を自作してみる：Select と Where 相当の機能

# C# の LINQ を自作してみる：Select と Where 相当の機能

こんにちは、@studio_meowtoon です。今回は、WSL の Ubuntu 環境の C# で、LINQ の機能を自作してみる方法を紹介します。  
![csharp_on_ubuntu.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/23c7c71f-e5f1-3bb8-45b6-60ad19e41238.png)

## 技術トピック

### LINQ とは？  

<details>
<summary>こちらを展開してご覧いただけます。</summary>

### [LINQ (リンク)](https://docs.microsoft.com/ja-jp/dotnet/csharp/linq/)
LINQ は、Microsoftによって開発されたプログラミング言語の機能です。

:::note info

|特徴|
|:--|
|LINQ を使用すると、データのクエリや操作を行うための統一された記述方法を提供します。|
|LINQ を使うことで、コレクション (リストや配列など) やデータベース、XML などさまざまなデータソースからデータを抽出、フィルタリング、ソート、変換などの操作が簡単に行えます。|
|LINQ は C# をはじめとする .NET 言語ファミリーで広く使用されており、データベースクエリ言語 (SQL) のような独自の構文を学ぶ必要がなくなります。|
|LINQ は型安全で、コンパイル時にエラーを検出できるため、安全かつ効率的なデータ操作が可能です。|

:::
</details>

:::note alert
C# には本来[厳格なコーディング規則](https://learn.microsoft.com/ja-jp/dotnet/csharp/fundamentals/coding-style/coding-conventions)がありますが、この記事では可読性のために、一部規則に沿わない表記方法を使用しています。ご注意ください。
:::

## 開発環境

+ Windows 11 Home 22H2 を使用しています。  

:::note info
[WSL](https://learn.microsoft.com/ja-jp/windows/wsl/about) の Ubuntu を操作していきますので macOS の方も参考にして頂けます。  
:::

<details>

WSL (Microsoft Store アプリ版) [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/2aaa6579cb8e4fa1d874)

```sh
> wsl --version
WSL バージョン: 1.0.3.0
カーネル バージョン: 5.15.79.1
WSLg バージョン: 1.0.47
```

Ubuntu [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/2aaa6579cb8e4fa1d874)

```sh
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.1 LTS
Release:        22.04
```

.NET SDK [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/d68031c91462f6808dff)

```sh
$ dotnet --list-sdks
7.0.202 [/usr/share/dotnet/sdk]
$ dotnet --version
7.0.202
```

</details>

:::note warn
この記事では基本的に Ubuntu のターミナルで操作を行います。**Vim を使用してコピペする方法**を初めて学ぶ人のために、以下の記事で手順を紹介しています。ぜひ挑戦してみてください。
:::

https://qiita.com/studio_meowtoon/items/7771ad48676926e41463

### プロジェクトの作成

:::note warn
.NET 環境をお持ちでない場合は、以下の関連記事から .NET SDK のインストール手順をご確認いただけます。
:::

https://qiita.com/studio_meowtoon/items/d68031c91462f6808dff

## Select について

まず、あるデータから値を取り出す処理について考えてみます。
このようなデータ構造から任意の情報を出力に表示することを考えます。

```csharp
List<Band> _band_list.Add(item: new() {
    Name = "The Beatles", Nation = UnitedKingdom,
    Period = new() { Begin = Parse("1960/01/01"), End = Parse("1970/12/31") }, Term = 1,
    Member = new() {
        new() { FirstName = "John", LastName = "Lennon", Gender = Male, Born = Parse("1940/10/09"), Role = new[] { Vocal, Guitar } },
        new() { FirstName = "Paul", LastName = "McCartney", Gender = Male, Born = Parse("1942/06/18"), Role = new[] { Vocal, Bass } },
        new() { FirstName = "George", LastName = "Harrison", Gender = Male, Born = Parse("1943/02/25"), Role = new[] { Vocal, Guitar } },
        new() { FirstName = "Ringo", LastName = "Starr", Gender = Male, Born = Parse("1940/07/07"), Role = new[] { Vocal, Drums } }
    },
});
_band_list.Add(item: new() {
    Name = "The Rolling Stones", Nation = UnitedKingdom,
    Period = new() { Begin = Parse("1962/01/01"), End = Parse("1969/12/31") }, Term = 1,
    Member = new() {
        new() { FirstName = "Brian", LastName = "Jones", Gender = Male, Born = Parse("1942/02/28"), Role = new[] { Guitar } },
        new() { FirstName = "Mick", LastName = "Jagger", Gender = Male, Born = Parse("1943/07/26"), Role = new[] { Vocal } },
        new() { FirstName = "Keith", LastName = "Richards", Gender = Male, Born = Parse("1943/12/18"), Role = new[] { Guitar } },
        new() { FirstName = "Bill", LastName = "Wyman", Gender = Male, Born = Parse("1936/10/24"), Role = new[] { Bass } },
        new() { FirstName = "Charlie", LastName = "Watts", Gender = Male, Born = Parse("1941/06/02"), Role = new[] { Drums } }
    },
});
// 以下、何組かのバンドの情報を設定
```

<details>
<summary>コードの全体を表示します。</summary>

```csharp
using System;
using System.Collections.Generic;
using static System.DateTime;

using static Common.Gender;
using static Common.Nation;
using static Common.Role;

namespace Common {
    public static class Data {

        static List<Band> _band_list;

        /// <summary>
        /// 全てのバンドを取得します
        /// </summary>
        public static List<Band> AllBand {
            get {
                if (_band_list is null) { _band_list = new(); create(); } 
                return _band_list;
            }
        }

        /// <summary>
        /// バンドのデータを生成します
        /// </summary>
        static void create() {
            _band_list.Add(item: new() {
                Name = "The Beatles", Nation = UnitedKingdom,
                Period = new() { Begin = Parse("1960/01/01"), End = Parse("1970/12/31") }, Term = 1,
                Member = new() {
                    new() { FirstName = "John", LastName = "Lennon", Gender = Male, Born = Parse("1940/10/09"), Role = new[] { Vocal, Guitar } },
                    new() { FirstName = "Paul", LastName = "McCartney", Gender = Male, Born = Parse("1942/06/18"), Role = new[] { Vocal, Bass } },
                    new() { FirstName = "George", LastName = "Harrison", Gender = Male, Born = Parse("1943/02/25"), Role = new[] { Vocal, Guitar } },
                    new() { FirstName = "Ringo", LastName = "Starr", Gender = Male, Born = Parse("1940/07/07"), Role = new[] { Vocal, Drums } }
                },
            });
            _band_list.Add(item: new() {
                Name = "The Rolling Stones", Nation = UnitedKingdom,
                Period = new() { Begin = Parse("1962/01/01"), End = Parse("1969/12/31") }, Term = 1,
                Member = new() {
                    new() { FirstName = "Brian", LastName = "Jones", Gender = Male, Born = Parse("1942/02/28"), Role = new[] { Guitar } },
                    new() { FirstName = "Mick", LastName = "Jagger", Gender = Male, Born = Parse("1943/07/26"), Role = new[] { Vocal } },
                    new() { FirstName = "Keith", LastName = "Richards", Gender = Male, Born = Parse("1943/12/18"), Role = new[] { Guitar } },
                    new() { FirstName = "Bill", LastName = "Wyman", Gender = Male, Born = Parse("1936/10/24"), Role = new[] { Bass } },
                    new() { FirstName = "Charlie", LastName = "Watts", Gender = Male, Born = Parse("1941/06/02"), Role = new[] { Drums } }
                },
            });
            _band_list.Add(item: new() {
                Name = "The Kinks", Nation = UnitedKingdom,
                Period = new() { Begin = Parse("1964/01/01"), End = Parse("1969/12/31") }, Term = 1,
                Member = new() {
                    new() { FirstName = "Ray", LastName = "Davies", Gender = Male, Born = Parse("1944/06/21"), Role = new[] { Vocal, Guitar } },
                    new() { FirstName = "Dave", LastName = "Davies", Gender = Male, Born = Parse("1947/02/03"), Role = new[] { Vocal, Guitar } },
                    new() { FirstName = "Pete", LastName = "Quaife", Gender = Male, Born = Parse("1943/12/31"), Role = new[] { Bass } },
                    new() { FirstName = "Mick", LastName = "Avory", Gender = Male, Born = Parse("1944/02/15"), Role = new[] { Drums } }
                },
            });
            _band_list.Add(item: new() {
                Name = "The Who", Nation = UnitedKingdom,
                Period = new() { Begin = Parse("1964/01/01"), End = Parse("1978/12/31") }, Term = 1,
                Member = new() {
                    new() { FirstName = "Roger", LastName = "Daltrey", Gender = Male, Born = Parse("1944/03/01"), Role = new[] { Vocal } },
                    new() { FirstName = "Pete", LastName = "Townshend", Gender = Male, Born = Parse("1945/05/19"), Role = new[] { Vocal, Guitar } },
                    new() { FirstName = "John", LastName = "Entwistle", Gender = Male, Born = Parse("1944/10/09"), Role = new[] { Vocal, Bass } },
                    new() { FirstName = "Keith", LastName = "Moon", Gender = Male, Born = Parse("1946/08/23"), Role = new[] { Drums } }
                },
            });
        }
    }

    /// <summary>
    /// パーソン
    /// </summary>
    public record Person {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public Gender Gender { get; set; }
        public DateTime Born { get; set; }
        public string FullName { get => $"{FirstName} {LastName}"; }
    }

    /// <summary>
    /// バンドメンバー
    /// </summary>
    public record BandMan : Person {
        public Role[] Role { get; set; }
    }

    /// <summary>
    /// バンド
    /// </summary>
    public record Band {
        public string Name { get; set; }
        public Nation Nation { get; set; }
        public Period Period { get; set; }
        public int Term { get; set; }
        public List<BandMan> Member { get; set; }
    }

    /// <summary>
    /// 期間
    /// </summary>
    public record Period {
        public DateTime Begin { get; set; }
        public DateTime End { get; set; }
    }

    /// <summary>
    /// 性別
    /// </summary>
    public enum Gender {
        Male,
        Femail
    }

    /// <summary>
    /// 役割
    /// </summary>
    public enum Role {
        Vocal,
        Chorus,
        Guitar,
        Bass,
        Keyboard,
        Drums,
        Percussion
    }

    /// <summary>
    /// 国籍
    /// </summary>
    public enum Nation {
        UnitedKingdom,
        UnitedStates
    }
}
```

</details>

### データの最初のバンドの全メンバーを表示してみます

初めにベタな foreach 文で書いてみます。

```csharp
/// <summary>
/// データの最初のバンドの全メンバーを表示
/// </summary>
public static void Select1() {
    List<Band> list = Data.AllBand;
    foreach (Band band in list) {
        foreach (BandMan band_man in band.Member) {
            WriteLine(band_man.FullName);
        }
        break;
    }
}
```

出力結果

```sh
John Lennon
Paul McCartney
George Harrison
Ringo Starr
```

### LINQ で処理

LINQ で同様の処理を記述してみます。

```csharp
/// <summary>
/// データの最初のバンドの全メンバーを表示
/// </summary>
public static void Select1() {
    List<Band> list = Data.AllBand;
    List<BandMan> member_list = list.Select(x => x.Member).First();
    member_list.ForEach(x => WriteLine(x.FullName));
}
```

出力結果

```sh
John Lennon
Paul McCartney
George Harrison
Ringo Starr
```

### ここまでのまとめ

LINQ を使用してベタな foreach 文を置き換えることが出来ました。

## Where について

### 次にデータの最初のバンドのベーシストを表示してみます。

初めにベタな foreach 文と if 文で書いてみます。

```csharp
/// <summary>
/// データの最初のバンドのベーシストを表示
/// </summary>
public static void Where1() {
    List<Band> list = Data.AllBand;
    foreach (Band band in list) {
        foreach (BandMan band_man in band.Member)
            foreach (Role role in band_man.Role)
                if (role.ToString().Contains(Role.Bass.ToString()))
                    WriteLine(band_man.FullName);
        break;
    }
}
```

出力結果

```sh
Paul McCartney
```

### LINQ で処理

LINQ で同様の処理を記述してみます。

```csharp
/// <summary>
/// データの最初のバンドのベーシストを表示
/// </summary>
public static void Where1() {
    List<Band> list = Data.AllBand;
    List<BandMan> member_list = list.Select(x => x.Member).First()
        .Where(x => x.Role.Contains(Role.Bass)).ToList();
    member_list.ForEach(x => WriteLine(x.FullName));
}
```

出力結果

```sh
Paul McCartney
```

LINQ を使用してベタな foreach 文と if 文を置き換えることが出来ました。

## ここまでのまとめ

**LINQ** を使うことにより **foreach(for) 文と if 文を使用せず** データから値を取り出すことが出来ました。

## 拡張メソッドについて

:::note info
ところで LINQ の Select メソッド、Where メソッド は何をしているのでしょうか？ 🤔
ここから C# の機能 拡張メソッド を使用して  LINQ の Select メソッド、Where メソッド相当の機能を自作してみます😋
:::

> 拡張メソッドを使用すると、新規の派生型の作成、再コンパイル、または元の型の変更を行うことなく既存の型にメソッドを "追加" できます。 拡張メソッドは静的メソッドですが、拡張された型のインスタンス メソッドのように呼び出します。

## Select の簡易実装

それでは早速 自作の Select メソッドを作成してみましょう。

```csharp
/// <summary>
/// Select の簡易実装
/// </summary>
public static IEnumerable<TResult> xSelect<TSource, TResult>(
    this IEnumerable<TSource> source, Func<TSource, TResult> selector) {
    foreach (TSource item in source) {
        yield return selector(item);
    }
}
```

純正 LINQ の時と同じように実行してみます。
※ xSelect, xFirst, xForEach を自作しています。

```csharp
/// <summary>
/// データの最初のバンドの全メンバーを表示
/// </summary>
public static void Select1() {
    List<Band> list = Data.AllBand;
    List<BandMan> member_list = list.xSelect(x => x.Member).xFirst();
    member_list.xForEach(x => WriteLine(x.FullName));
}
```

<details>
<summary>コードの全体を表示します。</summary>

```csharp
using System;
using System.Collections.Generic;

namespace OwnImplementation {
    public static class Extensions {

        /// <summary>
        /// Select の簡易実装
        /// </summary>
        public static IEnumerable<TResult> xSelect<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector) {
            foreach (TSource item in source) {
                yield return selector(item);
            }
        }

        /// <summary>
        /// First の簡易実装
        /// </summary>
        public static TSource xFirst<TSource>(this IEnumerable<TSource> source) {
            foreach (TSource item in source) {
                return item;
            }
            return default(TSource);
        }

        /// <summary>
        /// ForEach の簡易実装
        /// </summary>
        public static void xForEach<TSource>(this IEnumerable<TSource> source, Action<TSource> action) {
            foreach (TSource item in source) {
                action(item);
            }
        }
    }
}
```

</details>

出力結果

```sh
John Lennon
Paul McCartney
George Harrison
Ringo Starr
```

foreach 文が自作の xSelect メソッドの中に書かれているのが分かります。
純正 LINQ の Select メソッドと同じ出力を得ることが出来ました。

## Where の簡易実装

次に自作の Where メソッドを作成してみましょう

```csharp
/// <summary>
/// Where の簡易実装
/// </summary>
public static IEnumerable<TSource> xWhere<TSource>(
    this IEnumerable<TSource> source, Func<TSource, bool> predicate) {
    foreach (TSource item in source) {
        if (predicate(item)) {
            yield return item;
        }
    }
}
```

純正 LINQ の時と同じように実行してみます。  
※ さらに xWhere, xContains, xToList を自作しています。

```csharp
/// <summary>
/// データの最初のバンドのベーシストを表示
/// </summary>
public static void Where1() {
    List<Band> list = Data.AllBand;
    List<BandMan> member_list = list.xSelect(x => x.Member).xFirst()
        .xWhere(x => x.Role.xContains(Role.Bass)).xToList();
    member_list.xForEach(x => WriteLine(x.FullName));
}
```

<details>
<summary>コードの全体を表示します。</summary>

```csharp
using System;
using System.Collections.Generic;

namespace OwnImplementation {
    public static class Extensions {

        /// <summary>
        /// Where の簡易実装
        /// </summary>
        public static IEnumerable<TSource> xWhere<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) {
            foreach (TSource item in source) {
                if (predicate(item)) {
                    yield return item;
                }
            }
        }

        /// <summary>
        /// ToList の簡易実装
        /// </summary>
        public static List<TSource> xToList<TSource>(this IEnumerable<TSource> source) {
            return new List<TSource>(source);
        }

        /// <summary>
        /// Contains の簡易実装
        /// </summary>
        public static bool xContains<TSource>(this IEnumerable<TSource> source, TSource value) {
            foreach (TSource item in source) {
                if (EqualityComparer<TSource>.Default.Equals(item, value)) {
                    return true;
                }
            }
            return false;
        }
    }
}
```

</details>

出力結果

```sh
Paul McCartney
```

foreach 文と if 文が自作の xWhere メソッドの中に書かれているのが分かります。
純正 LINQ の Where メソッドと同じ出力を得ることが出来ました。

## おまけ

歴史的バンドの中からベーシストを列記してみます！
※ さらに xSelectMany を自作しています。

```csharp
/// <summary>
/// データの全てのバンドのベーシストだけを表示
/// </summary>
public static void Where2() {
    List<Band> list = Data.AllBand;
    List<BandMan> member_list = list.xSelectMany(x => x.Member).xToList()
        .xWhere(x => x.Role.xContains(Role.Bass)).xToList(); ;
    member_list.xForEach(x => WriteLine(x.FullName));
}
```

<details>
<summary>コードの全体を表示します。</summary>

```csharp
using System;
using System.Collections.Generic;

namespace OwnImplementation {
    public static class Extensions {

        /// <summary>
        /// SelectMany の簡易実装
        /// </summary>
        public static IEnumerable<TResult> xSelectMany<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, IEnumerable<TResult>> selector) {
            foreach (TSource item in source) {
                foreach (TResult result in selector(item)) {
                    yield return result;
                }
            }
        }
    }
}
```

</details>

出力結果

```sh
Paul McCartney
Bill Wyman
Pete Quaife
John Entwistle
```

## まとめ

+ 拡張メソッドの中に foreach 文と if 文実装することにより、それら**制御構文を内部に隠蔽**することが出来ました。
+ そのメソッドを利用する側は、それら制御構文を意識せず**処理の内容にフォーカス**することが出来ました。
+ 拡張メソッドを記述することにより LINQ の Select と Where 相当の機能を実装してみることが出来ました。

どうでしたか？ Window 11 の WSL Ubuntu に、C# / .NET の開発環境を手軽に構築することができます。ぜひお試しください。今後も .NET の開発環境などを紹介していきますので、ぜひお楽しみにしてください。

## 参考資料

https://source.dot.net/