C#を用いた開発において、データ構造の操作は避けて通れない重要な要素です。

その中でも、スタックやキュー、ストリームといったデータを扱う際に頻繁に登場するのがPeekメソッドです。

このメソッドは、コレクション内のデータを「取り出す」のではなく、「次に処理されるデータを覗き見る」という独自の役割を持っています。

プログラムのロジックを構築する際、次に控えているデータの内容に応じて処理を分岐させたい場面は多々あります。

しかし、一度取り出してしまうと元の状態に戻すのが手間になるため、Peekメソッドの正しい理解と活用は、コードの可読性やメンテナンス性を向上させるために不可欠です。

本記事では、Stack、Queue、そしてStreamReaderにおけるPeekメソッドの使い方から、注意点、例外処理までを詳しく解説します。

Peekメソッドとは何か

Peek(ピーク)という言葉には「そっと覗く」「ちらっと見る」という意味があります。

プログラミングにおけるPeekメソッドもその名の通り、データ構造の先頭にある要素を削除せずに参照する操作を指します。

通常、スタックであればPop、キューであればDequeueというメソッドを使用してデータを取り出しますが、これらは「取得と同時に削除」が行われます。

一方、Peekを使用すれば、現在の状態を維持したまま、次にどのようなデータが来るのかを確認できるため、条件分岐の判断材料として非常に有用です。

StackクラスにおけるPeekメソッド

Stack(スタック)は、LIFO (Last-In, First-Out:後入れ先出し)のデータ構造です。

最後に追加された要素が、最初に取り出される対象となります。

Stack<T>.Peek()の基本動作

StackにおけるPeekメソッドは、スタックの最上部(最後に追加された要素)を返します。

この操作によってスタックの内容が書き換わることはありません。

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

class Program
{
    static void Main()
    {
        // 文字列型のスタックを作成
        Stack<string> history = new Stack<string>();

        // 要素を追加 (Push)
        history.Push("Page 1");
        history.Push("Page 2");
        history.Push("Page 3");

        // 最上部の要素を覗き見る (Peek)
        string topItem = history.Peek();
        Console.WriteLine($"Peekの結果: {topItem}");
        Console.WriteLine($"Peek後のスタックの数: {history.Count}");

        // 要素を完全に取り出す (Pop)
        string poppedItem = history.Pop();
        Console.WriteLine($"Popされた要素: {poppedItem}");
        Console.WriteLine($"Pop後のスタックの数: {history.Count}");
    }
}
実行結果
Peekの結果: Page 3
Peek後のスタックの数: 3
Popされた要素: Page 3
Pop後のスタックの数: 2

StackでPeekを活用するシーン

スタックでのPeekは、主に「やり直し (Undo) 機能」や、構文解析(パーサー)の実装などで役立ちます。

例えば、特定の記号がスタックのトップにある場合のみ特定の処理を行いたいが、条件に合わない場合はそのままにしておきたい、といったケースで威力を発揮します。

QueueクラスにおけるPeekメソッド

Queue(キュー)は、FIFO (First-In, First-Out:先入れ先出し)のデータ構造です。

最初に追加された要素が、最初に取り出される対象となります。

Queue<T>.Peek()の基本動作

QueueにおけるPeekメソッドは、キューの先頭(最初に追加され、次に取り出される予定の要素)を返します。

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

class Program
{
    static void Main()
    {
        // 数値型のキューを作成
        Queue<int> taskQueue = new Queue<int>();

        // 要素を追加 (Enqueue)
        taskQueue.Enqueue(101);
        taskQueue.Enqueue(102);
        taskQueue.Enqueue(103);

        // 先頭の要素を確認
        int nextTask = taskQueue.Peek();
        Console.WriteLine($"次のタスクID: {nextTask}");
        Console.WriteLine($"現在のキューの数: {taskQueue.Count}");

        // 先頭を取り出して処理 (Dequeue)
        int processedTask = taskQueue.Dequeue();
        Console.WriteLine($"処理されたタスク: {processedTask}");
    }
}
実行結果
次のタスクID: 101
現在のキューの数: 3
処理されたタスク: 101

QueueでPeekを活用するシーン

キューでのPeekは、「処理の準備が整っているかを確認する」際に便利です。

例えば、メッセージキューからデータを受け取る際、先頭のメッセージのヘッダー情報をPeekで確認し、まだ処理すべきタイミングでなければキューに残したまま待機する、といった制御が可能になります。

StreamReaderにおけるPeekメソッド

コレクションクラス以外にも、System.IO.StreamReaderクラスにPeekメソッドが存在します。

これはファイルの読み込みなどにおいて、次に読み込む文字があるかどうか、あるいは次の文字が何かを確認するために使用されます。

StreamReader.Peek()の特徴

StreamReaderのPeekは、コレクションのPeekとは戻り値の型や挙動が少し異なります。

  • 戻り値: 次に読み取られる文字をint型で返します。
  • 終端の判定: 読み取る文字がない(ストリームの末尾に達した)場合、-1を返します。
  • 動作: ストリームの位置(ポインタ)を進めずに、次の1文字を取得します。
C#
using System;
using System.IO;
using System.Text;

class Program
{
    static void Main()
    {
        string text = "Hello";
        byte[] byteArray = Encoding.UTF8.GetBytes(text);
        using (MemoryStream stream = new MemoryStream(byteArray))
        using (StreamReader reader = new StreamReader(stream))
        {
            // 次の文字がある間ループ
            while (reader.Peek() >= 0)
            {
                char c = (char)reader.Read();
                Console.WriteLine($"読んだ文字: {c}, 次のPeek値: {reader.Peek()}");
            }
        }
    }
}
実行結果
読んだ文字: H, 次のPeek値: 101
読んだ文字: e, 次のPeek値: 108
読んだ文字: l, 次のPeek値: 108
読んだ文字: l, 次のPeek値: 111
読んだ文字: o, 次のPeek値: -1

なぜStreamReaderでPeekを使うのか

ファイルの終端を判定する方法としては、EndOfStreamプロパティを参照するのが一般的ですが、特定の文字が現れるまで読み進めたい場合や、1文字先を見てパース処理を分岐させたい場合にはPeekが適しています。

安全なPeek操作:TryPeekメソッドの活用

StackやQueueでPeek()を呼び出す際、もしコレクションが空であれば、プログラムはInvalidOperationExceptionをスローして停止してしまいます。

これを防ぐために、.NETではより安全なTryPeekメソッドが提供されています。

TryPeekの使い方

TryPeekは、要素の取得に成功したかどうかをbool型で返し、実際の値はoutパラメータを通じて取得します。

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

class Program
{
    static void Main()
    {
        Queue<string> names = new Queue<string>();

        // 空の状態でPeekを試みる
        if (names.TryPeek(out string result))
        {
            Console.WriteLine($"取得成功: {result}");
        }
        else
        {
            Console.WriteLine("キューは空です。安全に処理を続行します。");
        }
    }
}

PeekとTryPeekの使い分け表

メソッド戻り値空の場合の挙動推奨されるシーン
Peek()要素の型例外をスロー事前に Count > 0 を確認している場合
TryPeek(out T result)boolfalseを返す安全に処理を行いたい場合、マルチスレッド環境

応用:PriorityQueueでのPeek

.NET 6から導入されたPriorityQueue<TElement, TPriority>でもPeekメソッドが利用可能です。

これは優先度付きキューにおいて、最も優先度の高い要素を確認するために使用されます。

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

class Program
{
    static void Main()
    {
        // 優先度(int)が低いほど優先されるキュー
        PriorityQueue<string, int> pq = new PriorityQueue<string, int>();

        pq.Enqueue("低優先度タスク", 3);
        pq.Enqueue("緊急タスク", 1);
        pq.Enqueue("通常タスク", 2);

        // 最も優先度の高い(数値が小さい)要素を確認
        if (pq.TryPeek(out string element, out int priority))
        {
            Console.WriteLine($"次回の処理対象: {element} (優先度: {priority})");
        }
    }
}
実行結果
次回の処理対象: 緊急タスク (優先度: 1)

Peekメソッドを使用する際の注意点

Peekメソッドは非常に便利ですが、使用にあたってはいくつかの重要な注意点があります。

1. 例外処理(空のコレクション)

先述の通り、空のStackやQueueに対してPeek()を呼び出すとInvalidOperationExceptionが発生します。

常に要素が存在することを保証するか、TryPeekを使用する癖をつけましょう。

2. マルチスレッド環境での競合

PeekをしてからPopDequeueをするまでの間に、別のスレッドが要素を削除してしまう可能性があります。

スレッドセーフな操作が必要な場合は、ConcurrentStackConcurrentQueueを使用し、それらが提供するTryPeekをアトミックな操作として活用する必要があります。

3. パフォーマンス

Peek自体の計算量はO(1)、つまり定数時間で完了します。

要素数が増えても処理速度に影響を与えない非常に軽量なメソッドです。

しかし、ループ内で不必要にPeekを繰り返すようなロジックは、コードの複雑さを招くため注意が必要です。

4. 参照型のミュータビリティ

Peekで取得したオブジェクトが参照型の場合、そのオブジェクトのプロパティを変更すると、コレクション内にある実体も変更されます。

「覗き見ているだけだから変更されない」と思い込むのは危険です。

あくまで「コレクションからの削除が行われない」だけであり、オブジェクトそのものの状態操作は通常通り行えてしまいます。

Peekメソッドの活用パターンまとめ

C#におけるPeekメソッドの活用パターンを整理しました。

条件付き取り出し

「次に処理すべきデータが特定の条件を満たす場合のみ取り出す」というロジックに最適です。

C#
// 例:特定のIDを持つメッセージが来るまで待機
while (queue.TryPeek(out var message) && message.Id != targetId)
{
    // targetIdではないので、一旦保留するなどの処理
}

再帰的なアルゴリズムの簡略化

木構造の探索や計算式の解析において、スタックのトップを維持したまま子要素の処理を進める際にPeekが多用されます。

ストリームの先読み判定

StreamReader等で、次の文字が改行コードなのか、あるいは特定の区切り文字(カンマなど)なのかを判断し、読み込みバッファのサイズを調整する際に役立ちます。

まとめ

C#のPeekメソッドは、データ構造の先頭にある要素を「削除せずに参照する」ための非常に強力なツールです。

Stack、Queue、StreamReader、PriorityQueueといった様々なクラスで提供されており、適切に使い分けることで、効率的かつ安全なデータ処理を実現できます。

主なポイントを振り返ると以下の通りです。

  • Stack/Queue: 次に処理する要素を削除せずに確認する。
  • StreamReader: 次の文字コードを整数で返す(終端は-1)。
  • 安全策: 空のコレクションによる例外を避けるため、TryPeekの利用を検討する。
  • 注意点: マルチスレッド環境での競合や、参照型オブジェクトの変更には留意する。

データの流れを制御する際に「一歩先を見る」ことができるPeekメソッドをマスターし、より堅牢でスマートなC#プログラミングを目指しましょう。