16
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Enumalableクラスのメソッドについて本気出して調べてみた

Last updated at Posted at 2019-04-16

まえがき

久しぶりの投稿です。しばらく何も書いてなかったのは、やる気が出なかったお仕事が忙しかったからです。

Selectに関して書こうと思ったけど面倒くさくなって書くことがなく、とりあえずLINQ全体についてダラダラ書こうと思ってたら、なんだか長くなってしまいました。

定期的に見直して、書き直す予定です。(予定は未定)

LINQですよ、LINQ!

System.Core.dllのSystem.Linq名前空間の Enumalableクラスにあるメソッドの解説を並び立てる記事です。対象 は .Net Framework 4.7.2です。なので、SkipLastTakeLastは対象外です。

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

重複削除メソッドです。組み込み型だったら特に何も考えなくて良いんですが、自作クラスの重複削除だとちょっと面倒です。

  1. 自作クラスにIEquatableインターフェイスを実装する。
  2. 外部から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と同様で、自作クラスの判定だとちょっと面倒です。isBlank1isBlank3trueですが、isBlank2falseになります。

基本的には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つのデータリストの型が違っても比較可能です。(今回の例だとIEnumarableList。)

ただし、自作クラスの判定だとやっぱりちょっと面倒です。

算術系

いろいろな計算をしてくれるメソッド達です(適当)。

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種類の変数以外のリストでも、MaxMinは使用可能です。ただし、そのままでは実行時に例外を吐いて死にます。

データリストの中身がIComparableIComparable<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)xyの事です。

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と同じです。なので、注意事項も同じ。

何が違うかというと、ToHashSetHashSetクラスに変換します。なので、ここで評価が実行されます(即時評価)。

一方DistinctIEnumerableのままなので、評価は実行されません(遅延評価)。

使い分けの基準としては、

  • まだ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;
    }
}

OrderByOrderByDescendingを実行後のみ、呼び出せるメソッドです。OrderByOrderByDescendingで同一だった場合の副条件を設定できます。当然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);

引数で指定した条件に一致する要素が出てくるまで取得し、残った要素を飛ばします。この時、最後まで一致しなくても例外は発生しません。

見れば分かると思いますが、

  • SkipTake
  • SkipWhileTakeWhile

と対応する関係になってます。

結合系

複数のデータリストを様々な方法でくっつけるメソッド達です。

この辺から、結構説明が面倒くさいやつが増えてきます。

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とほぼ同じです。違いは評価のタイミングです。

ToLookupILookupインターフェイスに変換します。なので、ここで評価が実行されます(即時評価)。

一方GroupByIEnumerableのままなので、評価は実行されません(遅延評価)。

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

元のデータリストに、引数で指定したデータリストを結合します。

引数を順番に説明すると、

  1. 結合したいデータリスト
    • 2,3で指定するキーを使って、元のデータと結びつけるデータです。
  2. 元のデータのキー
    • 3のキーと比較されます。
  3. 結合したいデータのキー
    • 2のキーと比較されます。
  4. 最終的に出力する結果生成用のラムダ式
    • このラムダ式の戻り値の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)));

結合した後に、指定したキーでグループ化します。

引数を順番に説明すると、

  1. 結合したいデータリスト
    • 2,3で指定するキーを使って、元のデータと結びつけるデータです。
  2. 元のデータのキー
    • 3のキーと比較されます。
  3. 結合したいデータのキー
    • 2のキーと比較されます。
  4. 最終的に出力する結果生成用のラムダ式
    • このラムダ式の戻り値が、各グループに配置される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が返ってきます。引数がなにもないので、ジェネリックで型を指定します。有効活用できる場面が思いつかなかったので、誰か教えてください。

あとがき

一気呵成に書き上げました。左手が攣りそうです。

JoinGroupByGroupJoinはそのうち個別に何か書こうかと思ってます。(思ってるだけ)

「その説明間なんかオカシイで」とか「このメソッドもっと詳しく」とか「何いってんだか分かんねーぞ」とかあったらコメントとか編集リクエストとかでお願いします。

16
10
3

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
16
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?