C# の LINQ を自作してみる:Select と Where 相当の機能
こんにちは、@studio_meowtoon です。今回は、WSL の Ubuntu 環境の C# で、LINQ の機能を自作してみる方法を紹介します。
技術トピック
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 の開発環境などを紹介していきますので、ぜひお楽しみにしてください。
参考資料