まえがき
久しぶりの投稿です。しばらく何も書いてなかったのは、やる気が出なかったお仕事が忙しかったからです。
Select
に関して書こうと思ったけど面倒くさくなって書くことがなく、とりあえずLINQ全体についてダラダラ書こうと思ってたら、なんだか長くなってしまいました。
定期的に見直して、書き直す予定です。(予定は未定)
LINQですよ、LINQ!
System.Core.dllのSystem.Linq
名前空間の Enumalable
クラスにあるメソッドの解説を並び立てる記事です。対象 は .Net Framework 4.7.2です。なので、SkipLast
とTakeLast
は対象外です。
SkipLast
TakeLast
今回説明に使うデータ
アイドルマスターです。わっほ~い
// アイドルの名前だけを読み取り専用で提供するインターフェース
interface IIDOLName
{
string Name { get; }
}
// アイドルちゃん
class IDOL : IIDOLName
{
// 名前
public string Name { get; set; }
// 読み仮名
public string Phonetic { get; set; }
// 年齢
public int Age { get; set; }
// 身長
public double Height { get; set; }
// 体重
public double Weight { get; set; }
// バスト
public double Bust { get; set; }
// ウエスト
public double Waist { get; set; }
// ヒップ
public double Hip { get; set; }
// 作品ID
public int WorkID { get; set; }
// 所属しているユニットの一覧
public List<string> Unit { get; set; }
}
// アイマスキャラ277人分のデータが入ってます。(アイドル+トレーナー+事務員)
// 公開されてないパラメータは0埋めです。
List<IDOL> IDOLList;
分類
独断と偏見で10種類に分類しました。
- 射影系
- フィルタ系
- 探索系
- 算術系
- 単体取得系
- 変換系
- ソート系
- 範囲取得系
- 結合系
- その他
射影系
とりあえずMicrosoftの公式ドキュメントから説明を引っ張ってきました。
射影操作 (C#)
射影とは、オブジェクトを、必要なプロパティだけで構成された別の形式に変換する操作のことをいいます。
つまり、「もとのデータ」を「今使いたいデータ」に変換して取り出すメソッド達です。
Select
// 特定のプロパティを取り出したり
var bust = IDOLList.Select(x => x.Bust);
// 計算した結果を取り出したり
var bmi = IDOLList.Select(x => x.Weight / x.Height / x.Height);
// タプルで取り出したり
var namePhone = IDOLList.Select(x => (x.Name, x.Phonetic));
// ラムダ式じゃなくても大丈夫
var nonelambdaName = IDOLList.Select(GetName);
// インデックス付きのSelect
var indexIdol = IDOLList.Select((x,i) => (i, x));
// もとのデータ完全を無視も一応できる(ムダだけど)
var muda = IDOLList.Select(x => "0");
string GetName(IDOL x)
{
return x.Name;
}
射影系の中でも一番シンプルなやつです。そして、シンプルだけに一番人気候補です。
Microsoftも分かってるようで、Select
と、もう一つの一番人気候補Where
に関してはパフォーマンスのための最適化がかなりかかっています。
SelectMany
// 所属しているユニットが順番に結合される
var fullUnit = IDOLList.SelectMany(x => x.Unit);
// 名前に使われてる文字をすべて引っ張りだす
var nameChar = IDOLList.SelectMany(x => x.Name.ToList());
少しわかりにくいメソッドですが、やっている事はそんなに難しくないです。
もとのデータから新たなIEnumerable
を取り出し、それを1つに結合します。
Cast
// IIDOLNameインターフェイスにキャストされる
var castIF = IDOLList.Cast<IIDOLName>();
射影系なのかどうかと聞かれると、よくわかりませんが、他に入れるところも思いつかなかったので入れちゃいました。
各データに対して、ジェネリックで渡された方にキャストします。が、後述のOfType
と異なりキャストに失敗すると例外を吐くので、あまり使われてない気がするメソッドです。
フィルタ系
読んで字のごとく、フィルタリングするメソッド達です。
Where
// 特定のプロパティでフィルタリングしたり
var higher = IDOLList.Where(x => x.Height > 160);
// 計算結果でフィルタリングしたり
var noData = IDOLList.Where(x => x.Height * x.Weight == 0);
// ラムダ式じゃなくても大丈夫
var nonelambdaAmami = IDOLList.Where(IsHaruka);
// インデックス付きのWhere
var oddIdol = IDOLList.Where((x,i) => i % 2 == 0);
// もとのデータ完全を無視も一応できる(ムダだけど)
var muda = IDOLList.Where(x => true);
string IsHaruka(IDOL x)
{
return x.Name == "天海 春香";
}
フィルタ系の基本です。データリストに対して何か処理をする前に不都合なデータを弾くために使うことが多いと思います。
// 身長が0だと例外が発生するので
var bmiException = IDOLList.Select(x => x.Weight / x.Height / x.Height);
// 事前に取り除くことで対応する
var bmi = IDOLList.Where(x => x.Height != 0).Select(x => x.Weight / x.Height / x.Height);
こいつもかなり最適化されているので、興味がある人は昔書いたやつを読んでみてください。
OfType
// IIDOLNameインターフェイスにキャストされる
var castIF = IDOLList.OfType<IIDOLName>();
各データに対して、ジェネリックで渡された方にキャストします。が、前述のCast
と異なりキャストに失敗した要素はスキップされるので、使い勝手が良いメソッドです。
メソッドの実行後に全体のデータ数が減るので、フィルタ系ってことになってます(僕の中では)。
Distinct
// 特に指定しなければ規定の比較
var uniqueNameChar = IDOLList.SelectMany(x => x.Name.ToList()).Distinct();
// 自作の比較を指定することも可能(年齢の被りなしで取り出す)
var originalCompare = IDOLList.Distinct(new IdolCompare());
class IdolCompare : IEqualityComparer<IDOL>
{
public bool Equals(IDOL x, IDOL y)
{
return x.Age == y.Age;
}
public int GetHashCode(IDOL obj)
{
return obj.Age.GetHashCode();
}
}
重複削除メソッドです。組み込み型だったら特に何も考えなくて良いんですが、自作クラスの重複削除だとちょっと面倒です。
- 自作クラスに
IEquatable
インターフェイスを実装する。 - 外部から
IEqualityComparer
インターフェイスを渡す。
1の方が実装が1つにまとまるので良いと思います( こんな感じ )が、必ずしも自分で弄れるクラスばかりではないので2の出番もあるかと思います。(他にも方法はあるんですかね?僕は知りません。)
※追記(コメントにて頂いたアドバイス)
// アイドルちゃんを少し改造
class IDOL : IIDOLName
{
/*
既存プロパティ類は省略
*/
// IEqualityComparerを渡す読み取り専用プロパティ
public static IEqualityComparer<IDOL> ComparerName => new EqualityrName();
public static IEqualityComparer<IDOL> ComparerAge => new EqualityrAge();
// IEqualityComparerを継承しているクラス
class EqualityrName : IEqualityComparer<IDOL>
{
public bool Equals(IDOL x, IDOL y)
{
return x.Name == y.Name;
}
public int GetHashCode(IDOL obj)
{
return obj.Name.GetHashCode();
}
}
class EqualityrAge : IEqualityComparer<IDOL>
{
public bool Equals(IDOL x, IDOL y)
{
return x.Age == y.Age;
}
public int GetHashCode(IDOL obj)
{
return obj.Age.GetHashCode();
}
}
}
// 年齢で比較可能
var compareAge = IDOLList.Distinct(IDOL.ComparerAge);
「IEqualityComparer
を提供するプロパティ」と「IEqualityComparer
を継承しているクラス」を元のクラス内に定義すると、実装を1つにまとめつつ、複数の比較方法を使い分けられます。(これ以降のメソッドも同様です。)
また、タプルや匿名型等の比較を行いたい場合、IEquatable
インターフェイスを実装できないので、IEqualityComparer
を使用する事になります。
判定系
指定した条件を満たしていたらture
、そうでなければfalse
を返すメソッド達です。
Any
// 条件を満たしているので、true
var isOppai = IDOLList.Any(x => x.Bust >= 100);
// データが有るのでtrue
var anyData = IDOLList.Any();
データリスト内に、指定した条件を満たすものが1つでもあればtrue
、1つもなければfalse
です。
条件を指定しない場合、何でも良いから要素が1つでもあればtrue
です。
Count() == 0
とかやっちゃうとLINQ警察が飛んできて、こいつを投げつけてきます。
All
// 条件を満たしていないので、false
var isOppai = IDOLList.All(x => x.Bust >= 100);
// これは存在しない
//var allData = IDOLList.All();
データリスト内の、すべてが指定の条件を満たすせばtrue
、1つでも満たさなければfalse
です。
Any
と違って条件なしはありません。「なんでも良いから要素が全て入っている」→意味がわからないので。
Contains
var blankIDOL = new IDOL();
IDOLList.Add(blankIDOL);
// 同一のインスタンスなのでture
var isBlank1 = IDOLList.Contains(blankIDOL);
// 別のインスタンスなのでfalse
var isBlank2 = IDOLList.Contains(new IDOL());
// IEqualityComparerが指定されているので、名前で比較できる。よって、true
var isBlank3 = IDOLList.Contains(new IDOL(), new IdolCompare());
class IdolCompare : IEqualityComparer<IDOL>
{
public bool Equals(IDOL x, IDOL y)
{
return x.Name == y.Name;
}
public int GetHashCode(IDOL obj)
{
return obj.Name.GetHashCode();
}
}
データリスト内に、引数で渡したデータと同一の物があれば、true
、なければfalse
です。
Distinct
と同様で、自作クラスの判定だとちょっと面倒です。isBlank1
とisBlank3
はtrue
ですが、isBlank2
はfalse
になります。
基本的にはAny
の方が使い勝手が良いですが、ラムダ式の中で呼ぶ時にこっちを使うことがあります。
// ラムダ式の中にラムダ式だと視認性が良くない
var newGenerationsAny = IDOLList.Where(x => x.Unit.Any(y => y == "ニュージェネレーション"));
// わりとスッキリ
var newGenerationsContains = IDOLList.Where(x => x.Unit.Contains("ニュージェネレーション"));
SequenceEqual
// IDOLListには、渋谷 凛、神谷 奈緒、北条 加蓮の順に入っているものとする。
var triadPrimus = IDOLList.Where(x => x.Unit.Contains("トライアドプリムス"));
var siburin = new IDOL{Name = "渋谷 凛"};
var kamiyan = new IDOL{Name = "神谷 奈緒"};
var karen = new IDOL{Name = "北条 加蓮"};
var torapuri = new List<IDOL>
siburin,
kamiyan,
karen,
};
// 2つのシーケンスが等しいか、順番に確認
// こっちはfalse(中身が別のインスタンス)
var isTriadPrimus1 = triadPrimus.SequenceEqual(torapuri);
// こっちはtrue(IEqualityComparerによりNameで比較される)
var isTriadPrimus2 = triadPrimus.SequenceEqual(torapuri, new IdolCompare());
class IdolCompare : IEqualityComparer<IDOL>
{
public bool Equals(IDOL x, IDOL y)
{
return x.Name == y.Name;
}
public int GetHashCode(IDOL obj)
{
return obj.Name.GetHashCode();
}
}
今までのとは少し違います。2つのデータリストに対して要素を順番に比較し、全て一致していたらtrue
、そうでなければfalse
です。
意外と使い勝手の良いメソッドで、2つのデータリストの型が違っても比較可能です。(今回の例だとIEnumarable
とList
。)
ただし、自作クラスの判定だとやっぱりちょっと面倒です。
算術系
いろいろな計算をしてくれるメソッド達です(適当)。
Average
// 単純な平均を求める
// 17.1でした
var ageAve = IDOLList.Average(x => x.Age);
// フィルタリングしてからやってもよし
// 80.9でした
var bustAve = IDOLList.Where(x => x.Bust != 0).Average(x => x.Bust);
平均値を求めます。
こいつに限った話でないですが、算術系は大量のオーバーロードがあります。(Average
だけで20個)
とは言っても、
int
long
float
double
decimal
これら5種類の変数と、それぞれのnull許容型(?がつくやつ)、およびその両方のラムダ式に対応したメソッドなので、特にビビる必要はないです。
Sum
// 単純な合計を求める
// 43098.2でした
var heightSum = IDOLList.Sum(x => x.Height);
// 計算してからやってもよし
// 6649でした
var aboutUnderBustSum = IDOLList.Select(x => x.Bust - x.Waist).Sum();
合計を求めます。
こいつもオーバーロードが20個あります。内訳はAverage
と同様です。
Max
// 最大値を求める
// 93でした
var hipMax = IDOLList.Max(x => x.Hip);
// フィルタリングしてからやってもよし
// 86でした
var undeJCBust = IDOLList.Where(x => x.Age < 16).Max(x => x.Bust);
最大値を求めます。
こいつはオーバーロードが22個あります。内20個は、Average
と同様です。
// これでは年齢しか取れない
var maxAge = IDOLList.Max(x => x.Age);
// 特に何もしない場合、例外が発生
var max = IDOLList.Max();
// 最高齢の人の名前と年齢が同時に取れる
var maxAgeIDOL = IDOLList.Select(x => new IDOLAge(x)).Max();
// 高橋 礼子,31
Console.WriteLine($"{d.Name},{d.Age}");
// 比較用のクラスを作成
class IDOLAge : IComparable<IDOLAge>
{
public string Name{ get; set; }
public int Age{ get; set; }
public IDOLAge(IDOL x)
{
Name = x.Name;
Age = x.Age;
}
public int CompareTo(IDOLAge other)
{
return Age - other.Age;
}
}
残り2つの内の1つです。(もう1つはラムダ式を用いて、これと同じものを作成するメソッド)
Average
で上げた5種類の変数以外のリストでも、Max
とMin
は使用可能です。ただし、そのままでは実行時に例外を吐いて死にます。
データリストの中身がIComparable
かIComparable<T>
を継承している場合、比較が可能です。
一番大きい値を取得しつつ、他のパラメータも参照したい時にこの方法が使えます。
Min
// 最小値を求める
// 0でした(データが入ってない人がいるので)
var waistMin = IDOLList.Min(x => x.Waist);
// フィルタリングして計算してからやってもよし
// 14.9でした
var bmiMin = IDOLList.Where(x => x.Weight * x.Height != 0).Select(x => x.Weight / x.Height / x.Height).Min();
最小値を求めます。
こいつもオーバーロードが22個あります。内訳はMax
と同様です。
Count, LongCount
// 要素数の合計を求める
var idolCount = IDOLList.Count();
// 条件の指定も可能
var jsCount = IDOLList.Count(x => x.Age < 13);
// 要素数を求める
var idolLongCount = IDOLList.LongCount();
// 条件の指定も可能
var jkLongCount = IDOLList.LongCount(x => 15 < x.Age && x.Age < 19);
要素数の合計を求めます。条件を指定することも可能です。
2つともほぼ同じなので、基本はCount
で十分です。その名の通りLongCount
は戻りがLong
なので、アホみたいに大きなデータの塊を扱う場合のみこっちを使ってください。
Aggregate
// 年齢の合計を求める→Sum
var sumAge = IDOLList.Select(x = >x.Age).Aggregate((x, y) => x + y);
// 要素数を求める→Count
var countIdol = IDOLList.Aggregate(0, (x, y) => x + 1);
// 身長の平均を求める→Average
var aveHeight = IDOLList.Select(x => x.Height)
.Aggregate(
(height: 0.0, count: 0),
(x, y) => x = (x.height + y, x.count + 1),
z => z.height / z.count);
今まで出てきた算術系メソッドは(理論上)こいつですべて置き換え可能(なはず)です。ただし、面倒。
少し複雑なので、しっかり解説したいと思います。
オーバーロードは全部で3つです。(上の3つの例と対応してます。)
TSource Aggregate<TSource>(this IEnumerable<TSource> source, Func<TSource, TSource, TSource> func)
TAccumulate Aggregate<TSource, TAccumulate>(this IEnumerable<TSource> source, TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> func)
TResult Aggregate<TSource, TAccumulate, TResult>(this IEnumerable<TSource> source, TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> func, Func<TAccumulate, TResult> resultSelector)
その1
まずは1つ目から。
// 年齢を取り出して合計を求める
var ageSum = IDOLList.Select(x = >x.Age).Aggregate((x, y) => x + y);
// 中身はこんな感じです
public static TSource Aggregate<TSource>(this IEnumerable<TSource> source, Func<TSource, TSource, TSource> func)
{
if (source == null)
{
throw Error.ArgumentNull("source");
}
if (func == null)
{
throw Error.ArgumentNull("func");
}
using (IEnumerator<TSource> enumerator = source.GetEnumerator())
{
if (!enumerator.MoveNext())
{
throw Error.NoElements();
}
// 初期値として1つ目のデータを設定
TSource val = enumerator.Current;
while (enumerator.MoveNext())
{
// 今回の場合、valがx,enumerator.Currentがyにあたる
val = func(val, enumerator.Current);
}
return val;
}
}
Aggregate
の実装内容を引っ張り出してきました。
**「Aggregate
の引数で指定するラムダ式」は「引数が2つ」**です。
ややこしい日本語ですが、Aggregate((x, y) => x + y)
のx
とy
の事です。
x
はいままで計算してきた結果、y
は各要素の値です。
分かりづらいので、置き換えてみます。
while (enumerator.MoveNext())
{
// ここの部分が指定したメソッドに置き換わる
val = func(val, enumerator.Current);
}
return val;
↓
// (x, y) => x + yと置き換えると、valがx,enumerator.Currentがyなので
while (enumerator.MoveNext())
{
val = val + enumerator.Current
}
return val;
こうして合計を求める事ができました。
また、戻り値の型はデータリストの要素の型です。(今回はSelect
で取り出したdouble
)
その2
次は2つ目。
// 要素数を求める
var count = IDOLList.Aggregate(0, (x, y) => x + 1);
// 中身はこんな感じです
public static TAccumulate Aggregate<TSource, TAccumulate>(this IEnumerable<TSource> source, TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> func)
{
if (source == null)
{
throw Error.ArgumentNull("source");
}
if (func == null)
{
throw Error.ArgumentNull("func");
}
// 初期値が指定した値になる
TAccumulate val = seed;
foreach (TSource item in source)
{
// 今回の場合、valがx,itemがyにあたる
val = func(val, item);
}
return val;
}
1つ目ほぼ同じですが、初期値を指定することができます。
なので今回のケースで置き換えると、
// ここで初期値を設定し
TAccumulate val = seed;
foreach (TSource item in source)
{
// 指定したメソッドをぶん回す
val = func(val, item);
}
return val;
↓
// 初期値は0、メソッドは(x, y) => x + 1なので、
TAccumulate val = 0;
foreach (TSource item in source)
{
val = val + 1
}
return val;
ここで少し注意事項ですが、Aggregate
の第2引数のラムダ式は、1つ目の引数は初期値と同じ型で、2つ目の引数はデータリストの要素と同じ型です。
第2引数はFunc<TAccumulate, TSource, TAccumulate> func
です。TAccumulate
は第1引数から推論されます。TSource
はデータリストから推論されます。そして戻り値はTAccumulate
になり、これはラムダ式の戻り値から推論されます。推論だらけですね。
その3
最後に3つ目です。
// 身長の平均を求める
var aveHeight = IDOLList.Select(x => x.Height)
.Aggregate(
(height: 0.0, count: 0),
(x, y) => x = (x.height + y, x.count + 1),
z => z.height / z.count);
// 中身はこんな感じです
public static TResult Aggregate<TSource, TAccumulate, TResult>(this IEnumerable<TSource> source, TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> func, Func<TAccumulate, TResult> resultSelector)
{
if (source == null)
{
throw Error.ArgumentNull("source");
}
if (func == null)
{
throw Error.ArgumentNull("func");
}
if (resultSelector == null)
{
throw Error.ArgumentNull("resultSelector");
}
TAccumulate val = seed;
foreach (TSource item in source)
{
// 今回の場合、valがx,itemがyにあたる
val = func(val, item);
}
// 今回の場合、これがzです
return resultSelector(val);
}
2つ目とほぼ同じですが、戻り値に対して最終処理を行えます。
例によって置き換えてみます。
// ここで初期値を設定し
TAccumulate val = seed;
foreach (TSource item in source)
{
// 指定したメソッドをぶん回し
val = func(val, item);
}
// 最終処理をして終わり
return resultSelector(val);
↓
// 初期値:(height: 0.0, count: 0.0)
// 計算処理:(x, y) => x = (x.height + y, x.count + 1)
// 最終処理:z => z.height / z.count
TAccumulate val = (height: 0.0, count: 0.0);
foreach (TSource item in source)
{
val = (val.height + y, x.count + 1);
}
return val.height / val.count;
ここでの注意事項として、最終的な戻り値は、Aggregate
の**第3引数のラムダ式の戻り値です。**また第2引数のラムダ式に関しては2つ目と同じです。
C#でそれなりの量のコードを書いてきましたが、今の所一度も使った事はありません。
単体取得系
データリストの中から、1つだけ要素を取り出す事ができるメソッド達です。
また、大して変わらない大人の事情で、2つ同時に解説します。
First, FirstOrDefault
// 1人目のアイドル
var idolFirst = IDOLList.First();
// 条件の指定も可能
var after20First = IDOLList.FirstOrDefault(x => x.Age > 19);
条件に一致した最初の要素を取り出します。特に条件を指定しない場合、1つ目の要素が取り出せます。
2つの違いですが、条件を満たす要素が見つからなかった時の挙動で、First
は例外が発生し、FirstOrDefault
は既定値を返します。
単体取得系の中では一番人気だと思います。
Last, LastOrDefault
// 最後のアイドル
var idolLast = IDOLList.Last();
// 条件の指定も可能
var under149Last = IDOLList.LastOrDefault(x => x.Height < 150);
条件に一致した最後の要素を取り出します。特に条件を指定しない場合、最後の要素が取り出せます。
2つの違いですが、条件を満たす要素が見つからなかった時の挙動で、Last
は例外が発生し、LastOrDefault
は既定値を返します。
たまに使います。
Single, SingleOrDefault
// 単独のアイドル
var idolSingle = IDOLList.Single();
// 条件の指定も可能
var tubasaSingle = IDOLList.SingleOrDefault(x => x.Name == "伊吹 翼");
条件に一致した唯一の要素を取り出します。条件を満たす要素が複数あった場合、例外が発生します。特に条件を指定しない場合、ちょっと特殊な挙動をします。
-
Single
の場合- 要素数が1つならば、それを返す。0、もしくは2つ以上の場合例外。
-
SingleOrDefault
の場合- 要素数が1つならば、それを返す。0ならば既定値を返す。2つ以上の場合例外。
2つの違いですが、条件を満たす要素が見つからなかった時の挙動で、Single
は例外が発生し、SingleOrDefault
は既定値を返します。
SingleOrDefault
も、条件を満たす要素が複数あった場合、例外が発生します。使った事は1度もないです。
ElementAt, ElementAtOrDefault
// インデックスで指定
var idolElement = IDOLList.ElementAt(5);
// インデックスで指定
var overRange = IDOLList.ElementAtOrDefault(50000000);
インデックスで指定した場所の要素を取得します。特に条件とは設定できません。
2つの違いですが、領域外(0未満や要素数以上の値)を設定した時の挙動で、ElementAt
は例外が発生し、ElementAtOrDefault
は既定値を返します。
IEnumerable
のままインデックスで取得可能なので、便利な気がします。が、使った事はありません。
変換系
データリストをまるっと別の形に変えてしまうメソッド達です。
AsEnumerable
var idolEnumerable = IDOLList.AsEnumerable();
IEnumerable
に変換します。外部クラス等からList
や配列で受け取ったデータをIEnumerable
で公開したい時に使います。今の所、そんな局面に出会った事はないですが。
主な使いみちは2つあるそうです。
1つ目は、隠蔽されてしまった拡張クラスのメソッドを呼び出すとき。
拡張メソッドは、インスタンスメソッドよりも呼び出しの優先順位が低いです。
なので、
private class MyList<T> : List<T>
{
public bool Any()
{
return false;
}
}
こんなクラスを作って、Any
を実装してしまうと、LINQのAny
が呼び出せなくなります。
var myList = new MyList<IDOL>{ new IDOL{ Name = "俺" }};
var alwaysFalse = myList.Any(); // 常にfalse
こんな時にこいつの出番です。
var myList = new MyList<IDOL>{ new IDOL{ Name = "俺" }};
var correctlyJudge = myList.AsEnumerable().Any(); // LINQのAnyが呼ばれる
2つ目は、LINQ to SQL、つまりデータベースとデータをやり取りするときに使います。
詳しくはこちらをご覧ください。
ちなみに驚くほどシンプルな実装です。
public static IEnumerable<TSource> AsEnumerable<TSource>(this IEnumerable<TSource> source)
{
return source;
}
ToList
var bustList = IDOLList.Select(x => x.Bust).ToList();
var bust = bustList[5];
List
に変換します。
注意事項ですが、必要になるまで実行しないようにしてください。
いろいろな人がいろいろな所で言ってるので理由は割愛。(面倒だし)
// ToList()することで、結果が生成されるが
var bustList = IDOLList.Where(x => x.Age < 13).Select(x => x.Bust).ToList();
// 結局foreachでもう一度それらを確認することになる。
foreach(var b in bustList)
{
Console.WriteLine(b);
}
わりとよく見るコードですが、LINQ警察の大好物です。
// そのままにして
var bustList = IDOLList.Where(x => x.Age < 13).Select(x => x.Bust);
// foreachで結果を生成しましょう
foreach(var b in bustList)
{
Console.WriteLine(b);
}
こうやってLINQ警察を追い返しましょう。
ToArray
var hipAry = IDOLList.Select(x => x.Hip).ToArray();
var hip = hipAry[5];
配列に変換します。注意事項はToList
と同じです。
ToDictionary
// 名前で検索して、元データ(IDOLクラス)を取り出す辞書
var idolDict = IDOLList.ToDictionary(x => x.Name);
// 名前で検索して、読み仮名を取り出す辞書
var phoneticDict = IDOLList.ToDictionary(x => x.Name,y => y.Phonetic);
// 検索キーが重複すると、例外発生
var exceptionDict = IDOLList.ToDictionary(x => x.Age);
Dictionary
に変換します。取り出す形式は特に指定しなければ元のデータ、指定した場合はその型です。
注意事項ですが、重複する値をキーに指定すると例外が発生します。(アイマスに同姓同名のキャラはいないが、年齢が同じキャラはいる)
ToLookup
// 名前で検索して、元データ(IDOLクラス)を取り出すルックアップテーブル
var idolLook = IDOLList.ToLookup(x => x.Name);
// 名前で検索して、読み仮名を取り出すルックアップテーブル
var phoneticLook = IDOLList.ToLookup(x => x.Name,y => y.Phonetic);
// 検索キーが重複しても、問題なし
var noexceptionLook = IDOLList.ToLookup(x => x.Age);
// 14歳組
var age14 = noexceptionLook[14];
ILookup
に変換します。Dictionary
に似ていますが、Dictionary
が一致する単一の要素を取り出すのに対して、ILookup
は一致する要素のコレクションを取り出します。
ToHashSet
// 重複した値が削除される
var idolSet = IDOLList.ToHashSet();
// 比較方法を指定することも可能
var idolAgeSet = IDOLList.ToHashSet(new IdolCompare());
class IdolCompare : IEqualityComparer<IDOL>
{
public bool Equals(IDOL x, IDOL y)
{
return x.Age == y.Age;
}
public int GetHashCode(IDOL obj)
{
return obj.Age.GetHashCode();
}
}
HashSet
に変換します。やっている事自体はDistinct
と同じです。なので、注意事項も同じ。
何が違うかというと、ToHashSet
はHashSet
クラスに変換します。なので、ここで評価が実行されます(即時評価)。
一方Distinct
はIEnumerable
のままなので、評価は実行されません(遅延評価)。
使い分けの基準としては、
- まだLINQのメソッドを呼ぶ場合や、その後
foreach
を実行する場合、Distinct
- もうこれ以上LINQのメソッドを呼ばず、結果を利用する場合は、
ToHashSet
でいいと思います。
ソート系
ソートするメソッド達です。以上です。
OrderBy, OrderByDescending
// 昇順
var ascendingIdol = IDOLList.OrderBy(x => x.Bust);
// 降順
var descendingIdol = IDOLList.OrderByDescending(x => x.Bust);
// 外部のデータを使ってソートも出来る(ランダムに並び替え)
var rondomIdol = IDOLList.OrderBy(x=> Guid.NewGuid());
// ソート較方法を指定することも可能
var ascendingIdol2 = IDOLList.OrderBy(x => x, new IdolCompare());
var descendingIdol2 = IDOLList.OrderByDescending(x => x.Bust, new IdolCompare());
class IdolCompare : IComparer<IDOL>
{
public int Compare(IDOL x, IDOL y)
{
return x.Bust - y.Bust;
}
}
指定されたキーでソートします。IComparer
インターフェイスによってソート方法を指定することも可能です。
ThenBy, ThenByDescending
// 昇順→降順
var ascendingIdol = IDOLList.OrderBy(x => x.Bust).ThenByDescending(x=>x.Waist);
// 降順→昇順
var descendingIdol = IDOLList.OrderByDescending(x => x.Bust).ThenBy(x=>x.Waist);
// ソート較方法を指定することも可能
var ascendingIdol2 = IDOLList.OrderBy(x => x, new IdolCompareB()).ThenByDescending(x => x, new IdolCompareW());
var descendingIdol2 = IDOLList.OrderByDescending(x => x, new IdolCompareB()).ThenBy(x => x, new IdolCompareW());
class IdolCompareB : IComparer<IDOL>
{
public int Compare(IDOL x, IDOL y)
{
return x.Bust - y.Bust;
}
}
class IdolCompareW : IComparer<IDOL>
{
public int Compare(IDOL x, IDOL y)
{
return x.Waist - y.Waist;
}
}
OrderBy
とOrderByDescending
を実行後のみ、呼び出せるメソッドです。OrderBy
やOrderByDescending
で同一だった場合の副条件を設定できます。当然IComparer
インターフェイスによってソート方法を指定することも可能です。
範囲取得系
データリストから範囲を指定して取得するメソッド達です。
Skip
// 前から100人飛ばして、後ろの177人が残る
var hundredSkip = IDOLList.Skip(100);
引数で指定した数だけ飛ばして、残った要素を返します。この時、要素数よりも大きな数を指定しても例外は発生しません。
SkipWhile
// 最初に年齢が0歳のキャラが出るまで飛ばして、残りを返す。
var age0Skip = IDOLList.SkipWhile(x => x.Age != 0);
// インデックス付きもある
var indexedSkip = IDOLList.SkipWhile((x, i) => i < 100);
引数で指定した条件に一致する要素が出てくるまで飛ばして、残った要素を返します。この時、最後まで一致しなくても例外は発生しません。
Take
// 前から100人取得して、後ろのは飛ばす
var hundredTake = IDOLList.Take(100);
引数で指定した数だけ取得して、残った要素を飛ばします。この時、要素数よりも大きな数を指定しても例外は発生しません。
TakeWhile
// 最初に年齢が0歳のキャラが出るまで取得して、残りを飛ばす。
var age0Skip = IDOLList.SkipWhile(x => x.Age != 0);
// インデックス付きもある
var indexedSkip = IDOLList.SkipWhile((x, i) => i < 100);
引数で指定した条件に一致する要素が出てくるまで取得し、残った要素を飛ばします。この時、最後まで一致しなくても例外は発生しません。
見れば分かると思いますが、
-
Skip
⇔Take
-
SkipWhile
⇔TakeWhile
と対応する関係になってます。
結合系
複数のデータリストを様々な方法でくっつけるメソッド達です。
この辺から、結構説明が面倒くさいやつが増えてきます。
Concat
// 向井 拓海, 藤本 里奈, 松永 涼, 大和 亜季, 木村 夏樹
var enjin = IDOLList.Where(x => x.Unit.Contains("炎陣"));
// 向井 拓海, 藤本 里奈
var naughty = IDOLList.Where(x => x.Unit.Contains("ノーティギャルズ"));
// 向井 拓海, 藤本 里奈, 松永 涼, 大和 亜季, 木村 夏樹, 向井 拓海, 藤本 里奈
var gocha = enjin.Concat(naughty);
元のデータリストの後ろに、引数で指定したデータリストをくっつけます。
ただの結合なので、重複してても関係ありません。
Zip
// 星井 美希, 高山 紗代子, 天空橋 朋花, 永吉 昴, 二階堂 千鶴
var milky = IDOLList.Where(x => x.Unit.Contains("ミルキーウェイ"));
// 中谷 育, 七尾 百合子, 松田 亜利沙
var birth = IDOLList.Where(x => x.Unit.Contains("トゥインクルリズム"));
// (星井 美希, 中谷 育), (高山 紗代子, 七尾 百合子), (天空橋 朋花, 松田 亜利沙)
var gocha = milky.Zip(birth, (x, y) => (x.Name, y.Name));
2つのデータリストから1つのデータリストを作ります。
第1引数で指定したデータリストと元のデータリストを、第2引数のラムダ式で変換して1つのデータリストにまとめます。
注意事項ですが、最終的な要素数は2つのデータリストのうち少ない方と同じです。今回の場合は、上のリストの方が長いので、後ろ2人は無視されます。
Union
// 森久保 乃々, 星 輝子, 早坂 美玲
var indivi = IDOLList.Where(x => x.Unit.Contains("インディヴィジュアルズ"));
// 輿水 幸子, 星 輝子, 白坂 小梅
var kawaii = IDOLList.Where(x => x.Unit.Contains("カワイイボクと142's"));
// 森久保 乃々, 星 輝子, 早坂 美玲, 輿水 幸子, 白坂 小梅
var syokoHarem = indivi.Concat(under);
元のデータリストと、引数で指定したデータリストの和集合を求めます。
和集合なので重複は削除されます。
Intersect
// 速水 奏, 塩見 周子, 宮本 フレデリカ, 一ノ瀬 志希, 城ヶ崎 美嘉
var lipps = IDOLList.Where(x => x.Unit.Contains("LiPPS"));
// 速水 奏, 小松 伊吹
var luminous = IDOLList.Where(x => x.Unit.Contains("ルミナスボーダー"));
// 速水 奏
var momiyade = lipps.Intersect(luminous);
元のデータリストと、引数で指定したデータリストの積集合を求めます。
積集合なので、重複したもののみが残ります。
Except
// 渋谷 凛, 本田 未央, 島村 卯月
var newGene = IDOLList.Where(x => x.Unit.Contains("ニュージェネレーション"));
// 渋谷 凛、神谷 奈緒、北条 加蓮
var luminous = IDOLList.Where(x => x.Unit.Contains("トライアドプリムス"));
// 本田 未央, 島村 卯月
var udumio = newGene.Except(luminous);
元のデータリストから、引数で指定したデータリストを引いた差集合を求めます。
「元のデータリストにない要素」が、「引数のデータリストにある」状態でも例外は発生しません。
GroupBy
// 名前で検索して、元データ(IDOLクラス)を取り出すグループ
var idolGroup = IDOLList.GroupBy(x => x.Name);
// 名前で検索して、読み仮名を取り出すグループ
var phoneticGroup = IDOLList.GroupBy(x => x.Name,y => y.Phonetic);
// 検索キーが重複しても、問題なし
var noexceptionGroup = IDOLList.GroupBy(x => x.Age);
// 14歳組
var age14 = noexceptionGroup.First(x => x.Key == 14);
結合系では無いような気もしますが、他に場所が無かったのでここで説明します。
変換系で説明したToLookup
とほぼ同じです。違いは評価のタイミングです。
ToLookup
はILookup
インターフェイスに変換します。なので、ここで評価が実行されます(即時評価)。
一方GroupBy
はIEnumerable
のままなので、評価は実行されません(遅延評価)。
ToHashSet
に対するDistinct
と同じです。なので、使い分けもそちらと同様です。
Join
// 作品識別クラス
class Work
{
public int ID { get; set; }
public string Name { get; set; }
}
// 作品一覧
var workList = new List<Work>{
new Work{ ID = 1, Name = "AllStarts" },
new Work{ ID = 2, Name = "DearlyStars" },
new Work{ ID = 3, Name = "CinderellaGirls" },
new Work{ ID = 4, Name = "MillionLive" },
new Work{ ID = 5, Name = "ShinyColors" },
};
// 名前と作品名のタプル
var workName = workList.Join(
IDOLList,
x => x.ID,
y => y.WorkID,
(x, y) => (Work: x.Name, y.Name));
元のデータリストに、引数で指定したデータリストを結合します。
引数を順番に説明すると、
- 結合したいデータリスト
- 2,3で指定するキーを使って、元のデータと結びつけるデータです。
- 元のデータのキー
- 3のキーと比較されます。
- 結合したいデータのキー
- 2のキーと比較されます。
- 最終的に出力する結果生成用のラムダ式
- このラムダ式の戻り値の
IEnumerable
が生成されます。
- このラムダ式の戻り値の
こんな感じです。
GroupJoin
// 作品識別クラス
class Work
{
public int ID { get; set; }
public string Name { get; set; }
}
// 作品一覧
var workList = new List<Work>{
new Work{ ID = 1, Name = "AllStarts" },
new Work{ ID = 2, Name = "DearlyStars" },
new Work{ ID = 3, Name = "CinderellaGirls" },
new Work{ ID = 4, Name = "MillionLive" },
new Work{ ID = 5, Name = "ShinyColors" },
};
// 作品をキーとして、「作品と名前のタプル」の列挙を作成
var workNameDouji = workList.GroupJoin(
IDOLList,
x => x.ID,
y => y.WorkID,
(x, y) => (x, y.Select(x => x.Name)));
結合した後に、指定したキーでグループ化します。
引数を順番に説明すると、
- 結合したいデータリスト
- 2,3で指定するキーを使って、元のデータと結びつけるデータです。
- 元のデータのキー
- 3のキーと比較されます。
- 結合したいデータのキー
- 2のキーと比較されます。
- 最終的に出力する結果生成用のラムダ式
- このラムダ式の戻り値が、各グループに配置される
IEnumerable
になります。
- このラムダ式の戻り値が、各グループに配置される
こんな感じです。Join
とほとんど同じです。
その他
どこに入れたら良いかわからないもの達です。
Reverse
// 昇順
var ascendingIdol = IDOLList.OrderBy(x => x.Bust);
// 降順を反転→昇順
var descendingReverseIdol = IDOLList.OrderByDescending(x => x.Bust).Reverse();
データリストをひっくり返します。
ただひっくり返すだけなので、上記の2つが同一のデータリストを返すとは限りません。
// trueになるとは限らない(同一の値が存在する場合)
var rslt = ascendingIdol.SequenceEqual(descendingReverseIdol);
- 元データ(一部抜粋)
Name | Bust |
---|---|
如月 千早 | 72 |
及川 雫 | 105 |
中谷 育 | 72 |
横山 千佳 | 65 |
OrderBy(x => x.Bust)
Name | Bust |
---|---|
横山 千佳 | 65 |
如月 千早 | 72 |
中谷 育 | 72 |
及川 雫 | 105 |
OrderByDescending(x => x.Bust)
Name | Bust |
---|---|
及川 雫 | 105 |
如月 千早 | 72 |
中谷 育 | 72 |
横山 千佳 | 65 |
OrderByDescending(x => x.Bust).Reverse()
Name | Bust |
---|---|
横山 千佳 | 65 |
中谷 育 | 72 |
如月 千早 | 72 |
及川 雫 | 105 |
Append, Prepend
// 星井 美希, 我那覇 響, 四条 貴音
var fairy = IDOLList.Where(x => x.Unit.Contains("プロジェクト・フェアリー"));
// 俺、アイドルになります。
var newIDOL = new IDOLViewData { Name = "俺" };
// 星井 美希, 我那覇 響, 四条 貴音, 俺
var fairyore = fairy.Append(newIDOL);
// 俺, 星井 美希, 我那覇 響, 四条 貴音
var orefairy = fairy.Prepend(newIDOL);
先頭や末尾に新たな要素を追加します。
注意事項ですが、Add
と異なり、**元の要素は変更されません。**あくまで新しいIEnumerable
を返すメソッドです。
DefaultIfEmpty
// さすがにそんなに大きな人はいない
var bigBust = IDOLList.Where(x => x.Bust > 110);
// そもそもforeachの中に入らない
foreach(var bb in bigBust)
{
// 呼ばれることなく終わる
Console.WriteLine(bb.Name);
}
// 規定の値を返す
foreach(var bb in bigBust.DefaultIfEmpty())
{
try
{
// 例外が発生(クラスの既定はnull)
Console.WriteLine(bb.Name);
}
catch(Exception e)
{
Console.WriteLine(e.Message);
}
}
// 規定の値を指定することも可能
foreach(var bb in bigBust.DefaultIfEmpty(new IDOL { Name = "俺" }))
{
// 俺
Console.WriteLine(bb.Name);
}
要素数が0の時、代わりに返す値を設定できるメソッドです。
検索結果が見つからなかった時に返すメッセージを設定しておくと、捗るかもしれませんね。
var over40 = IDOLList.Where(x => x.Age > 40).Select(x => x.Name).DefaultIfEmpty("404");
foreach(var o in over40)
{
Console.WriteLine(o);
}
Range, Repeat, Empty
// 0,1,2,3,4,5,6,7,8,9
var range = Enumerable.Range(0, 10);
// 0,0,0,0,0,0,0,0,0
var repeat = Enumerable.Repeat(0, 10);
// 素寒貧
var empty = Enumerable.Empty<int>();
// foreachでforっぽいことが出来る
foreach(var i in range)
{
// 0123456789
Console.Write(i);
}
// 初期値を指定した配列が出来る
var zeroAry = repeat.ToArray();
新たなIEnumerable
を作り出してくれるメソッド達です。
Range
は初期値と回数を指定します。初期値から1ずつインクリメントされたIEnumerable
が返ってきます。
Repeat
は設定値と回数を指定します。Range
とは異なり、設定値を回数分繰り返したIEnumerable
が返ってきます。
Empty
は空っぽのIEnumerable
が返ってきます。引数がなにもないので、ジェネリックで型を指定します。有効活用できる場面が思いつかなかったので、誰か教えてください。
あとがき
一気呵成に書き上げました。左手が攣りそうです。
Join
とGroupBy
とGroupJoin
はそのうち個別に何か書こうかと思ってます。(思ってるだけ)
「その説明間なんかオカシイで」とか「このメソッドもっと詳しく」とか「何いってんだか分かんねーぞ」とかあったらコメントとか編集リクエストとかでお願いします。