入出力

【C#】Console.ReadLineメソッドの基本操作と入力を安全に扱うコツ

Console.ReadLineはコンソールから1行の文字列を取得するメソッドで、ユーザーがEnterを押すまで処理を待ちます。

何も入力せずEnterなら空文字、入力ストリームが終端ならnullを返します。

数値を扱う場合はint.Parseint.TryParseでの変換が必要です。

目次から探す
  1. Console.ReadLineとは
  2. 基本的な入力パターン
  3. 文字列入力の細やかな扱い
  4. 数値入力への変換手順
  5. その他の型への変換例
  6. 入力検証フロー
  7. null・EOF処理の実践
  8. 複数行入力の設計
  9. 標準入力のリダイレクト応用
  10. 非同期・並行処理との併用
  11. エラーと例外のハンドリング
  12. コンソールUIのカスタマイズ
  13. クロスプラットフォーム留意点
  14. テストとデバッグのヒント
  15. よくあるミス集
  16. まとめ

Console.ReadLineとは

C#のコンソールアプリケーションでユーザーからの入力を受け取る際に、最も基本的でよく使われるメソッドがConsole.ReadLineです。

このメソッドは、ユーザーがキーボードから入力した1行分の文字列を取得し、プログラム内で利用できる形で返します。

ここでは、Console.ReadLineの基本的な仕組みや動作について詳しく解説いたします。

メソッドシグネチャと戻り値

Console.ReadLineは、System.Consoleクラスに定義されている静的メソッドです。

メソッドのシグネチャは以下のようになっています。

public static string? ReadLine();
  • 戻り値の型string?(nullable string)です。つまり、文字列を返すか、入力が終了した場合はnullを返す可能性があります
  • 引数はありません。ユーザーの入力を1行分読み取るために、特別なパラメータは不要です

このメソッドは、ユーザーがEnterキーを押すまでの間、コンソールからの入力を待機し、入力された文字列をそのまま返します。

もしユーザーが何も入力せずにEnterキーを押した場合は、空文字列("")が返されます。

また、標準入力がパイプやリダイレクトで接続されている場合や、入力の終端に達した場合はnullが返されることがあります。

これにより、プログラムは入力の終了を検知できます。

処理がブロックされるタイミング

Console.ReadLineは「ブロッキングメソッド」と呼ばれ、呼び出された時点で処理が一時停止し、ユーザーの入力を待ちます。

具体的には、以下のような流れで動作します。

  1. メソッドが呼び出されると、コンソールは標準入力ストリームから1行分の入力を待機します。
  2. ユーザーがキーボードで文字を入力し、Enterキーを押すまで処理は停止します。
  3. Enterキーが押されると、入力された文字列(改行コードは含まれません)がメソッドの戻り値として返されます。
  4. プログラムはその戻り値を受け取り、次の処理に進みます。

このため、Console.ReadLineを使うと、ユーザーの入力が完了するまでプログラムの実行が止まることになります。

ユーザーの入力を待つ必要がある対話型のコンソールアプリケーションでは非常に便利ですが、入力待ちの間は他の処理が進まない点に注意が必要です。

標準入力ストリームの基本構造

Console.ReadLineは、内部的には標準入力ストリームConsole.Inから1行分のデータを読み取っています。

標準入力ストリームは、通常はキーボードからの入力に接続されていますが、パイプやファイルリダイレクトなど、他の入力ソースに切り替えることも可能です。

標準入力ストリームの特徴は以下の通りです。

  • テキストストリームとして扱われ、文字単位で読み書きが可能です
  • 入力は行単位で区切られ、Console.ReadLineは1行分の文字列を取得します
  • 入力の終端(EOF)に達すると、Console.ReadLinenullを返します。これは、例えばファイルの終わりやパイプの終了を意味します
  • Windows環境では、ユーザーがCTRL+Zを押してEnterを押すとEOFが送信され、LinuxやmacOSではCTRL+Dが同様の役割を果たします

この仕組みを理解しておくと、Console.ReadLineがどのように動作しているかイメージしやすくなります。

たとえば、標準入力がファイルにリダイレクトされている場合は、ファイルの内容を1行ずつ読み取ることができ、入力が終わるとnullが返されてループを終了させることができます。

以下に、Console.ReadLineの基本的な使い方を示すサンプルコードを掲載します。

ユーザーに入力を促し、入力された文字列をそのまま表示します。

using System;
class Program
{
    static void Main()
    {
        Console.WriteLine("何か入力してください:");
        string? input = Console.ReadLine();
        if (input == null)
        {
            Console.WriteLine("入力が終了しました。");
        }
        else if (input == "")
        {
            Console.WriteLine("空の文字列が入力されました。");
        }
        else
        {
            Console.WriteLine($"入力された内容: {input}");
        }
    }
}
何か入力してください:
こんにちは
入力された内容: こんにちは

このコードでは、Console.ReadLineの戻り値がnullか空文字列か、それ以外かを判定しています。

nullの場合は入力の終了を検知し、空文字列の場合はユーザーが何も入力せずにEnterを押したことを示しています。

これにより、入力の状態に応じた適切な処理が可能になります。

基本的な入力パターン

最小コード例の概念整理

ユーザーからの入力を受け取る際、最もシンプルなコードはConsole.ReadLineを使って1行分の文字列を取得し、そのまま表示するものです。

以下のコードはその最小限の例です。

using System;
class Program
{
    static void Main()
    {
        Console.WriteLine("入力してください:");
        string? input = Console.ReadLine();
        Console.WriteLine($"入力内容: {input}");
    }
}
入力してください:
テスト
入力内容: テスト

このコードでは、Console.ReadLineがユーザーの入力を待ち、Enterキーが押されると入力内容をinputに格納します。

inputは文字列型で、null許容型string?にしているのは、入力の終端(EOF)が来た場合にnullが返る可能性があるためです。

この最小コード例のポイントは以下の通りです。

  • ユーザーの入力をそのまま受け取るため、特別な変換や検証は行わない
  • 入力がnullになる可能性を考慮していないため、EOFが発生するとnullがそのまま表示されます
  • 空文字列(何も入力せずEnterを押した場合)は空のまま表示されます

このように、最小コード例は動作確認や簡単なテストに適していますが、実際のアプリケーションでは入力の検証やエラーハンドリングが必要になることが多いです。

空文字とnullの発生条件

Console.ReadLineの戻り値には、主に3つのパターンがあります。

戻り値発生条件意味
空文字列 ("")ユーザーが何も入力せずにEnterキーを押した場合入力はあったが内容は空
文字列ユーザーが何か文字を入力しEnterキーを押した場合入力された文字列
null入力の終端(EOF)に達した場合入力が終了したことを示す
  • 空文字列は、ユーザーが何も入力せずにEnterを押したときに返ります。これは「入力はあったが内容が空」という状態です
  • nullは、標準入力がパイプやファイルリダイレクトで接続されている場合や、ユーザーがCTRL+Z(Windows)やCTRL+D(Linux/macOS)を押して入力の終端を示した場合に返ります。これにより、プログラムは入力の終了を検知できます

この違いを理解しておくことは、入力の検証やループ処理で重要です。

たとえば、nullを検知してループを終了し、空文字列は再入力を促すといった使い分けが可能です。

Trimで前後空白を除去する理由

ユーザーが入力する文字列には、意図せず前後に空白文字(スペースやタブ)が含まれることがあります。

これをそのまま扱うと、入力の比較や変換で誤動作が起きる可能性があります。

string.Trim()メソッドを使うと、文字列の先頭と末尾にある空白文字を取り除けます。

たとえば、以下のコードを見てください。

using System;
class Program
{
    static void Main()
    {
        Console.WriteLine("名前を入力してください(前後に空白を含めて入力してみてください):");
        string? input = Console.ReadLine();
        if (input != null)
        {
            string trimmedInput = input.Trim();
            Console.WriteLine($"入力前: '{input}'");
            Console.WriteLine($"Trim後: '{trimmedInput}'");
        }
    }
}
名前を入力してください(前後に空白を含めて入力してみてください):
  山田 太郎
入力前: '  山田 太郎  '
Trim後: '山田 太郎'

この例では、ユーザーが前後にスペースを含めて入力しても、Trim()を使うことで余計な空白を除去し、正確な文字列として扱えます。

前後の空白を除去する理由は以下の通りです。

  • 入力の正規化:ユーザーの入力を一定の形式に揃えることで、比較や検索が正確になります
  • 誤入力の防止:意図しない空白が原因で、条件判定や変換が失敗するのを防ぐ
  • UIの見た目向上:表示時に余計な空白があると見栄えが悪くなるため、除去して整えます

ただし、文字列の中間にある空白はTrim()では除去されません。

中間の空白を除去したい場合はReplaceや正規表現を使う必要がありますが、通常は前後の空白だけを取り除くことが多いです。

このように、Console.ReadLineで取得した文字列は、Trim()で前後の空白を除去してから処理するのが基本的なパターンとなっています。

文字列入力の細やかな扱い

大文字小文字の正規化

ユーザーからの文字列入力は、大文字と小文字の違いによって意図しない動作を招くことがあります。

たとえば、”Yes”、”YES”、”yes”は見た目は似ていますが、文字列比較を行う際には区別されるため、同じ意味として扱いたい場合は正規化が必要です。

C#では、文字列の大文字小文字を統一するためにToUpper()ToLower()メソッドを使います。

一般的には、比較や判定の前にどちらかに変換してから処理を行います。

using System;
class Program
{
    static void Main()
    {
        Console.WriteLine("はい/いいえを入力してください:");
        string? input = Console.ReadLine();
        if (input != null)
        {
            string normalized = input.Trim().ToLower(); // 小文字に統一
            if (normalized == "はい" || normalized == "yes")
            {
                Console.WriteLine("肯定の入力を受け付けました。");
            }
            else if (normalized == "いいえ" || normalized == "no")
            {
                Console.WriteLine("否定の入力を受け付けました。");
            }
            else
            {
                Console.WriteLine("不明な入力です。");
            }
        }
    }
}
はい/いいえを入力してください:
YES
肯定の入力を受け付けました。

この例では、ToLower()で入力を小文字に変換し、”yes”や”はい”と比較しています。

これにより、大文字・小文字の違いを気にせず判定できます。

なお、ToUpper()を使う場合も同様に動作します。

どちらを使うかは好みやプロジェクトのコーディング規約に合わせて選んでください。

全角・半角の統一

日本語環境では、全角文字と半角文字が混在することが多く、特に英数字や記号で全角・半角の違いがあると、文字列比較や検索で不一致が起きやすくなります。

たとえば、全角の「A」と半角の「A」は見た目は似ていますが、別の文字として扱われます。

これを防ぐために、入力文字列の全角・半角を統一する処理を行うことがあります。

C#では、Microsoft.VisualBasic名前空間のStrings.StrConvメソッドを使うと簡単に変換できます。

Visual Basicの機能ですが、C#からも利用可能です。

using System;
using Microsoft.VisualBasic;
class Program
{
    static void Main()
    {
        Console.WriteLine("英数字を入力してください(全角・半角混在可):");
        string? input = Console.ReadLine();
        if (input != null)
        {
            // 半角に変換
            string converted = Strings.StrConv(input, VbStrConv.Narrow);
            Console.WriteLine($"変換前: '{input}'");
            Console.WriteLine($"半角変換後: '{converted}'");
        }
    }
}
英数字を入力してください(全角・半角混在可):
ABC123
変換前: 'ABC123'
半角変換後: 'ABC123'

この例では、全角の英数字を半角に変換しています。

逆に半角から全角に変換したい場合は、VbStrConv.Wideを指定します。

全角・半角の統一は、ユーザー入力の正規化に役立ち、検索や比較の精度を高めます。

ただし、全角・半角の違いが意味を持つ場合は変換を控えるべきです。

特定文字禁止のチェック方法

ユーザー入力に特定の文字や記号を含めたくない場合、入力文字列に禁止文字が含まれていないかチェックする必要があります。

たとえば、ファイル名に使えない文字やSQLインジェクション対策のための記号などです。

禁止文字のチェックは、string.Containsstring.IndexOfを使う方法や、正規表現を使う方法があります。

禁止文字を配列で管理し、Containsでチェックする例

using System;
class Program
{
    static void Main()
    {
        char[] forbiddenChars = { '@', '#', '$', '%', '^', '&' };
        Console.WriteLine("文字列を入力してください(@ # $ % ^ & は使用禁止):");
        string? input = Console.ReadLine();
        if (input != null)
        {
            bool hasForbidden = false;
            foreach (char c in forbiddenChars)
            {
                if (input.Contains(c))
                {
                    hasForbidden = true;
                    Console.WriteLine($"禁止文字 '{c}' が含まれています。");
                }
            }
            if (!hasForbidden)
            {
                Console.WriteLine("入力は問題ありません。");
            }
        }
    }
}
文字列を入力してください(@ # $ % ^ & は使用禁止):
hello@world
禁止文字 '@' が含まれています。

正規表現で禁止文字を検出する例

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string pattern = @"[@#$%^&]";
        Regex regex = new Regex(pattern);
        Console.WriteLine("文字列を入力してください(@ # $ % ^ & は使用禁止):");
        string? input = Console.ReadLine();
        if (input != null)
        {
            if (regex.IsMatch(input))
            {
                Console.WriteLine("禁止文字が含まれています。");
            }
            else
            {
                Console.WriteLine("入力は問題ありません。");
            }
        }
    }
}
文字列を入力してください(@ # $ % ^ & は使用禁止):
safeinput
入力は問題ありません。

禁止文字のチェックは、ユーザーの誤入力防止やセキュリティ対策として重要です。

禁止文字のリストは用途に応じて柔軟に変更してください。

正規表現を使うと複雑なパターンも簡潔に表現できるため便利です。

数値入力への変換手順

int.Parseとint.TryParseの比較

ユーザーからの入力は文字列として受け取られるため、数値として扱うには文字列を数値型に変換する必要があります。

C#では整数変換に主にint.Parseint.TryParseの2つの方法がありますが、それぞれ特徴が異なります。

例外を許容するケース

int.Parseは、文字列が正しい整数形式であれば対応するint値を返しますが、変換に失敗するとFormatExceptionOverflowExceptionをスローします。

例外処理を使ってエラーを捕捉する場合に適しています。

using System;
class Program
{
    static void Main()
    {
        Console.WriteLine("整数を入力してください:");
        string? input = Console.ReadLine();
        try
        {
            int number = int.Parse(input!);
            Console.WriteLine($"入力された整数: {number}");
        }
        catch (FormatException)
        {
            Console.WriteLine("入力が整数の形式ではありません。");
        }
        catch (OverflowException)
        {
            Console.WriteLine("入力された数値がintの範囲を超えています。");
        }
    }
}
整数を入力してください:
123
入力された整数: 123
整数を入力してください:
abc
入力が整数の形式ではありません。

この方法はコードがシンプルですが、例外処理はコストが高いため、頻繁に失敗する可能性がある入力処理にはあまり向いていません。

失敗時に再入力させる流れ

int.TryParseは、変換が成功したかどうかをboolで返し、失敗しても例外をスローしません。

安全に繰り返し入力を促す場合に便利です。

using System;
class Program
{
    static void Main()
    {
        int number;
        while (true)
        {
            Console.WriteLine("整数を入力してください:");
            string? input = Console.ReadLine();
            if (input == null)
            {
                Console.WriteLine("入力が終了しました。");
                break;
            }
            if (int.TryParse(input, out number))
            {
                Console.WriteLine($"入力された整数: {number}");
                break;
            }
            else
            {
                Console.WriteLine("無効な入力です。整数を入力してください。");
            }
        }
    }
}
整数を入力してください:
abc
無効な入力です。整数を入力してください。
整数を入力してください:
100
入力された整数: 100

この方法は例外処理を使わずに済むため、ユーザーの誤入力が多い場面での入力受付に適しています。

浮動小数点の読み取り方

浮動小数点数floatdoubleを入力として受け取る場合も、TryParseメソッドを使うのが一般的です。

double.TryParseを使う例を示します。

using System;
class Program
{
    static void Main()
    {
        double value;
        while (true)
        {
            Console.WriteLine("小数を入力してください:");
            string? input = Console.ReadLine();
            if (input == null)
            {
                Console.WriteLine("入力が終了しました。");
                break;
            }
            if (double.TryParse(input, out value))
            {
                Console.WriteLine($"入力された小数: {value}");
                break;
            }
            else
            {
                Console.WriteLine("無効な入力です。小数を入力してください。");
            }
        }
    }
}
小数を入力してください:
3.14
入力された小数: 3.14

浮動小数点の入力では、ユーザーのロケールによって小数点の表記が異なる場合があるため、CultureInfoを考慮した解析が必要になることがあります。

CultureInfoを考慮した数値解析

数値の文字列変換は、文化圏(ロケール)によって小数点や桁区切りの記号が異なります。

たとえば、日本や米国では小数点は「.」ですが、ドイツなど一部の国では「,」が使われます。

TryParseParseメソッドは、IFormatProviderを受け取るオーバーロードがあり、CultureInfoを指定して解析できます。

using System;
using System.Globalization;
class Program
{
    static void Main()
    {
        Console.WriteLine("小数を入力してください(例: 3.14 または 3,14):");
        string? input = Console.ReadLine();
        if (input != null)
        {
            // 日本のカルチャー(小数点は「.」)
            CultureInfo cultureJP = new CultureInfo("ja-JP");
            // ドイツのカルチャー(小数点は「,」)
            CultureInfo cultureDE = new CultureInfo("de-DE");
            if (double.TryParse(input, NumberStyles.Float, cultureJP, out double valueJP))
            {
                Console.WriteLine($"日本の文化圏で解析: {valueJP}");
            }
            else
            {
                Console.WriteLine("日本の文化圏では無効な入力です。");
            }
            if (double.TryParse(input, NumberStyles.Float, cultureDE, out double valueDE))
            {
                Console.WriteLine($"ドイツの文化圏で解析: {valueDE}");
            }
            else
            {
                Console.WriteLine("ドイツの文化圏では無効な入力です。");
            }
        }
    }
}
小数を入力してください(例: 3.14 または 3,14):
3,14
日本の文化圏では無効な入力です。
ドイツの文化圏で解析: 3.14

この例では、同じ入力文字列を日本とドイツの文化圏で解析し、どちらで有効かを判定しています。

ユーザーの環境や入力形式に応じて適切なCultureInfoを指定することで、誤った解析を防げます。

数値入力を扱う際は、文化圏の違いを考慮し、必要に応じてCultureInfoを指定して変換処理を行うことが望ましいです。

その他の型への変換例

DateTime入力の定番パターン

日付や時刻をユーザーから入力してもらう場合、文字列をDateTime型に変換する必要があります。

DateTime.ParseDateTime.TryParseを使うのが一般的ですが、ユーザーの入力形式が多様なため、エラーを防ぐためにTryParseを使った安全な入力受付が推奨されます。

using System;
class Program
{
    static void Main()
    {
        DateTime date;
        while (true)
        {
            Console.WriteLine("日付を入力してください(例: 2024/06/01 または 2024-06-01):");
            string? input = Console.ReadLine();
            if (input == null)
            {
                Console.WriteLine("入力が終了しました。");
                break;
            }
            if (DateTime.TryParse(input, out date))
            {
                Console.WriteLine($"入力された日付: {date:yyyy-MM-dd}");
                break;
            }
            else
            {
                Console.WriteLine("無効な日付形式です。再度入力してください。");
            }
        }
    }
}
日付を入力してください(例: 2024/06/01 または 2024-06-01):
2024-06-01
入力された日付: 2024-06-01

このコードは、ユーザーが入力した文字列をDateTime.TryParseで解析し、成功すればDateTime型の値として扱います。

失敗した場合は再入力を促します。

TryParseは多くの一般的な日付形式に対応しているため、柔軟に使えます。

bool入力をyes/noで受け取る方法

真偽値boolをユーザーから「はい」「いいえ」や「yes」「no」といった文字列で受け取りたい場合、文字列を正規化して判定する方法がよく使われます。

bool.TryParsetruefalseの文字列しか受け付けないため、より柔軟な判定が必要です。

using System;
class Program
{
    static void Main()
    {
        Console.WriteLine("はい/いいえで答えてください:");
        string? input = Console.ReadLine();
        if (input != null)
        {
            string normalized = input.Trim().ToLower();
            if (normalized == "はい" || normalized == "yes" || normalized == "y")
            {
                Console.WriteLine("真(true)として扱います。");
            }
            else if (normalized == "いいえ" || normalized == "no" || normalized == "n")
            {
                Console.WriteLine("偽(false)として扱います。");
            }
            else
            {
                Console.WriteLine("不明な入力です。");
            }
        }
    }
}
はい/いいえで答えてください:
Yes
真(true)として扱います。

この方法では、入力を小文字に変換し、複数の表現を許容しています。

必要に応じて判定パターンを増やすことも可能です。

Enumにマッピングするアイデア

列挙型enumを使う場合、ユーザーの文字列入力を対応する列挙値に変換することがあります。

Enum.TryParseを使うと、文字列から列挙値への変換が簡単にできます。

using System;
enum Color
{
    Red,
    Green,
    Blue
}
class Program
{
    static void Main()
    {
        Console.WriteLine("色を入力してください(Red, Green, Blue):");
        string? input = Console.ReadLine();
        if (input != null)
        {
            if (Enum.TryParse<Color>(input.Trim(), true, out Color color))
            {
                Console.WriteLine($"選択された色: {color}");
            }
            else
            {
                Console.WriteLine("無効な色の入力です。");
            }
        }
    }
}
色を入力してください(Red, Green, Blue):
green
選択された色: Green

Enum.TryParseの第3引数にtrueを指定すると、大文字小文字を区別せずに変換できます。

これにより、ユーザーが「green」や「Green」と入力しても正しく認識されます。

また、列挙型の値をユーザーに提示する際は、Enum.GetNames(typeof(Color))を使って候補を動的に表示することも便利です。

これにより、入力ミスを減らせます。

入力検証フロー

正規表現で形式を確認する

ユーザーからの入力が特定の形式に合致しているかをチェックする際、正規表現(Regex)を使うと効率的に検証できます。

たとえば、メールアドレスや電話番号、郵便番号などのパターンを簡潔に表現できるため、入力の妥当性を高精度で判定可能です。

以下は、メールアドレスの形式を正規表現で検証する例です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        Console.WriteLine("メールアドレスを入力してください:");
        string? input = Console.ReadLine();
        if (input != null)
        {
            string pattern = @"^[\w\.-]+@[\w\.-]+\.\w+$";
            Regex regex = new Regex(pattern);
            if (regex.IsMatch(input))
            {
                Console.WriteLine("有効なメールアドレス形式です。");
            }
            else
            {
                Console.WriteLine("無効なメールアドレス形式です。");
            }
        }
    }
}
メールアドレスを入力してください:
example@test.com
有効なメールアドレス形式です。

この例では、Regex.IsMatchメソッドを使い、入力文字列がメールアドレスの基本的なパターンに合致するかを判定しています。

正規表現は用途に応じて複雑にできますが、過度に複雑にすると誤判定やパフォーマンス低下の原因になるため、必要な範囲でシンプルに保つことが望ましいです。

カスタムバリデーション関数の構築

正規表現だけでは対応しきれない複雑な検証や、複数条件を組み合わせたチェックが必要な場合は、カスタムバリデーション関数を作成して入力を検証します。

関数に分けることで再利用性が高まり、コードの見通しも良くなります。

以下は、パスワードの強度をチェックするカスタム関数の例です。

条件は「8文字以上」「英大文字を含む」「数字を含む」としています。

using System;
using System.Text.RegularExpressions;
class Program
{
    static bool ValidatePassword(string password)
    {
        if (password.Length < 8)
            return false;
        if (!Regex.IsMatch(password, @"[A-Z]"))
            return false;
        if (!Regex.IsMatch(password, @"\d"))
            return false;
        return true;
    }
    static void Main()
    {
        Console.WriteLine("パスワードを入力してください(8文字以上、大文字と数字を含む):");
        string? input = Console.ReadLine();
        if (input != null)
        {
            if (ValidatePassword(input))
            {
                Console.WriteLine("パスワードは有効です。");
            }
            else
            {
                Console.WriteLine("パスワードの条件を満たしていません。");
            }
        }
    }
}
パスワードを入力してください(8文字以上、大文字と数字を含む):
Passw0rd
パスワードは有効です。

このように、複数の条件を組み合わせて判定する場合は、カスタム関数にまとめると管理しやすくなります。

ループを用いた再入力ロジック

ユーザーの入力が不正な場合に再度入力を促すには、ループを使って検証と入力受付を繰り返す方法が一般的です。

これにより、正しい入力が得られるまで処理を継続できます。

以下は、メールアドレスの形式チェックを行い、正しい形式が入力されるまで繰り返す例です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static bool IsValidEmail(string email)
    {
        string pattern = @"^[\w\.-]+@[\w\.-]+\.\w+$";
        return Regex.IsMatch(email, pattern);
    }
    static void Main()
    {
        string? input;
        while (true)
        {
            Console.WriteLine("メールアドレスを入力してください:");
            input = Console.ReadLine();
            if (input == null)
            {
                Console.WriteLine("入力が終了しました。");
                break;
            }
            if (IsValidEmail(input))
            {
                Console.WriteLine("有効なメールアドレスが入力されました。");
                break;
            }
            else
            {
                Console.WriteLine("無効なメールアドレス形式です。再度入力してください。");
            }
        }
    }
}
メールアドレスを入力してください:
invalid-email
無効なメールアドレス形式です。再度入力してください。
メールアドレスを入力してください:
user@example.com
有効なメールアドレスが入力されました。

このように、ループ内で検証関数を呼び出し、条件を満たすまで繰り返すことで、ユーザーに正しい入力を促せます。

nullが返された場合は入力終了とみなしてループを抜ける処理も重要です。

null・EOF処理の実践

パイプ使用時の終端検出

コンソールアプリケーションで標準入力をパイプで受け取る場合、入力の終端(EOF: End Of File)を正しく検知することが重要です。

Console.ReadLineは、入力が終了するとnullを返します。

これを利用して、パイプからの入力が完了したことを判別できます。

たとえば、以下のようなコードでパイプ入力を受け取り、nullが返るまでループで読み込みを続けることができます。

using System;
class Program
{
    static void Main()
    {
        Console.WriteLine("パイプ入力を受け付けます。Ctrl+Z(Windows)またはCtrl+D(Linux/macOS)で終了。");
        string? line;
        while ((line = Console.ReadLine()) != null)
        {
            Console.WriteLine($"読み取った行: {line}");
        }
        Console.WriteLine("入力の終端に達しました。");
    }
}
> echo Hello World | dotnet run
パイプ入力を受け付けます。Ctrl+Z(Windows)またはCtrl+D(Linux/macOS)で終了。
読み取った行: Hello World
入力の終端に達しました。

この例では、echoコマンドの出力をパイプで渡し、Console.ReadLinenullを返すまで読み込みを続けています。

パイプの入力が終了するとnullが返り、ループを抜けて処理を終了します。

パイプ入力では、ユーザーが直接キーボードから入力するわけではないため、EOFの検知は自動的に行われます。

これにより、ファイルや他のコマンドの出力を連続して処理することが可能です。

ファイルリダイレクトでのnull判定

ファイルリダイレクトを使って標準入力にファイルの内容を渡す場合も、Console.ReadLineはファイルの終端に達するとnullを返します。

これを利用してファイルの全行を読み込むことができます。

以下は、ファイルリダイレクトで渡されたテキストを1行ずつ読み込み、内容を表示する例です。

using System;
class Program
{
    static void Main()
    {
        Console.WriteLine("ファイルリダイレクトからの入力を読み取ります。");
        string? line;
        while ((line = Console.ReadLine()) != null)
        {
            Console.WriteLine($"読み取った行: {line}");
        }
        Console.WriteLine("ファイルの終端に達しました。");
    }
}
> dotnet run < sample.txt
ファイルリダイレクトからの入力を読み取ります。
読み取った行: これはサンプルテキストです。
読み取った行: 2行目の内容です。
読み取った行: 最終行です。
ファイルの終端に達しました。

このように、ファイルリダイレクト時もnull判定で入力の終了を検知できるため、ループ処理で安全に全行を読み込めます。

なお、ファイルの内容が空の場合は最初のConsole.ReadLinenullが返るため、入力がないことも判別可能です。

nullの判定は、標準入力がどのようなソースであっても共通の終了検知手段として使えます。

パイプやファイルリダイレクトを活用したコンソールアプリケーションでは、Console.ReadLineの戻り値がnullになるタイミングを正しく扱うことが安定した動作の鍵となります。

複数行入力の設計

Whileループによる連続読み取り

複数行の入力を受け取る場合、Console.ReadLineを繰り返し呼び出して1行ずつ読み取るのが基本です。

whileループを使うことで、ユーザーが入力を終了するまで連続して読み込みを続けられます。

終了条件を空行に設定する

ユーザーが空行(何も入力せずにEnterを押す)を入力したら入力終了とする方法はシンプルでわかりやすいです。

空行を検知してループを抜けることで、複数行の入力を受け付けられます。

using System;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        Console.WriteLine("複数行のテキストを入力してください。空行で終了します。");
        List<string> lines = new List<string>();
        string? line;
        while (true)
        {
            line = Console.ReadLine();
            if (line == null || line == "")
            {
                // 空行またはEOFで入力終了
                break;
            }
            lines.Add(line);
        }
        Console.WriteLine("入力された内容:");
        foreach (var l in lines)
        {
            Console.WriteLine(l);
        }
    }
}
複数行のテキストを入力してください。空行で終了します。
こんにちは
これはテストです。
入力された内容:
こんにちは
これはテストです。

この例では、空行が入力されるまでConsole.ReadLineで読み取りを続け、入力された行をリストに保存しています。

空行が来た時点でループを抜けて、まとめて表示しています。

行数上限を設けるアプローチ

無制限に入力を受け付けるとメモリを大量に消費したり、誤操作で永遠に入力待ちになる可能性があります。

そこで、最大行数を設定して上限に達したら自動的に入力を終了させる方法があります。

using System;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        const int maxLines = 5;
        Console.WriteLine($"複数行のテキストを入力してください。最大{maxLines}行まで。空行で早期終了可能です。");
        List<string> lines = new List<string>();
        string? line;
        while (lines.Count < maxLines)
        {
            line = Console.ReadLine();
            if (line == null || line == "")
            {
                break;
            }
            lines.Add(line);
        }
        Console.WriteLine("入力された内容:");
        foreach (var l in lines)
        {
            Console.WriteLine(l);
        }
    }
}
複数行のテキストを入力してください。最大5行まで。空行で早期終了可能です。
1行目
2行目
3行目
4行目
5行目
6行目
入力された内容:
1行目
2行目
3行目
4行目
5行目

このコードでは、lines.CountmaxLinesに達すると自動的にループを抜けます。

これにより、入力の上限を制御でき、過剰な入力を防止できます。

StringBuilderでメモリ効率を上げる

複数行のテキストをまとめて扱う場合、List<string>に行ごとに格納してから結合する方法もありますが、文字列の連結が多いとメモリ効率が悪くなります。

StringBuilderを使うと効率的に文字列を連結できます。

using System;
using System.Text;
class Program
{
    static void Main()
    {
        Console.WriteLine("複数行のテキストを入力してください。空行で終了します。");
        StringBuilder sb = new StringBuilder();
        string? line;
        while (true)
        {
            line = Console.ReadLine();
            if (line == null || line == "")
            {
                break;
            }
            sb.AppendLine(line);
        }
        Console.WriteLine("入力された内容:");
        Console.WriteLine(sb.ToString());
    }
}
複数行のテキストを入力してください。空行で終了します。
こんにちは
複数行の入力テストです。
入力された内容:
こんにちは
複数行の入力テストです。

StringBuilder.AppendLineは行末に自動で改行コードを追加するため、複数行のテキストを効率よくまとめられます。

大量の文字列を連結する場合は、StringBuilderを使うことでパフォーマンスとメモリ使用量の改善が期待できます。

標準入力のリダイレクト応用

コマンドパイプラインとの連携

C#のコンソールアプリケーションでは、標準入力を他のコマンドの出力にパイプで接続することで、複数のコマンドを連携させた処理が可能です。

パイプラインを使うと、前段のコマンドの結果をリアルタイムに受け取り、プログラム内で処理できます。

たとえば、WindowsのdirコマンドやLinuxのlsコマンドの出力をパイプで渡し、C#プログラムで1行ずつ読み取る例を示します。

using System;
class Program
{
    static void Main()
    {
        string? line;
        while ((line = Console.ReadLine()) != null)
        {
            Console.WriteLine($"受け取った行: {line}");
        }
    }
}

このプログラムをPipeExample.exeとしてビルドした場合、以下のようにパイプで接続できます。

dir | PipeExample.exe

またはLinux/macOS環境では、

ls | dotnet PipeExample.dll

パイプで渡された標準入力は、Console.ReadLineで1行ずつ読み取れます。

パイプの終端に達するとnullが返るため、ループを抜けて処理を終了します。

この方法を使うと、他のコマンドの出力をフィルタリングしたり、加工したりするツールを簡単に作成できます。

たとえば、ログファイルの特定行だけを抽出して解析するなどの用途に便利です。

巨大ファイルからの一括読み取り

標準入力をファイルリダイレクトで接続し、大きなファイルを一括で読み込むことも可能です。

ファイルの内容を1行ずつ処理することで、メモリを節約しつつ効率的にデータを扱えます。

以下は、ファイルリダイレクトで渡されたテキストを1行ずつ読み込み、行数をカウントする例です。

using System;
class Program
{
    static void Main()
    {
        int lineCount = 0;
        string? line;
        while ((line = Console.ReadLine()) != null)
        {
            lineCount++;
            // 必要に応じて行ごとの処理をここに記述
        }
        Console.WriteLine($"読み込んだ行数: {lineCount}");
    }
}

このプログラムをCountLines.exeとしてビルドし、巨大なテキストファイルlargefile.txtをリダイレクトで渡す例です。

CountLines.exe < largefile.txt

またはLinux/macOS環境では、

dotnet CountLines.dll < largefile.txt

この方法は、ファイル全体をメモリに読み込まずに済むため、巨大ファイルの処理に適しています。

1行ずつ処理することで、メモリ使用量を抑えつつ必要な解析や変換を行えます。

さらに、パイプラインと組み合わせて、圧縮ファイルの解凍コマンドやフィルタリングコマンドの出力を直接受け取ることも可能です。

これにより、複雑なデータ処理を効率的に実現できます。

非同期・並行処理との併用

Task.Runで入力待ちを隔離する

Console.ReadLineは同期的に動作し、呼び出すと入力が完了するまで処理がブロックされます。

これが原因で、UIの応答が止まったり、他の処理が遅延したりすることがあります。

そこで、Task.Runを使って入力待ちを別スレッドで実行し、メインスレッドの処理を妨げないようにする方法があります。

以下は、Task.RunConsole.ReadLineを非同期的に実行し、メインスレッドで他の処理を並行して行う例です。

using System;
using System.Threading.Tasks;
class Program
{
    static async Task Main()
    {
        Console.WriteLine("入力を待っています。入力中も他の処理が動作します。");
        Task<string?> inputTask = Task.Run(() => Console.ReadLine());
        // メインスレッドで別の処理を実行
        for (int i = 0; i < 5; i++)
        {
            Console.WriteLine($"メインスレッドの処理 {i + 1}");
            await Task.Delay(1000);
        }
        string? input = await inputTask;
        Console.WriteLine($"入力された内容: {input}");
    }
}
入力を待っています。入力中も他の処理が動作します。
メインスレッドの処理 1
メインスレッドの処理 2
メインスレッドの処理 3
メインスレッドの処理 4
メインスレッドの処理 5
Hello World
入力された内容: Hello World

この例では、Console.ReadLineの呼び出しをTask.Runで別スレッドに移し、メインスレッドは1秒ごとにメッセージを表示しています。

ユーザーが入力を完了すると、await inputTaskで結果を受け取ります。

これにより、入力待ち中も他の処理を並行して実行可能です。

CancellationTokenでユーザー中断

非同期処理を行う際、ユーザーが処理を中断したい場合に備えてCancellationTokenを使うことが多いです。

CancellationTokenはキャンセル要求を伝える仕組みで、非同期タスクの途中でキャンセルを検知して処理を終了できます。

ただし、Console.ReadLine自体はキャンセルをサポートしていないため、キャンセルトークンを使って入力待ちを強制的に中断するには工夫が必要です。

以下は、Task.RunCancellationTokenSourceを組み合わせて、一定時間内に入力がなければキャンセルする例です。

using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
    static async Task Main()
    {
        using CancellationTokenSource cts = new CancellationTokenSource();
        Task<string?> inputTask = Task.Run(() => Console.ReadLine(), cts.Token);
        // 5秒後にキャンセルを要求
        Task delayTask = Task.Delay(5000);
        Task completedTask = await Task.WhenAny(inputTask, delayTask);
        if (completedTask == delayTask)
        {
            cts.Cancel();
            Console.WriteLine("入力待ちがタイムアウトしました。");
        }
        else
        {
            string? input = await inputTask;
            Console.WriteLine($"入力された内容: {input}");
        }
    }
}
(5秒以内に入力しなかった場合)
入力待ちがタイムアウトしました。

この例では、5秒間入力を待ち、入力がなければキャンセルを試みます。

ただし、Console.ReadLineはキャンセルに対応していないため、実際にはキャンセル後も入力待ちが続くことがあります。

完全にキャンセル可能な入力待ちを実装するには、別の方法(例えば低レベルのコンソールAPIの利用)が必要です。

非同期ストリームとの相互作用

C# 8.0以降では、非同期ストリームIAsyncEnumerable<T>を使って非同期にデータを逐次処理できます。

標準入力を非同期ストリームとして扱うことで、入力を待ちながら他の非同期処理とシームレスに連携できます。

以下は、標準入力を非同期に1行ずつ読み取り、非同期ストリームとして処理する例です。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
class Program
{
    static async IAsyncEnumerable<string> ReadLinesAsync()
    {
        string? line;
        while ((line = await Task.Run(() => Console.ReadLine())) != null)
        {
            yield return line;
        }
    }
    static async Task Main()
    {
        Console.WriteLine("非同期ストリームで入力を受け付けます。Ctrl+Z(Windows)またはCtrl+D(Linux/macOS)で終了。");
        await foreach (var line in ReadLinesAsync())
        {
            Console.WriteLine($"入力: {line}");
        }
        Console.WriteLine("入力の終端に達しました。");
    }
}
非同期ストリームで入力を受け付けます。Ctrl+Z(Windows)またはCtrl+D(Linux/macOS)で終了。
Hello
入力: Hello
World
入力: World
(Ctrl+Z + Enter)
入力の終端に達しました。

このコードでは、ReadLinesAsyncメソッドが非同期ストリームを返し、await foreachで1行ずつ非同期に処理しています。

Console.ReadLine自体は同期メソッドなのでTask.Runでラップして非同期化しています。

非同期ストリームを使うことで、入力待ちと他の非同期処理を自然に組み合わせられ、複雑な非同期ワークフローの構築に役立ちます。

エラーと例外のハンドリング

FormatExceptionが起きる背景

FormatExceptionは、文字列を数値や日付などの特定の型に変換しようとした際に、入力文字列の形式が期待される形式と異なる場合に発生します。

たとえば、int.ParseDateTime.Parseで「abc」や「2024-13-01」のような不正な文字列を渡すと、この例外がスローされます。

using System;
class Program
{
    static void Main()
    {
        try
        {
            string input = "abc";
            int number = int.Parse(input);  // ここでFormatExceptionが発生
            Console.WriteLine(number);
        }
        catch (FormatException ex)
        {
            Console.WriteLine($"FormatExceptionが発生しました: {ex.Message}");
        }
    }
}
FormatExceptionが発生しました: 入力文字列の形式が正しくありません。

この例外は、入力が数値や日付の形式に合致していないことを示すため、ユーザーに再入力を促すなどの対策が必要です。

int.TryParseDateTime.TryParseを使うと例外を避けて安全に判定できます。

OverflowExceptionへの対策

OverflowExceptionは、変換しようとした数値が対象の型の範囲を超えた場合に発生します。

たとえば、int.Parse"999999999999"のようにintの最大値(約21億)を超える文字列を変換しようとすると起きます。

using System;
class Program
{
    static void Main()
    {
        try
        {
            string input = "999999999999";
            int number = int.Parse(input);  // ここでOverflowExceptionが発生
            Console.WriteLine(number);
        }
        catch (OverflowException ex)
        {
            Console.WriteLine($"OverflowExceptionが発生しました: {ex.Message}");
        }
    }
}
OverflowExceptionが発生しました: 値が Int32 の範囲を超えています。

対策としては、int.TryParseを使い、変換が成功したかどうかを判定する方法が有効です。

TryParseは範囲外の値も失敗として扱い、例外をスローしません。

using System;
class Program
{
    static void Main()
    {
        string input = "999999999999";
        if (int.TryParse(input, out int number))
        {
            Console.WriteLine($"変換成功: {number}");
        }
        else
        {
            Console.WriteLine("変換失敗: 入力がintの範囲外か形式が不正です。");
        }
    }
}
変換失敗: 入力がintの範囲外か形式が不正です。

また、より大きな数値を扱いたい場合はlongdecimalなど、より大きな範囲の型を検討するとよいでしょう。

想定外入力のログ出力ポリシー

ユーザーからの入力は多様であり、想定外の形式や値が送られてくることがあります。

これらの入力を適切にログに記録することは、問題の原因解析やセキュリティ対策に役立ちます。

ログ出力の際のポイントは以下の通りです。

  • 入力内容の記録

どのような入力がエラーを引き起こしたかを記録します。

ただし、パスワードや個人情報などの機密情報はマスクや除外が必要です。

  • エラー内容の詳細

例外の種類やメッセージ、スタックトレースをログに残すことで、原因特定が容易になります。

  • ログレベルの設定

想定外入力は通常WarningErrorレベルで記録し、正常な入力はInfoレベルで管理します。

  • ログの保管と管理

ログファイルのローテーションや保存期間を設定し、過剰なログ肥大化を防ぎます。

以下は、想定外入力をキャッチしてログに出力する簡単な例です。

using System;
class Program
{
    static void Main()
    {
        Console.WriteLine("整数を入力してください:");
        string? input = Console.ReadLine();
        try
        {
            int number = int.Parse(input!);
            Console.WriteLine($"入力された整数: {number}");
        }
        catch (FormatException ex)
        {
            LogWarning($"FormatException: 入力値='{input}', メッセージ='{ex.Message}'");
            Console.WriteLine("入力形式が正しくありません。");
        }
        catch (OverflowException ex)
        {
            LogWarning($"OverflowException: 入力値='{input}', メッセージ='{ex.Message}'");
            Console.WriteLine("入力値が範囲外です。");
        }
    }
    static void LogWarning(string message)
    {
        // 実際はファイルやログ管理システムに出力することが多い
        Console.WriteLine($"[Warning] {DateTime.Now}: {message}");
    }
}
整数を入力してください:
abc
[Warning] 2024/06/01 12:00:00: FormatException: 入力値='abc', メッセージ='入力文字列の形式が正しくありません。'
入力形式が正しくありません。

このように、想定外の入力をログに残すことで、後から問題の傾向を分析したり、不正アクセスの兆候を検知したりできます。

ログの取り扱いはプライバシーやセキュリティにも配慮しつつ、適切に設計してください。

コンソールUIのカスタマイズ

カラフルなプロンプト表示

コンソールアプリケーションのユーザーインターフェースを見やすく、わかりやすくするために、文字色を変えてプロンプトをカラフルに表示することがよくあります。

C#ではConsole.ForegroundColorプロパティを使って文字色を変更できます。

以下は、緑色の文字でプロンプトを表示し、入力後に元の色に戻す例です。

using System;
class Program
{
    static void Main()
    {
        // 現在の文字色を保存
        ConsoleColor originalColor = Console.ForegroundColor;
        // プロンプトを緑色で表示
        Console.ForegroundColor = ConsoleColor.Green;
        Console.Write("名前を入力してください: ");
        // 色を元に戻す
        Console.ForegroundColor = originalColor;
        string? input = Console.ReadLine();
        Console.WriteLine($"こんにちは、{input}さん!");
    }
}
名前を入力してください: 山田
こんにちは、山田さん!

このように、プロンプトだけ色を変えることで、ユーザーが入力すべき場所を視覚的に強調できます。

Console.ForegroundColorには多くの色が用意されているため、用途に応じて使い分けるとよいでしょう。

背景色を切り替える効果

文字色だけでなく、背景色も変更することで、より強調した表示が可能です。

Console.BackgroundColorプロパティを使い、背景色を切り替えられます。

以下は、背景色を青にして文字色を白に設定し、注意メッセージを表示する例です。

using System;
class Program
{
    static void Main()
    {
        ConsoleColor originalForeground = Console.ForegroundColor;
        ConsoleColor originalBackground = Console.BackgroundColor;
        Console.ForegroundColor = ConsoleColor.White;
        Console.BackgroundColor = ConsoleColor.Blue;
        Console.WriteLine("注意: 入力内容をよく確認してください。");
        // 色を元に戻す
        Console.ForegroundColor = originalForeground;
        Console.BackgroundColor = originalBackground;
    }
}
注意: 入力内容をよく確認してください。

背景色を変えると文字が目立ち、重要なメッセージやエラー表示に適しています。

ただし、背景色を変えたままにすると他の表示も影響を受けるため、表示後は必ず元の色に戻すことが大切です。

入力中の文字をマスクする方法

パスワードなどの機密情報を入力する際は、入力中の文字を画面に表示しない(マスクする)ことが求められます。

Console.ReadLineは入力内容をそのまま表示するため、マスク入力にはConsole.ReadKeyを使って1文字ずつ読み取り、画面には*などの記号を表示する方法が一般的です。

以下は、入力中の文字を*でマスクし、Enterキーで入力終了とする例です。

using System;
using System.Text;
class Program
{
    static void Main()
    {
        Console.Write("パスワードを入力してください: ");
        StringBuilder password = new StringBuilder();
        while (true)
        {
            ConsoleKeyInfo keyInfo = Console.ReadKey(intercept: true);
            if (keyInfo.Key == ConsoleKey.Enter)
            {
                Console.WriteLine();
                break;
            }
            else if (keyInfo.Key == ConsoleKey.Backspace)
            {
                if (password.Length > 0)
                {
                    // バックスペース処理
                    password.Length--;
                    Console.Write("\b \b"); // カーソルを戻して空白で消す
                }
            }
            else
            {
                password.Append(keyInfo.KeyChar);
                Console.Write("*");
            }
        }
        Console.WriteLine($"入力されたパスワードの長さ: {password.Length}文字");
    }
}
パスワードを入力してください: ********
入力されたパスワードの長さ: 8文字

このコードでは、Console.ReadKey(intercept: true)でキー入力を画面に表示せずに取得し、入力された文字数分だけ*を表示しています。

バックスペースキーにも対応し、入力ミスを修正可能です。

この方法を使うことで、パスワードや秘密情報の入力時に画面上で内容が見えないようにできます。

クロスプラットフォーム留意点

改行コード差異の影響

Windows、Linux、macOSなど異なるOS間では改行コードの扱いに違いがあります。

Windowsでは改行コードは\r\n(CR+LF)、LinuxやmacOSでは\n(LF)のみが使われます。

この差異は、Console.ReadLineで読み取った文字列の末尾には影響しませんが、ファイルの読み書きや文字列の処理で注意が必要です。

たとえば、ファイルから読み込んだテキストをそのまま表示したり、改行コードを含む文字列を比較したりする場合、改行コードの違いで意図しない動作が起きることがあります。

using System;
class Program
{
    static void Main()
    {
        string text = "行1\r\n行2";  // Windowsの改行コード
        Console.WriteLine("Windows改行コードを含む文字列:");
        Console.WriteLine(text);
        string normalized = text.Replace("\r\n", "\n");
        Console.WriteLine("改行コードをLFに統一した文字列:");
        Console.WriteLine(normalized);
    }
}
Windows改行コードを含む文字列:
行1
行2
改行コードをLFに統一した文字列:
行1
行2

このように、改行コードを統一することで、異なる環境間での文字列処理の一貫性を保てます。

特にファイルの入出力やネットワーク通信で改行コードの違いが問題になる場合は、明示的に変換を行うことが推奨されます。

CTRL+ZとCTRL+Dの動作比較

コンソールでの入力終了(EOF: End Of File)を示す操作は、OSによって異なります。

WindowsではCTRL+Zを押してからEnterキーを押すことでEOFを送信し、LinuxやmacOSではCTRL+Dが同様の役割を果たします。

Console.ReadLineはEOFを検知するとnullを返します。

これにより、入力の終端を判定できますが、ユーザーがどの操作を行うかは環境依存です。

  • Windows

ユーザーはCTRL+Zを押し、Enterキーを押すとEOFが送信されます。

> (入力)
^Z
  • Linux/macOS

ユーザーはCTRL+Dを押すだけでEOFが送信されます。

$ (入力)
^D

この違いを理解しておくことで、クロスプラットフォーム対応のコンソールアプリケーションでEOF検知の説明やヘルプを適切に案内できます。

コンソールエンコーディング設定

コンソールの文字コード(エンコーディング)はOSや環境によって異なり、文字化けや入力・出力の不具合の原因となることがあります。

C#のConsoleクラスでは、Console.InputEncodingConsole.OutputEncodingプロパティでエンコーディングを設定できます。

Windowsの標準コンソールはデフォルトでShift_JIS(コードページ932)を使うことが多く、UTF-8と異なるため日本語の扱いに注意が必要です。

一方、LinuxやmacOSのターミナルはUTF-8が標準です。

using System;
using System.Text;
class Program
{
    static void Main()
    {
        // 入力と出力のエンコーディングをUTF-8に設定
        Console.InputEncoding = Encoding.UTF8;
        Console.OutputEncoding = Encoding.UTF8;
        Console.WriteLine("UTF-8エンコーディングで入力してください:");
        string? input = Console.ReadLine();
        Console.WriteLine($"入力内容: {input}");
    }
}
UTF-8エンコーディングで入力してください:
こんにちは
入力内容: こんにちは

Windows環境でUTF-8を使いたい場合は、コンソールのコードページをchcp 65001でUTF-8に変更してから実行することもあります。

ただし、すべての環境で完全に動作するわけではないため、エンコーディングの設定は環境に応じて調整が必要です。

クロスプラットフォーム対応のアプリケーションでは、エンコーディングの違いを考慮し、必要に応じてConsole.InputEncodingConsole.OutputEncodingを明示的に設定することが望ましいです。

テストとデバッグのヒント

モック入力でユニットテスト

コンソールアプリケーションの入力処理をユニットテストする際、Console.ReadLineの直接呼び出しはテストの自動化を難しくします。

そこで、標準入力をモック(模擬)する方法が有効です。

Console.SetInメソッドを使って、StringReaderなどのカスタムTextReaderを標準入力に設定し、任意の入力をシミュレートできます。

以下は、Console.ReadLineを使うメソッドをテストする例です。

using System;
using System.IO;
using Xunit;
public class InputProcessor
{
    public string ReadUserInput()
    {
        return Console.ReadLine() ?? string.Empty;
    }
}
public class InputProcessorTests
{
    [Fact]
    public void ReadUserInput_ReturnsExpectedString()
    {
        // モック入力を用意
        string simulatedInput = "テスト入力";
        using var stringReader = new StringReader(simulatedInput);
        Console.SetIn(stringReader);
        var processor = new InputProcessor();
        string result = processor.ReadUserInput();
        Assert.Equal(simulatedInput, result);
    }
}

このように、Console.SetInで入力を差し替えることで、ユーザー入力を自動的に供給し、テストを自動化できます。

テスト終了後は元の標準入力に戻すことも検討してください。

CI環境での標準入力シミュレーション

継続的インテグレーション(CI)環境では、対話的な入力ができないため、標準入力をシミュレートする必要があります。

CIでのテストやビルド時に、ファイルや文字列をリダイレクトして標準入力に渡す方法が一般的です。

たとえば、テスト用の入力ファイルを用意し、以下のようにコマンドラインでリダイレクトします。

dotnet run < test_input.txt

または、CIのスクリプト内で標準入力をエコーコマンドでパイプする方法もあります。

echo "テスト入力" | dotnet run

これにより、プログラムは通常通りConsole.ReadLineで入力を受け取りつつ、CI環境でも自動的に入力が供給されます。

CI環境でのテスト自動化においては、標準入力のシミュレーションは必須のテクニックです。

ログ出力レベルの切り替えテクニック

デバッグや運用時にログの詳細度を切り替えられると便利です。

C#のコンソールアプリケーションでは、ログレベルに応じて出力内容を制御する仕組みを自作することが多いです。

以下は、簡単なログレベル管理の例です。

using System;
enum LogLevel
{
    Debug,
    Info,
    Warning,
    Error
}
class Logger
{
    private LogLevel currentLevel;
    public Logger(LogLevel level)
    {
        currentLevel = level;
    }
    public void Log(string message, LogLevel level)
    {
        if (level >= currentLevel)
        {
            Console.WriteLine($"[{level}] {message}");
        }
    }
}
class Program
{
    static void Main()
    {
        var logger = new Logger(LogLevel.Info);
        logger.Log("これはデバッグメッセージです。", LogLevel.Debug);
        logger.Log("これは情報メッセージです。", LogLevel.Info);
        logger.Log("これは警告メッセージです。", LogLevel.Warning);
        logger.Log("これはエラーメッセージです。", LogLevel.Error);
    }
}
[Info] これは情報メッセージです。
[Warning] これは警告メッセージです。
[Error] これはエラーメッセージです。

この例では、LoggerクラスのcurrentLevelに設定したレベル以上のログだけが表示されます。

開発時はDebugに設定し、運用時はInfoWarningに切り替えることで、必要な情報だけを出力可能です。

より高度なログ管理には、Microsoft.Extensions.Loggingなどのフレームワークを利用することも検討してください。

よくあるミス集

入力後のTrim漏れによる不具合

ユーザーからの入力は、意図せず前後に空白文字(スペースやタブ)が含まれていることが多いです。

Console.ReadLineで取得した文字列をそのまま処理すると、前後の空白が原因で比較や判定が失敗するケースがあります。

これを防ぐために、入力後にTrim()メソッドで前後の空白を除去することが基本ですが、これを忘れると以下のような不具合が起きます。

using System;
class Program
{
    static void Main()
    {
        Console.WriteLine("yes または no を入力してください:");
        string? input = Console.ReadLine();
        if (input == "yes")
        {
            Console.WriteLine("肯定の処理を実行します。");
        }
        else if (input == "no")
        {
            Console.WriteLine("否定の処理を実行します。");
        }
        else
        {
            Console.WriteLine("不正な入力です。");
        }
    }
}

もしユーザーが" yes "(前後に空白あり)と入力した場合、input == "yes"の比較はfalseとなり、不正な入力と判定されてしまいます。

対策としては、必ず入力直後にTrim()を使って空白を除去します。

string? input = Console.ReadLine()?.Trim();

これにより、前後の空白が取り除かれ、意図した比較が正しく行えます。

Trim漏れは見落としやすいミスなので、入力処理の最初に必ず行う習慣をつけましょう。

文化圏依存フォーマットの落とし穴

数値や日付の文字列変換は、文化圏(ロケール)によってフォーマットが異なるため、ParseTryParseで想定外の結果や例外が発生することがあります。

例えば、小数点の表記は日本や米国では「.」ですが、ドイツやフランスでは「,」が使われます。

これを考慮せずにdouble.Parseを使うと、誤った解析や例外が起きることがあります。

using System;
using System.Globalization;
class Program
{
    static void Main()
    {
        string input = "3,14"; // ドイツ式小数点
        try
        {
            double value = double.Parse(input); // カルチャー指定なし
            Console.WriteLine($"解析結果: {value}");
        }
        catch (FormatException)
        {
            Console.WriteLine("フォーマットエラーが発生しました。");
        }
    }
}

このコードは、実行環境のカルチャーが日本や米国の場合、FormatExceptionが発生します。

正しく解析するには、適切なCultureInfoを指定する必要があります。

double value = double.Parse(input, new CultureInfo("de-DE"));

日付も同様で、yyyy/MM/dd形式とdd.MM.yyyy形式などが混在するため、文化圏依存のフォーマットを意識しないと誤動作の原因になります。

例外を握りつぶすパターン

例外処理でcatchブロック内を空にしたり、例外情報をログに残さずに無視することは「例外を握りつぶす」と呼ばれ、バグの原因や問題の発見を遅らせる大きなリスクです。

try
{
    int number = int.Parse(Console.ReadLine()!);
}
catch
{
    // 何もしない(例外を握りつぶしている)
}

このコードは、入力が不正でも何の通知もなく処理が続行されるため、後続の処理で予期しない動作やクラッシュが起きる可能性があります。

正しい対処は、例外の内容をログに記録したり、ユーザーにエラーメッセージを表示して再入力を促すことです。

try
{
    int number = int.Parse(Console.ReadLine()!);
}
catch (FormatException ex)
{
    Console.WriteLine("入力が整数の形式ではありません。再度入力してください。");
    // ログ出力などもここで行う
}

例外を握りつぶすパターンは、デバッグ時に原因がわからず時間を浪費する原因となるため、必ず適切なハンドリングを行いましょう。

まとめ

この記事では、C#のConsole.ReadLineを使った基本的な入力処理から、数値や日付、列挙型への変換、入力検証やエラー処理、非同期処理との併用まで幅広く解説しました。

特に入力の正規化や例外の適切なハンドリング、クロスプラットフォームでの注意点を押さえることで、安全かつ柔軟なコンソール入力が実現できます。

テストやデバッグの工夫も紹介しているため、実践的な開発に役立つ内容となっています。

関連記事

Back to top button
目次へ