LoginSignup
3
2

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

Last updated at Posted at 2023-06-02

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

こんにちは、@studio_meowtoon です。今回は、WSL の Ubuntu 環境の C# で、LINQ の機能を自作してみる方法を紹介します。
csharp_on_ubuntu.png

技術トピック

LINQ とは?

こちらを展開してご覧いただけます。

LINQ (リンク)

LINQ は、Microsoftによって開発されたプログラミング言語の機能です。

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

C# には本来厳格なコーディング規則がありますが、この記事では可読性のために、一部規則に沿わない表記方法を使用しています。ご注意ください。

開発環境

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

WSL の Ubuntu を操作していきますので macOS の方も参考にして頂けます。

WSL (Microsoft Store アプリ版) ※ こちらの関連記事からインストール方法をご確認いただけます

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

Ubuntu ※ こちらの関連記事からインストール方法をご確認いただけます

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

.NET SDK ※ こちらの関連記事からインストール方法をご確認いただけます

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

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

プロジェクトの作成

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

Select について

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

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 } }
    },
});
// 以下、何組かのバンドの情報を設定
コードの全体を表示します。
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
    }
}

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

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

/// <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;
    }
}

出力結果

John Lennon
Paul McCartney
George Harrison
Ringo Starr

LINQ で処理

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

/// <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));
}

出力結果

John Lennon
Paul McCartney
George Harrison
Ringo Starr

ここまでのまとめ

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

Where について

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

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

/// <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;
    }
}

出力結果

Paul McCartney

LINQ で処理

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

/// <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));
}

出力結果

Paul McCartney

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

ここまでのまとめ

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

拡張メソッドについて

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

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

Select の簡易実装

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

/// <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 を自作しています。

/// <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));
}
コードの全体を表示します。
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);
            }
        }
    }
}

出力結果

John Lennon
Paul McCartney
George Harrison
Ringo Starr

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

Where の簡易実装

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

/// <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 を自作しています。

/// <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));
}
コードの全体を表示します。
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;
        }
    }
}

出力結果

Paul McCartney

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

おまけ

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

/// <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));
}
コードの全体を表示します。
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;
                }
            }
        }
    }
}

出力結果

Paul McCartney
Bill Wyman
Pete Quaife
John Entwistle

まとめ

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

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

参考資料

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2