7
7

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 5 years have passed since last update.

yield returnを使わずにIEnumerable<T>を生成する

Posted at

やりたいこと

yield return を使わずに、かつなるべく楽して IEnumerable<T> を返すメソッドを実装したい

本題

C#、で、例えばこんな感じの拡張メソッドを定義すると、これをLinq to Objectのメソッドチェーンの中で利用することができる。

linqex.cs
/// <summary>
/// シーケンスから、同一のキーを持つ要素の重複を取り除く
/// </summary>
public static IEnumerable<T> DistinctBy<T, TKey>(this IEnumerable<T> source, Func<T, TKey> keySelector)
{
    var done = new HashSet<TKey>();
    foreach (var item in source)
    {
        if (done.Add(keySelector(item)))
            yield return item;
    }
}

これと同じことを yield return を使わずに実現しようと思うと、正攻法では IEnumerable<T>IEnumerator<T> を実装したクラスをそれぞれ用意して、という感じで結構面倒くさい。

だったらこんな感じにすればいいんじゃないの?と思ってやってみると、

linqex.cs
public static IEnumerable<T> DistinctBy<T, TKey>(this IEnumerable<T> source, Func<T, TKey> keySelector)
{
    var done = new HashSet<TKey>();
    return source.Where(x => done.Add(keySelector(x)));
}

一見うまくいくんだけど、下の例みたいに生成された IEnumerable<T> を複数回列挙すると2回目以降の結果がおかしくなる。
こういう使い方をするのはレアなケースではあるのだけれど。

test.cs
[Test]
public void EnumerateTwice()
{
    var ret = new [] {1, 2, 3, 1, 2}.DistinctBy(x => x);
    Assert.That(ret.ToArray(), Is.EqualTo(new [] {1, 2, 3}));  // PASS
    Assert.That(ret.ToArray(), Is.EqualTo(new [] {1, 2, 3}));  // FAIL ret.ToArray() は空の配列になる
}

要は初期化(この場合はHashSetの生成)処理をメソッドチェーンの外でやってるのが問題なので、これをメソッドチェーンに埋め込んでしまえばいい訳で、こんな感じに変えてやると意図したとおりに動くようになる。

linqex.cs
public static IEnumerable<T> DistinctBy<T, TKey>(this IEnumerable<T> source, Func<T, TKey> keySelector)
{
    return new[] { source }.SelectMany(s =>
        {
            var done = new HashSet<TKey>();
            return s.Where(x => done.Add(keySelector(x)));
        });
}

別の例

linqex.cs
/// <summary>
/// シーケンスを指定されたサイズごとに分割する.
/// [1, 2, 3, 4, 5, 6, 7].EachSlice(3) -> [1, 2, 3], [4, 5, 6], [7]
/// </summary>
public static IEnumerable<T[]> EachSlice<T>(this IEnumerable<T> source, int size)
{
    return new[] { source }.SelectMany(s =>
        {
            var r = Enumerable.Repeat(s.GetEnumerator(), size).ToArray();
            return Enumerable.Repeat(0, int.MaxValue)
                             .Select(_ => r.TakeWhile(e => e.MoveNext()).Select(e => e.Current).ToArray())
                             .TakeWhile(arr => arr.Length > 0);
        });
}

おとなしくyield使えばいいんじゃないの?

デスヨネー
Visual Studio 2010でVBを書くとかそういう縛りプレイを強いられてる時にはそれなりに役に立つ気がする。
C#なら、まあyield使いますよね。はい。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?