第65章:Iterator ③:演習(注文のフィルタ列挙をyieldで)🛒
ねらい 🎯💖

- 「注文一覧をフィルタして返す」みたいな処理を、IEnumerable
と yield return でスッキリ書けるようになるよ〜😊 - **foreach で回せる形(=列挙可能)**にしておくと、呼び出し側が気持ちよくなる✨
- **遅延実行(必要になった分だけ取り出す)**の感覚を、テストでちゃんと体験するよ🧪🌸(LINQの遅延実行と同じ発想だよ)(Microsoft Learn)
- IEnumerator を自力実装しなくても、C#は yield で Iterator(反復子)を書けるのが強い💪😆(Microsoft Learn)
到達目標 🏁🌟
- IEnumerable
を返すフィルタ処理を yield return で書ける😊 - 「Listで全部作って返す(即時実行)」と「yieldで返す(遅延実行)」の違いを説明できる🧠✨
- MSTest で「動く・テストが通る・責務が薄い」を満たせる🧪🎀(Microsoft Learn)
- “同じ IEnumerable を2回列挙すると2回走る” を理解して、対策(ToListでキャッシュ等)を選べる🔁🙂
手順 🧩🛠️
1) プロジェクトを作る📁✨
- ソリューション:GofPatterns
- クラスライブラリ:ECommerce(Target Framework は net10.0 でOK。LTSだよ〜)🧡(Microsoft Learn)
- テスト:ECommerce.Tests(MSTest)🧪🌸(Microsoft Learn)
2) 例題の最小ドメインを用意する🍰🛒
「注文(Order)」を最低限だけ作るよ😊
OrderStatus.cs
namespace ECommerce;
public enum OrderStatus
{
New = 0,
Paid = 1,
Shipped = 2,
Cancelled = 3
}
Order.cs
namespace ECommerce;
public sealed record Order(
int Id,
OrderStatus Status,
decimal TotalAmount,
DateTime CreatedAtUtc
);
3) 導入前:まずは“素朴にListで返す”版を作る🧺🙂
「全部走査して、条件に合うものを List に詰めて返す」やつ。わかりやすいけど、毎回ぜんぶ走るし、全部メモリに乗る感じになりがち😵
OrderFilters_Eager.cs
using System.Collections.Generic;
namespace ECommerce;
public static class OrderFiltersEager
{
public static List<Order> PaidOver(IEnumerable<Order> source, decimal minTotalAmount)
{
ArgumentNullException.ThrowIfNull(source);
if (minTotalAmount < 0) throw new ArgumentOutOfRangeException(nameof(minTotalAmount));
var result = new List<Order>();
foreach (var order in source)
{
if (order.Status == OrderStatus.Paid && order.TotalAmount >= minTotalAmount)
{
result.Add(order);
}
}
return result;
}
}
4) 導入後:Iterator(yield return)版に置き換える🚶♀️✨
ここが本番〜!💖
IEnumerable
コツ:引数チェックを“即時”にしたいときはラッパーを作る⚠️
yield を含むメソッドは 呼び出した瞬間には中身が走らず、列挙されたときに走る(遅延)から、引数チェックも遅れがち🙃 だから「チェックだけ先にして、実体は別メソッドにyieldで書く」って形が超よく使われるよ👍
OrderFilters_Iterator.cs
using System.Collections.Generic;
namespace ECommerce;
public static class OrderFilters
{
// ✅ ここは“普通のメソッド”。なので引数チェックが呼び出し時に走るよ🙂
public static IEnumerable<Order> PaidOver(IEnumerable<Order> source, decimal minTotalAmount)
{
ArgumentNullException.ThrowIfNull(source);
if (minTotalAmount < 0) throw new ArgumentOutOfRangeException(nameof(minTotalAmount));
return PaidOverImpl(source, minTotalAmount);
}
// ✅ ここがIterator本体(遅延実行)
private static IEnumerable<Order> PaidOverImpl(IEnumerable<Order> source, decimal minTotalAmount)
{
foreach (var order in source)
{
if (order.Status == OrderStatus.Paid && order.TotalAmount >= minTotalAmount)
{
yield return order; // 🌟 1個ずつ返す
}
}
// 何も返すものがなければ、自然に終わる(yield breakでもOK)🧁
}
}
5) “呼び出し側”はどう楽になる?😆🎉
- foreach でそのまま回せる🚶♀️✨
- さらに LINQ と繋げやすい(Where / Take / First など)🔗💎
- ただし!同じ IEnumerable を2回列挙すると2回走る(重要)🔁😵
6) テストを書く🧪🌸(動く・通る・薄い!)
MSTest は、TestClass / TestMethod でテストを認識するよ〜😊(Microsoft Learn) (TestMethod は void / Task / ValueTask もOK、みたいな契約もあるよ)(Microsoft Learn)
OrderFiltersTests.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ECommerce.Tests;
[TestClass]
public class OrderFiltersTests
{
[TestMethod]
public void PaidOver_filters_correctly()
{
var orders = SampleOrders();
var filtered = OrderFilters.PaidOver(orders, minTotalAmount: 3000m).ToList();
// Paid かつ 3000以上だけ
Assert.AreEqual(2, filtered.Count);
CollectionAssert.AreEqual(new[] { 2, 4 }, filtered.Select(o => o.Id).ToArray());
}
[TestMethod]
public void PaidOver_is_lazy_until_enumerated()
{
var log = new List<int>();
IEnumerable<Order> source = SampleOrdersWithLog(log);
var query = OrderFilters.PaidOver(source, minTotalAmount: 3000m);
// まだ列挙してないのでログは空🫧
Assert.AreEqual(0, log.Count);
// First() した瞬間に必要な分だけ進む🏃♀️💨
var first = query.First();
Assert.AreEqual(2, first.Id);
CollectionAssert.AreEqual(new[] { 1, 2 }, log); // 1→2まで見たところで最初のヒット
}
[TestMethod]
public void PaidOver_argument_check_happens_immediately()
{
Assert.ThrowsException<ArgumentNullException>(() =>
{
_ = OrderFilters.PaidOver(null!, 100m); // 呼び出し時点で落ちるのが嬉しい👍
});
}
private static List<Order> SampleOrders() =>
new()
{
new Order(1, OrderStatus.New, 5000m, DateTime.UnixEpoch),
new Order(2, OrderStatus.Paid, 3000m, DateTime.UnixEpoch),
new Order(3, OrderStatus.Paid, 1000m, DateTime.UnixEpoch),
new Order(4, OrderStatus.Paid, 9000m, DateTime.UnixEpoch),
new Order(5, OrderStatus.Shipped, 7000m, DateTime.UnixEpoch),
};
private static IEnumerable<Order> SampleOrdersWithLog(List<int> log)
{
foreach (var o in SampleOrders())
{
log.Add(o.Id); // いま何個目まで見たかを記録📌
yield return o; // ここもyieldで“供給”してるよ✨
}
}
}
ここまで動けばOK〜!🎉✨ ※ 公式ドキュメントを読むときは、Microsoft の Learn がいちばん確実だよ📚🧡(yield / IEnumerable / MSTest も全部まとまってる)(Microsoft Learn)
落とし穴 🕳️⚠️
- 遅延実行で「例外がいつ出るか」ズレる😵
- 呼び出した瞬間じゃなく、列挙したタイミングで落ちることがあるよ。遅延実行の性質だね🫠(Microsoft Learn)
- 2回列挙すると2回“同じ処理”が走る🔁😱
- DB/HTTP みたいな重いソースに繋がってる IEnumerable だと事故る💥
- 必要なら ToList() で一度だけ評価してキャッシュするのもアリ🙂
- 副作用(ログ出力、乱数、現在時刻)を iterator の中でやると再現性が壊れやすい🌀
- テストが不安定になりがちだよ🥲
- “順番に依存する処理”が増えると、読みにくくなる📦😵
- Iterator は「走査を隠す」のが主役!
- ビジネスロジックを詰め込みすぎないのがコツだよ🍬
演習 📝💗
演習1:フィルタ条件を増やす(期間も入れる)📅✨
-
OrderFilters に、作成日時の範囲でも絞れる版を追加してね😊
- 例:Paid かつ minTotal 以上 かつ from〜to の間
ヒント:引数チェックはラッパーに置いて、Impl の中で yield return する構造はそのまま使ってOK👍
演習2:先頭N件だけ返す(yield breakを使う)🧁🔚
- “条件に合う注文を最大N件だけ返す” メソッドを作ってみてね😊
- N件返したら、その時点で終わらせると効率よし✨(yield は「終了」も表現できるよ)(Microsoft Learn)
演習3:AI補助でテスト雛形→人間がレビュー🤖👀✨
-
GitHub Copilot などに「MSTestで、遅延実行を検証するテストを書いて」って頼んで雛形を出してもらう
-
最後に人間の目でチェック👇
- そのテスト、列挙回数をちゃんと観測できてる?🔁
- “たまたま通る” じゃなくて、失敗条件も作れてる?💥
- 依存(DateTime.Now とか)を入れてテスト不安定にしてない?🌀
(もし OpenAI Codex を使うなら、OpenAI 系の拡張でも同じノリでOKだよ😊)
チェック ✅🌸
- IEnumerable
を返すと、呼び出し側が foreach で回せるって説明できる?🚶♀️✨(Microsoft Learn) - yield return のコードは「呼んだ瞬間」じゃなく「列挙した瞬間」に動くって言える?🫧(Microsoft Learn)
- 引数チェックを“即時”にしたいとき、ラッパー+Impl に分ける理由を説明できる?🙂
- 同じ IEnumerable を2回列挙すると2回走るのを理解して、必要なら ToList() を選べる?🔁💡
- MSTest の TestClass / TestMethod を使って、演習がテストで守れてる?🧪🎀(Microsoft Learn)