C Sharp/反復と反復子

出典: フリー教科書『ウィキブックス(Wikibooks)』

反復と反復子[編集]

C#には、do, for, foreach, whileの4つの反復構文があります。 また、反復構文(主に foreach)と組合せ、ユーザー定義オブジェクトに反復を行う機構「反復子」があります。

反復構文[編集]

反復構文は、文または文のブロックを反復実行します。

do文
条件付きで文本体を1回以上実行します。
for文
指定されたブール式が真に評価される間、文本体を実行します。
foreach文
コレクションの要素を列挙し、その要素ごとに文本体を実行します。
while文
条件付きで文本体を0回以上実行します。

反復制御構文

break文
反復実行文の本体のどの位置でも、ループから抜け出すことができます。
continue文
ループ内の次の反復処理に進むことができます。

do文[編集]

do文は、指定されたブール式が真と評価される間、文または文のブロックを実行します。 この式はループの各実行後に評価されるため、do文は1回以上実行されます。 do文は、0回以上実行されるwhile文とは異なります。

public class DoStmtExample {
  public static void Main() {
    int n = 1;
    do {
      System.Console.Write(n);
      n += n;
    } while (n < 10);
    System.Console.WriteLine();
    
    do {
      System.Console.Write(n);
      n += n;
    } while (n < 10);
  }
}
実行結果
1248
16
10 行目からの do 文の条件は最初から成立していませんが、1回は実行されています。

for文[編集]

for文は、C風のfor(;;)です。 for文は、指定されたブール式が真と評価される間、文または文のブロックを実行します。

public class ForStmtExample {
  public static void Main() {
    for (int i = 0; i < 10; i++) {
      System.Console.Write(i);
    }
    // System.Console.Write(i); --> Main.cs(6,32): error CS0103: The name `i' does not exist in the current context
  }
}
実行結果
0123456789
初期化式
ここでは int i = 0
for文の最初に1度だけ(必ず)実行されます。
初期化式では、変数の宣言(と初期化)が可能です。
for 文で宣言された変数のスコープは、for文の終了までです。
省略可
条件式
ここでは i < 10
真の場合、文または文のブロックを実行します。
省略された場合、true とみなされます。
反復式
ここでは i++
文または文のブロックを実行したあと、反復毎に実行します。
省略可

foraech文[編集]

foreach文は、System.Collections.IEnumerable または System.Collections.Generic.IEnumerable<T> インターフェースを実装する型のインスタンスの要素ごとに文または文のブロックを実行します。

using System;
using System.Collections.Generic;

public class ForeachExample {
  public static void Main() {
    var primes = new List < int > { 2, 3, 5, 7, 11, 13, 17, 19, };
    foreach(var prime in primes) {
      Console.Write($"{prime} ");
    }
    // Console.Write($"{prime} "); --> Main.cs(10,22): error CS0103: The name `prime' does not exist in the current context
  }
}
実行結果
2 3 5 7 11 13 17 19

foreach文は上記に加え、次の条件を満たす任意の型のインスタンスを反復することができます。

  • public なパラメータなしの GetEnumerator メソッドがある型。
    • C# 9.0 以降、GetEnumerator メソッドは型の拡張メソッドにすることができます。
  • GetEnumerator メソッドの戻値はpublicなCurrentプロパティと、戻値がboolのpublicなパラメータなしの MoveNext メソッドを持っています。
await foreach[編集]

await foreachはC# 8.0から導入された機能で、非同期的にforeachループを実行するためのものです。以下に、await foreachを使った例を示します。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

public class Program {
  public static async Task Main() {
    List<int> numbers = new() { 1, 2, 3, 4, 5 };

    // 同期的な foreach
    foreach(var number in numbers) {
      Console.WriteLine(number);
    }

    // 非同期的な foreach
    await foreach(var number in GetAsyncNumbers()) {
      Console.WriteLine(number);
    }
  }

  // 非同期的なデータを生成するメソッド
  public static async IAsyncEnumerable < int > GetAsyncNumbers() {
    await Task.Delay(100); // 例として遅延を追加
    yield return 6;
    await Task.Delay(100);
    yield return 7;
    await Task.Delay(100);
    yield return 8;
    // 他の非同期処理も可能
  }
}

この例では、List<int>に対して通常の同期的なforeachループと、IAsyncEnumerable<int>を返す非同期メソッドGetAsyncNumbers()に対するawait foreachループを示しています。

GetAsyncNumbers()メソッドはIAsyncEnumerable<int>を使用して、非同期的にデータを生成しています。await foreachを使うことで、GetAsyncNumbers()から非同期的にデータを取得し、非同期処理を実行できます。

while文[編集]

while文は、指定されたブール式が真と評価される間、文または文のブロックを実行します。 条件式は、ループの各実行前に評価されるため、whileループは0回以上実行されます。while文は、1 回以上実行するdo文とは異なります。

public class WhileStmtExample {
  public static void Main() {
    int n = 1;
    while (n < 10) {
      System.Console.Write(n);
      n += n;
    }
    System.Console.WriteLine();
    
    while (n < 10) {
      System.Console.Write(n);
      n += n;
    }
  }
}
実行結果
1248
10 行目からの while 文の条件は最初から成立していませんので、1回も実行されません。

反復子[編集]

反復子( iterator )は、リストや配列のようなコレクションを操作するために使用されます。

反復子メソッドまたは get アクセサを使用すると、コレクションに対して独自の反復処理を行います。反復子メソッドは、yield returnステートメントを使用して、各要素を一度に1つずつ返します。yield return文に到達すると、コード内の現在の位置が記憶されます。次に反復子メソッドを呼出すと、その位置から実行が再開されます。

クライアントコードから反復子を消費するには、foreach文またはLINQクエリを使用します。

foreach[編集]

foreach文の例
using System;
using System.Collections;

class Program {
  static void Main() {
    int n = 0;
    foreach(var prime in primesNumbers()) {
      Console.Write($"{prime} ");
      n++;
      if (n > 20) {
        break;
      }
    }
  }
  public static System.Collections.IEnumerable primesNumbers() {
    var primes = new ArrayList();
    for (var i = 2;; i++) {
      var flag = true;
      foreach(int prime in primes) {
        if (i % prime == 0) {
          flag = false;
          break;
        }
      }
      if (flag) {
        primes.Add(i);
        yield return i;
      }
    }
  }
}
実行結果
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73
foreachループの最初の反復により、最初のyield return文に到達するまで、primesNumbers反復メソッドで実行が進められる。この繰返しでは、2が返され、イテレータメソッド内の現在の位置が保持されます。ループの次の繰返しでは、イテレータメソッドの実行は中断したところから続き、再びyield return文に到達すると停止します。この繰返しでは、3が返され、イテレータメソッドの現在の位置が再び保持されます。素数は無数にあるので、この反復メソッドは整数の幅の範囲で永遠に値を返し続けます。

LINQ[編集]

LINQを使用して素数を生成するコードに書き換えてみます。

LINQの例
using System;
using System.Collections.Generic;
using System.Linq;

class Program {
  static void Main() {
    var primes = PrimesNumbers().Take(20);
    foreach(var prime in primes) {
      Console.Write($"{prime} ");
    }
  }

  public static IEnumerable < int > PrimesNumbers() {
    var primes = new List < int > ();
    for (var i = 2;; i++) {
      if (primes.All(p => i % p != 0)) {
        primes.Add(i);
        yield return i;
      }
    }
  }
}
実行結果
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73

このコードでは、LINQのメソッドであるTake()を使用して最初の20個の素数を取得しています。また、All()メソッドを使用して、primesリストの全ての要素が条件を満たすかどうかを判定しています。このコードでは、primesリストに新しい素数を追加し、yield returnで素数を返すPrimesNumbers()メソッドを作成しています。

LINQ版は、内部的にAll()メソッドを使って素数の判定を行っており、より直感的でシンプルなコードになっています。また、ArrayListではなくList<int>を使用しており、ジェネリックなリストを使うことで型安全性が向上しています。