C#を用いてアプリケーションを開発する際、データの待機列を管理するために「Queue(キュー)」というデータ構造は欠かせない存在です。

キューは「先入れ先出し(FIFO: First-In, First-Out)」の原則に基づき、最初に追加された要素から順番に処理を行う仕組みを持っています。

このキュー操作において、要素を取り出す前に「次に処理される要素が何か」を一時的に確認したい場面が多々あります。

そこで活用されるのがPeekメソッドです。

本記事では、Queue.Peekメソッドの基本的な使い方から、Dequeueとの違い、実務で役立つエラー回避策まで、プロフェッショナルな視点で詳しく解説します。

QueueクラスとPeekメソッドの基本概念

C#のSystem.Collections.Generic名前空間に含まれるQueue<T>クラスは、要素を順番に並べて保持するためのコレクションです。

キューの操作には主に、要素を追加するEnqueue、要素を取り出して削除するDequeue、そして今回フォーカスするPeekがあります。

Peekメソッドの最大の特徴は、キューの先頭にある要素を「削除せずに」参照できる点にあります。

例えば、印刷ジョブの待機列において、次に印刷される文書の名前だけを確認したい場合、その文書を列から除外してはいけません。

このような「中身をのぞき見る」動作を実現するのがPeekの役割です。

Peekメソッドの定義

Queue<T>.Peekメソッドの定義は以下のようになっています。

C#
public T Peek();

戻り値はキューの先頭にあるオブジェクトです。

型引数Tによって、キューに格納されているデータ型がそのまま返されます。

もしキューが空の状態でこのメソッドを呼び出すと、InvalidOperationExceptionが発生するため、実装時には注意が必要です。

Peekメソッドの具体的な使い方

まずは、最も基本的なPeekメソッドの使用例を見ていきましょう。

以下のプログラムでは、文字列型のキューを作成し、要素の確認と削除の挙動を比較しています。

C#
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // キューの初期化
        Queue<string> taskQueue = new Queue<string>();

        // 要素の追加 (Enqueue)
        taskQueue.Enqueue("タスクA");
        taskQueue.Enqueue("タスクB");
        taskQueue.Enqueue("タスクC");

        Console.WriteLine($"現在のキューの数: {taskQueue.Count}");

        // 先頭要素の確認 (Peek)
        // 要素は削除されません
        string nextTask = taskQueue.Peek();
        Console.WriteLine($"Peekで確認した要素: {nextTask}");
        Console.WriteLine($"Peek後のキューの数: {taskQueue.Count}");

        // 先頭要素の取り出し (Dequeue)
        // 要素が削除されます
        string processedTask = taskQueue.Dequeue();
        Console.WriteLine($"Dequeueで取り出した要素: {processedTask}");
        Console.WriteLine($"Dequeue後のキューの数: {taskQueue.Count}");
        
        // 再度Peekで次の要素を確認
        Console.WriteLine($"次のPeekで確認した要素: {taskQueue.Peek()}");
    }
}
実行結果
現在のキューの数: 3
Peekで確認した要素: タスクA
Peek後のキューの数: 3
Dequeueで取り出した要素: タスクA
Dequeue後のキューの数: 2
次のPeekで確認した要素: タスクB

このコードから分かるように、Peekを呼び出した後もCount(要素数)は変わっていません。

これにより、「特定の条件を満たしている場合のみDequeueを実行する」といった条件分岐が可能になります。

PeekとDequeueの違いを整理する

PeekDequeueはどちらもキューの先頭にアクセスするメソッドですが、その副作用の有無に決定的な違いがあります。

以下の表でその違いを比較してみましょう。

特徴PeekメソッドDequeueメソッド
役割先頭要素を参照する先頭要素を取得し、削除する
要素数の変化変化しない1つ減る
戻り値先頭にある要素先頭にあり、削除された要素
主な用途次の処理内容の事前確認実際のデータ処理・消化
例外発生条件キューが空の場合キューが空の場合

実務レベルの開発では、「Peekで内容を確認し、ビジネスロジックに合致していればDequeueで処理を開始する」というフローが一般的です。

空のキューに対する安全な操作方法

前述の通り、空のキューに対してPeekを実行するとプログラムは例外を投げて停止してしまいます。

これを防ぐためには、主に2つのアプローチがあります。

1. Countプロパティによるチェック

最も古典的で分かりやすい方法は、Countプロパティを参照して要素が存在するかを確認することです。

C#
if (taskQueue.Count > 0)
{
    var item = taskQueue.Peek();
    // 処理を継続
}
else
{
    Console.WriteLine("キューは空です。");
}

2. TryPeekメソッドの活用

.NET Core 2.0以降や.NET Framework 4.7.1以降では、より安全で効率的なTryPeekメソッドが導入されました。

このメソッドは、要素の取得に成功したかどうかをbool値で返し、要素自体はout引数で受け取ります。

C#
if (taskQueue.TryPeek(out string result))
{
    Console.WriteLine($"取得成功: {result}");
}
else
{
    Console.WriteLine("キューが空のため取得できませんでした。");
}

TryPeekを使用することで、例外処理(try-catch)のオーバーヘッドを避けつつ、スッキリとしたコードを記述できるようになります。

実践的な活用シーン:優先度や条件の判定

なぜPeekが必要なのか、具体的な業務シナリオを想定してみましょう。

例えば、メッセージキューからメッセージを取得する際、そのメッセージの「優先度」や「有効期限」をチェックし、まだ処理すべきタイミングではない(待機が必要な)場合に役立ちます。

C#
using System;
using System.Collections.Generic;

public class Job
{
    public string Name { get; set; }
    public DateTime ScheduledTime { get; set; }
}

class Program
{
    static void Main()
    {
        Queue<Job> jobQueue = new Queue<Job>();
        jobQueue.Enqueue(new Job { Name = "定期メンテナンス", ScheduledTime = DateTime.Now.AddSeconds(5) });

        while (jobQueue.Count > 0)
        {
            // Peekで次のジョブの予定時刻を確認
            Job nextJob = jobQueue.Peek();

            if (DateTime.Now >= nextJob.ScheduledTime)
            {
                // 時刻が来たので取り出して実行
                Job currentJob = jobQueue.Dequeue();
                Console.WriteLine($"{currentJob.Name}を実行しました。");
            }
            else
            {
                // まだ時間ではないので待機
                Console.WriteLine("実行時刻まで待機中...");
                System.Threading.Thread.Sleep(1000);
            }
        }
    }
}

この例では、Peekを使うことで「条件を満たさない限り、キューの中にジョブを残し続ける」という制御を実現しています。

もしDequeueを使っていれば、条件外だった場合に一度取り出したジョブを再度キューの最後尾に入れ直す(再エンキュー)という無駄な操作が発生してしまいます。

マルチスレッド環境での注意点:ConcurrentQueue

通常のQueue<T>クラスはスレッドセーフではありません。

複数のスレッドから同時にPeekEnqueueを行うと、データの整合性が崩れる可能性があります。

マルチスレッド環境(パラレル処理など)でキューを使用する場合は、System.Collections.Concurrent名前空間にあるConcurrentQueue<T>を使用してください。

ConcurrentQueue<T>にはPeekメソッドの代わりに、スレッドセーフなTryPeekメソッドが用意されています。

C#
using System.Collections.Concurrent;

ConcurrentQueue<int> concurrentQueue = new ConcurrentQueue<int>();
concurrentQueue.Enqueue(100);

// スレッドセーフに先頭を確認
if (concurrentQueue.TryPeek(out int val))
{
    Console.WriteLine($"スレッドセーフに確認: {val}");
}

パフォーマンスに関する考察

Queue.Peekメソッドの計算量はO(1)です。

内部的には配列(または連結リスト)の特定のインデックスを参照するだけであるため、キューの要素数が100万個あっても、10個であっても、処理速度は変わりません。

しかし、ループ内で頻繁にPeekを呼び出す構成にする場合、その後の条件判定やスリープ処理を含めた設計全体がパフォーマンスに影響します。

特にポーリング(監視)処理で使用する場合は、CPUリソースを過度に消費しないよう、適切な待機時間を設けることが推奨されます。

まとめ

C#のQueue.Peekメソッドは、「キューの先頭要素を削除せずにのぞき見る」という、シンプルながらも非常に強力な機能を提供します。

本記事のポイントを振り返ります。

  • 非破壊的参照:要素を削除せずに取得できるため、事前の条件判定に最適。
  • 例外への配慮:空のキューに対しては例外が発生するため、CountチェックやTryPeekを併用する。
  • 適切な使い分け:単にデータを取り出すならDequeue、中身を確認するならPeek
  • スレッドセーフ:多人数・多スレッドでの利用にはConcurrentQueueを検討する。

これらの特性を理解し、適切に使い分けることで、バグが少なくメンテナンス性の高いプログラムを構築することができます。

C#におけるコレクション操作の基礎として、ぜひPeekメソッドをマスターしてください。