あまりないコーディングパターンとLINQ to Objectの対応付け

今日も今日とてテライケメン!と叫んでいる方/叫ばれている方がTLにいました。と、見てみるとLINQ to Objectがどうのこうの、と。

あると便利ですよね、ということで書いてみた。
よくあるコーディングパターンには yield とか使ってないです。
こっちの方がよくありそうでしょ?

2011-02-10

まあyieldなんて例によって未確認飛行Cでこないだ初めて知ったぐらいですし。
LINQ to Objectでおなじみの拡張メソッドとよくあるループとか使ったパターンとの対応付けが書かれています。

Skip
先頭から n 個無視したい場合に使います。
後で書く。

・・・先に書いちゃる。
と言っても”よくあるパターン”の知識は少ないので、実践的に。マニアックとも言う。

Skip

先頭からn個無視したい場合に。

// たぶんあまりないコーディングパターン1
// 最初の要素だけ除外
// ほらコマンドライン引数の処理とかなんかあるでしょ・・・
public IEnumerable<string> SkipFirst(string[] target)
{
    if (target.Length < 1) // 空の配列かもよ!
        return new string[0];
    else
    {
        var result = new string[target.Length - 1];
        Array.Copy(target, 1, result, 0, target.Length - 1);
        return result;
    }
}
// Skipで書き直し
public IEnumerable<string> SkipFirst(string[] target)
{
    return target.Skip(1);
}

必要なこと("1個"除外ということ)だけ書けばいいというのはイイネ!

SkipWhile

先頭から条件を満たす要素だけ無視。

// たぶんあまりないコーディングパターン2
// -で始まる要素が続く限り除外
// ほらコマンドライン引数の処理とか(ry
public IEnumerable<string> SkipOptions(string[] target)
{
    var result = new List<string>();
    foreach (string s in target)
    {
        if (!s.StartsWith("-"))
            result.Add(s);
    }
    return result;
}
// SkipWhileを使って書き直し
public IEnumerable<string> SkipOptions(string[] target)
{
    return target.SkipWhile(s => s.StartsWith("-"));
}

Take

Skipとは逆に、先頭n要素を取り出す。

// たぶんあまりないコーディングパターン3
// 先頭10要素までを取得
// ほらheadコマンドとかあるじゃない・・・
public IEnumerable<string> ArrayHead(string[] target)
{
    if (target.Length < 10) // 10要素もないかもしれない
        return target;
    else
    {
        var result = new string[10];
        Array.Copy(target, result, 10);
        return result;
    }
}

まあLength<10の時にもコピーすればいい気はするけど。

// Takeを使って書き直し
public IEnumerable<string> ArrayHead(string[] target)
{
    return target.Take(10);
}

やっぱりスマート。

TakeWhile

SkipWhileと逆に、条件を満たさないものまでを取得したいとき。

// たぶんあまりないコーディングパターン4
// 先頭から、-で始まる要素を取得
// ほらコマンドライン引数の処理とか(ry
public IEnumerable<string> GetOptions(string[] target)
{
    var result = new List<string>();
    foreach (string s in target)
    {
        if (s.StartsWith("-"))
            result.Add(s);
    }
    return result;
}
// TakeWhileを使って書き直し
public IEnumerable<string> GetOptions(string[] target)
{
    return target.TakeWhile(s => s.StartsWith("-"));
}

なに、デジャブ?気のせいだ。

First

条件を満たす最初の要素を返す・・・って要は検索か。

// たぶんあまりないコーディングパターン5
// 10文字より長い要素を探す
// 誰とk
public string FindLongStr(string[] target)
{
    foreach (var s in target)
        if (s.Length > 10) return s;
    throw new InvalidOperationException();
}
// Firstを使って書き直し
public string FindLongStr(string[] target)
{
    return target.First(s => s.Length > 10);
}

条件を指定しないと単純に最初の要素を返します。

FirstOrDefault

Firstと違うのは見つからなかったときに既定値を返すこと。
参照型・null許容型の既定値はnull、値型は・・・MSDNにも書いてないんだが察すればいいんだろうか。

// たぶんあまりないコーディングパターン6
// 10文字より長い要素を探す。見つからなければnull。
// 誰とk
public string FindLongStr(string[] target)
{
    foreach (var s in target)
        if (s.Length > 10) return s;
    return null;
}
// Firstを使って書き直し
public string FindLongStr(string[] target)
{
    return target.FirstOrDefault(s => s.Length > 10);
}

これも条件指定されなければ最初の要素を返す。

Single

条件を満たす"唯一"の要素を返す。複数あれば例外が飛んでくる。

// たぶんあまりないコーディングパターン7
// 値が100を超える要素を返す。複数あれば例外スロー。
// 誰とk
public int GetBigValue(int[] target)
{
    int result = 0;
    bool found = false;

    foreach (var n in target)
        if (n > 100)
            if (!found) result = n;
            else throw new InvalidOperationException();
    if (found) return result;
    else throw new InvalidOperationException();
}
// Singleを使って書き直し
public int GetBigValue(int[] target)
{
    return target.Single(n => n > 100);
}

これも条件指定なしで、要素が一つかチェックするのにも使える。

SingleOrDefault

Singleの例外回避版。要素が見つからない・複数見つかった時に既定値を返す。

// たぶんあまりないコーディングパターン8
// 値が100を超える要素を返す。複数あれば0を返す。
// 誰とk
public int GetBigValueOrDefault(int[] target)
{
    int result = 0;
    bool found = false;

    foreach (var n in target)
        if (n > 100)
            if (!found) result = n;
            else return 0;
    if (found) return result;
    else return 0;
}
public int GetBigValueOrDefault(int[] target)
{
    return target.SingleOrDefault(n => n > 100);
}

Aggregate

アキュムレート・・・?*1
要素をまとめるのに使う・・・ぽい。

// たぶんあまりないコーディングパターン9
// 配列をカンマ区切りの文字列にまとめる
public string ToCSV(string[] target)
{
    if (target.Length == 0) throw new InvalidOperationException();
    var result = target[0];
    foreach (var s in target.Skip(1))
        result += ", " + s;
    return result;
}
// Aggregateを使って書き直し
public string ToCSV(string[] target)
{
    return target.Aggregate((acc, next) => acc + ", " + next);
}

このメソッド、オーバーロードが3つある。

TSource TSource.Aggregate(アキュムレータ関数)
この時最初の要素は最初のアキュムレート値になる=アキュムレータ関数は呼ばれない
TAccmulate TSource.Aggregate(TAccumlate シード値, アキュムレータ関数)
シード値が最初のアキュムレート値になる。この場合要素の型と結果の型は異なってもいい。
TResult TSource.Aggregate(TAccumlate シード値, アキュムレータ関数, 結果値への変換関数)
2番目のに加え、最後のアキュムレート値をさらに変換して結果にできる。

・・・配列をまとめるぐらいの感覚でいいよね。うん。

まとめ

どこが実践的なんだっていう。
ただAggregateの例でもSkipを使いましたが、単独より他のと組み合わせて使う事が多いような。
あとインテリセンスでエラーが出てないだけなので実行結果は確認してないとかどうとか(ぇ

*1:累積とか蓄積とか積もるとかいう意味らしい