LoginSignup
17
11

More than 5 years have passed since last update.

C#:イテレータメソッドを作るときはローカル関数を使った方がいい理由

Last updated at Posted at 2018-04-16

C# のイテレータは遅延評価です。
イテレータメソッドに引数チェックがある場合でも、メソッド呼び出し時にはチェックが行われず、評価時に例外が発生します。

素数を返すイテレータを例に見てみます。

class PrimeService
{
    public IEnumerable<int> Generate(int start, int end)
    {
        if (start <= 1)
            throw new ArgumentException("引数は1以上を指定してください");
        if (end < start)
            throw new ArgumentException($"{nameof(end)}には{nameof(start)}以上の値を指定して下さい");

        for(var i = start; start <= end; end++)
        {
            if (IsPrime(i))
                yield return i;
        }

    }

    private bool IsPrime(int n)
    {
         // 素数判定の中身は省略します。一番下に置いておきます。
    }
}

上記のイテレータメソッドは

  • 開始が 1 以下
  • 開始が終りより大きい

と例外が投げられるはずですが、メソッド呼び出し時に例外は発生しません。

class Program
{
    static void Main(string[] args)
    {
        var primeService = new PrimeService();
        var primes = primeService.Generate(start: 123, end: 32); // <- 引数が不正

        Console.WriteLine("まだ例外がなげられない!");

        foreach (var prime in primes) // <- ここで例外が発生する
            Console.WriteLine(prime);
    }
}

今回のように例外の発生個所と実際に例外をスローするコードが近い場合には問題の発見は簡単です。
しかし、大規模なプログラムになればイテレータを作るコードと列挙を開始するコードが近くにあるとは限らないため、問題の発見が困難になる場合があります。

イテレータの実装部をローカル関数として持とう

イテレータメソッドをローカル関数として内部に持つことでこの問題は解決します。
Generate が呼び出された時に引数が不正である場合、その場で例外がスローされるようになります。

public IEnumerable<int> Generate(int start, int end)
{
    if (start <= 1)
        throw new ArgumentException("引数は1以上を指定してください");
    if (end < start)
        throw new ArgumentException($"{nameof(end)}には{nameof(start)}以上の値を指定して下さい");

    return generateImpl();

    IEnumerable<int> generateImpl()
    {
        for (var i = start; start <= end; end++)
        {
            if (IsPrime(i))
                yield return i;
        }
    }
}

ローカル関数として実装を持つメリットは以下が挙げられます。

  • 実装メソッドへのアクセスをラッパーの Generate メソッド内のスコープに限定できる
    • そのためエラーチェックのない実装メソッドが直接アクセスされる危険がない
  • 実装メソッドはラッパーメソッドのすべての引数とローカル変数にアクセスできるため、実装メソッドの引数として渡す必要がない

More Effective C# 6.0/7.0 を参考にしました。

省略した IsPrime メソッドの中身

下記の書籍を参考にしました。
実戦で役立つ C#プログラミングのイディオム/定石&パターン

private bool IsPrime(int n)
{
    if (n == 1)
        return false;
    if (n == 2)
        return true;
    var boudary = (long)Math.Floor(Math.Sqrt(n));
    for (long i = 2; i <= boudary; ++i)
    {
        if (n % i == 0)
        {
            return false;
        }
    }
    return true;
}
17
11
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
17
11