C#は、Microsoftが開発したモダンで汎用性の高いプログラミング言語です。

デスクトップアプリからWeb開発、さらにはUnityを用いたゲーム開発まで、幅広い分野で利用されています。

C#を習得する上で最も効果的な学習方法は、「実際にコードを書いて動かすこと」です。

文法を暗記するだけでなく、提示された課題に対して自分なりのロジックを組み立てる経験が、エンジニアとしての基礎体力を養います。

本記事では、プログラミング初心者から実務レベルを目指す中級者までを対象に、ステップアップ形式でC#の練習問題を用意しました。

各問題には詳細な解説と解答例を付けていますので、まずは自力で挑戦し、その後コードを確認して理解を深めていきましょう。

C#学習における練習問題の重要性

プログラミングの学習において、参考書を読むだけでは「わかったつもり」になりがちです。

しかし、いざ真っ白なエディタを前にすると、何から書き始めれば良いか分からなくなることが多々あります。

練習問題を解くことには、以下の3つの大きなメリットがあります。

  1. 文法の定着:変数、ループ、条件分岐といった基本要素を繰り返し使うことで、構文が自然と身につきます。
  2. 論理的思考力の育成:「入力をどう処理して期待する出力を得るか」というプロセスを考えることで、アルゴリズムの考え方が養われます。
  3. デバッグ能力の向上:エラーに直面し、それを解決する過程で、言語の仕様や開発ツールの使いかたに習熟できます。

最新のC#では、より簡潔に記述できる構文が次々と導入されています。

本記事の解答例では、現代的なC#の書き方も意識して解説していきます。

【レベル1】初心者向け:基本文法と算術演算

まずは、C#の基本中の基本である変数の定義、ユーザー入力の受け取り、そして簡単な計算を行う問題からスタートしましょう。

ここでは、コンソールアプリケーションの形式でプログラムを作成します。

問題1:自己紹介プログラムの作成

ユーザーから「名前」と「年齢」をコンソールで入力してもらい、それらを組み合わせてメッセージを表示するプログラムを作成してください。

期待される実行結果

名前を入力してください:田中
年齢を入力してください:25
こんにちは、田中さん。あなたは25歳ですね。

解答例と解説

この問題では、Console.ReadLine()による入力の取得と、文字列補完(String Interpolation)を用いた表示がポイントです。

C#
using System;

namespace CSharpPractice
{
    class Program
    {
        static void Main(string[] args)
        {
            // ユーザーに名前の入力を促す
            Console.Write("名前を入力してください:");
            string? name = Console.ReadLine();

            // ユーザーに年齢の入力を促す
            Console.Write("年齢を入力してください:");
            string? ageInput = Console.ReadLine();

            // 文字列を整数に変換(簡易的な変換)
            if (int.TryParse(ageInput, out int age))
            {
                // 文字列補完 $"" を使用して結果を表示
                Console.WriteLine($"こんにちは、{name}さん。あなたは{age}歳ですね。");
            }
            else
            {
                Console.WriteLine("年齢は数字で入力してください。");
            }
        }
    }
}

解説: C#では、文字列の中に変数を埋め込む際、文字列の先頭に $ を付ける文字列補完を利用するのが一般的です。

また、ユーザー入力は常に文字列(string)として受け取られるため、数値として計算に使いたい場合は int.TryParse などを使用して型変換を行う必要があります。

問題2:BMI計算機の作成

身長(m)と体重(kg)を入力し、BMI(体格指数)を算出するプログラムを作成してください。

BMIの計算式は 体重 / (身長 * 身長) です。

期待される実行結果

身長(m)を入力してください:1.7
体重(kg)を入力してください:65
あなたのBMIは 22.49 です。

解答例と解説

浮動小数点数(double型)の扱いと、計算の優先順位に注意しましょう。

C#
using System;

namespace CSharpPractice
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.Write("身長(m)を入力してください:");
            double height = double.Parse(Console.ReadLine() ?? "0");

            Console.Write("体重(kg)を入力してください:");
            double weight = double.Parse(Console.ReadLine() ?? "0");

            // BMIの計算
            double bmi = weight / (height * height);

            // 結果を小数点第2位まで表示
            Console.WriteLine($"あなたのBMIは {bmi:F2} です。");
        }
    }
}

解説: 実数を扱う場合は float よりも精度の高い double 型を使用するのがC#の標準です。

また、{bmi:F2} という書式指定子を使うことで、小数点以下の表示桁数を制御することができます。

【レベル2】初級~中級:制御構文とループ

次に、プログラムに「判断」と「繰り返し」をさせる制御構文の問題に挑戦しましょう。

問題3:FizzBuzz問題

1から50までの数字を順番に表示してください。

ただし、以下の条件に従ってください。

  • 3の倍数のときは数字の代わりに「Fizz」と表示
  • 5の倍数のときは数字の代わりに「Buzz」と表示
  • 3と5の両方の倍数(15の倍数)のときは「FizzBuzz」と表示

期待される実行結果

1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz ...

解答例と解説

もっとも有名なプログラミング問題の一つです。

for 文と if-else if-else 文を組み合わせます。

C#
using System;

namespace CSharpPractice
{
    class Program
    {
        static void Main(string[] args)
        {
            for (int i = 1; i <= 50; i++)
            {
                // 条件の厳しい(15の倍数)ものから先に判定するのがコツ
                if (i % 15 == 0)
                {
                    Console.Write("FizzBuzz ");
                }
                else if (i % 3 == 0)
                {
                    Console.Write("Fizz ");
                }
                else if (i % 5 == 0)
                {
                    Console.Write("Buzz ");
                }
                else
                {
                    Console.Write($"{i} ");
                }
            }
        }
    }
}

解説: 条件分岐の順序が非常に重要です。

もし最初に i % 3 == 0 を判定してしまうと、15の時に「Fizz」と表示されてしまい、後続の15の判定(FizzBuzz)まで到達しません。

「より限定的な条件」を先に記述するのがプログラミングの鉄則です。

問題4:九九表の表示

2重の for ループを使用して、1の段から9の段までの九九表を表示してください。

各数値はタブ(\t)で区切って綺麗に整列させてください。

解答例と解説

ループの入れ子構造(ネスト)を理解するための問題です。

C#
using System;

namespace CSharpPractice
{
    class Program
    {
        static void Main(string[] args)
        {
            for (int i = 1; i <= 9; i++) // 行
            {
                for (int j = 1; j <= 9; j++) // 列
                {
                    // \t は水平タブを表すエスケープシーケンス
                    Console.Write($"{i * j}\t");
                }
                // 段が終わるごとに改行
                Console.WriteLine();
            }
        }
    }
}

解説: 外側のループが1回回るごとに、内側のループが9回回ります。

このように多次元の構造を処理する際には2重ループが頻繁に使われます。

Console.Write(改行なし)と Console.WriteLine(改行あり)の使い分けに注目しましょう。

【レベル3】中級編:配列とコレクション、LINQ

データの集合を扱う「配列」や「List」、そしてC#の強力な機能である「LINQ」を扱う問題です。

問題5:配列内の最大値と最小値を求める

以下の整数配列から、最大値と最小値を検索して表示するプログラムを作成してください。

int[] numbers = { 12, 45, 7, 34, 100, 23, 56 };

解答例と解説

まずはループを使った基本的な解法を示し、その後にLINQを使ったモダンな解法を解説します。

C#
using System;
using System.Linq; // LINQを使用するために必要

namespace CSharpPractice
{
    class Program
    {
        static void Main(string[] args)
        {
            int[] numbers = { 12, 45, 7, 34, 100, 23, 56 };

            // --- 方法1: ループによる手動検索 ---
            int max = numbers[0];
            int min = numbers[0];

            foreach (int n in numbers)
            {
                if (n > max) max = n;
                if (n < min) min = n;
            }
            Console.WriteLine($"[手動] 最大: {max}, 最小: {min}");

            // --- 方法2: LINQを使用した簡潔な記述 ---
            int linqMax = numbers.Max();
            int linqMin = numbers.Min();
            Console.WriteLine($"[LINQ] 最大: {linqMax}, 最小: {linqMin}");
        }
    }
}

解説: 実務では System.Linq 名前空間をインポートして、Max()Min() などの拡張メソッドを使うのが一般的です。

コードの可読性が飛躍的に向上するため、LINQの習得は中級者への必須条件と言えます。

問題6:Listの操作とフィルタリング

以下の要件を満たすプログラムを作成してください。

  1. List<int> を作成し、ランダムな数値を10個追加する。
  2. その中から「50以上の数値」だけを抽出し、昇順に並び替えて表示する。

解答例と解説

動的配列である List<T> と、LINQのフィルタリング(Where)およびソート(OrderBy)を活用します。

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

namespace CSharpPractice
{
    class Program
    {
        static void Main(string[] args)
        {
            List<int> data = new List<int>();
            Random rand = new Random();

            // 1. 1〜100の乱数を10個生成してリストに追加
            for (int i = 0; i < 10; i++)
            {
                data.Add(rand.Next(1, 101));
            }

            Console.WriteLine("生成されたリスト: " + string.Join(", ", data));

            // 2. LINQを用いて50以上を抽出し、昇順ソート
            var result = data.Where(n => n >= 50)
                             .OrderBy(n => n);

            Console.WriteLine("50以上かつ昇順: " + string.Join(", ", result));
        }
    }
}

解説: Where メソッドの中に記述されている n => n >= 50ラムダ式と呼ばれます。

「各要素 n に対して、n >= 50 という条件を適用する」という意味になります。

LINQはこのようにメソッドをチェーン(連結)させて処理を記述できるのが特徴です。

【レベル4】中級編:オブジェクト指向(クラスと継承)

C#の真骨頂であるオブジェクト指向プログラミング(OOP)の練習です。

クラスの設計、プロパティ、メソッドの概念を学びましょう。

問題7:銀行口座クラスの作成

以下の要件を満たす BankAccount クラスを作成し、メインメソッドで動作を確認してください。

  • プロパティ:Owner(所有者名)、Balance(残高、外部からは読み取り専用)
  • メソッド:Deposit(amount)(入金)、Withdraw(amount)(出金、残高不足ならエラー表示)
  • コンストラクタで所有者名と初期残高を設定できること

解答例と解説

カプセル化(データの保護)を意識した設計を行います。

C#
using System;

namespace CSharpPractice
{
    public class BankAccount
    {
        // プロパティ
        public string Owner { get; private set; }
        public decimal Balance { get; private set; } // お金はdecimal型が推奨

        // コンストラクタ
        public BankAccount(string owner, decimal initialBalance)
        {
            Owner = owner;
            Balance = initialBalance;
        }

        // 入金メソッド
        public void Deposit(decimal amount)
        {
            if (amount <= 0) return;
            Balance += amount;
            Console.WriteLine($"{amount}円入金しました。現在の残高は{Balance}円です。");
        }

        // 出金メソッド
        public void Withdraw(decimal amount)
        {
            if (amount > Balance)
            {
                Console.WriteLine("残高不足です。出金できません。");
                return;
            }
            Balance -= amount;
            Console.WriteLine($"{amount}円出金しました。現在の残高は{Balance}円です。");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var myAccount = new BankAccount("山田太郎", 5000);
            myAccount.Deposit(2000);
            myAccount.Withdraw(4000);
            myAccount.Withdraw(5000); // ここで残高不足になるはず
        }
    }
}

解説: Balance プロパティに private set を指定することで、クラス外部から勝手に残高を書き換えられるのを防いでいます。

これこそがカプセル化の基本です。

また、金額を扱う際は、浮動小数点数の誤差を避けるために decimal 型を使用するのが実務上の常識です。

問題8:継承とポリモーフィズム

「動物(Animal)」という基本クラスを作り、それを継承した「犬(Dog)」と「猫(Cat)」クラスを作成してください。

  • Animal クラスには仮想メソッド MakeSound() を定義し、「動物が鳴いています」と出力。
  • DogMakeSound() をオーバーライドして「ワンワン!」と出力。
  • CatMakeSound() をオーバーライドして「ニャー!」と出力。

解答例と解説

継承とメソッドのオーバーライド(上書き)を学びます。

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

namespace CSharpPractice
{
    // 基本クラス
    public class Animal
    {
        public virtual void MakeSound()
        {
            Console.WriteLine("動物が鳴いています");
        }
    }

    // 派生クラス(犬)
    public class Dog : Animal
    {
        public override void MakeSound()
        {
            Console.WriteLine("ワンワン!");
        }
    }

    // 派生クラス(猫)
    public class Cat : Animal
    {
        public override void MakeSound()
        {
            Console.WriteLine("ニャー!");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // 多態性(ポリモーフィズム)の活用
            List<Animal> animals = new List<Animal>
            {
                new Dog(),
                new Cat(),
                new Animal()
            };

            foreach (var animal in animals)
            {
                animal.MakeSound();
            }
        }
    }
}

解説: virtual キーワードを付けたメソッドは、派生クラスで override できます。

この仕組みにより、同じ MakeSound() という呼び出しでも、実際のオブジェクトの種類(DogなのかCatなのか)に応じて適切な振る舞いが行われます。

これをポリモーフィズム(多態性)と呼びます。

【レベル5】実践編:例外処理とファイル操作

実務アプリケーションでは避けて通れない、エラーハンドリングと外部データの取り扱いについて学びます。

問題9:例外処理(try-catch)の実装

ユーザーに2つの数値を入力させ、割り算の結果を表示するプログラムを作成してください。

ただし、以下の2つのエラーを適切にキャッチしてエラーメッセージを表示してください。

  1. 数値以外の文字が入力された場合
  2. 0で割り算をしようとした場合(ゼロ除算)

解答例と解説

プログラムの異常終了を防ぐための必須スキルです。

C#
using System;

namespace CSharpPractice
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                Console.Write("割られる数を入力してください:");
                int num1 = int.Parse(Console.ReadLine() ?? "0");

                Console.Write("割る数を入力してください:");
                int num2 = int.Parse(Console.ReadLine() ?? "0");

                int result = num1 / num2;
                Console.WriteLine($"結果: {result}");
            }
            catch (FormatException)
            {
                Console.WriteLine("エラー:数値を入力してください。");
            }
            catch (DivideByZeroException)
            {
                Console.WriteLine("エラー:0で割ることはできません。");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"予期せぬエラーが発生しました: {ex.Message}");
            }
            finally
            {
                Console.WriteLine("処理を終了します。");
            }
        }
    }
}

解説: try ブロック内で発生したエラーは、対応する catch ブロックへジャンプします。

finally ブロックは、エラーの有無に関わらず必ず実行されるため、後片付け処理などに適しています。

問題10:ファイルの書き込みと読み込み

プログラムと同じフォルダに sample.txt という名前でテキストファイルを作成し、任意の文字列を3行書き込んでください。

その後、そのファイルを読み込んでコンソールに表示してください。

解答例と解説

System.IO 名前空間を利用します。

最新のC#では File クラスの静的メソッドを使うのが非常に簡単です。

C#
using System;
using System.IO;

namespace CSharpPractice
{
    class Program
    {
        static void Main(string[] args)
        {
            string filePath = "sample.txt";
            string[] lines = { "C#の練習中です。", "ファイル操作を学んでいます。", "完了!" };

            // ファイルへの書き込み
            try
            {
                File.WriteAllLines(filePath, lines);
                Console.WriteLine("ファイルを保存しました。");

                // ファイルの読み込み
                if (File.Exists(filePath))
                {
                    string content = File.ReadAllText(filePath);
                    Console.WriteLine("--- ファイルの内容 ---");
                    Console.WriteLine(content);
                }
            }
            catch (IOException ex)
            {
                Console.WriteLine($"入出力エラー: {ex.Message}");
            }
        }
    }
}

解説: File.WriteAllLinesFile.ReadAllText は、ファイルのオープン・クローズ処理を内部で自動的に行ってくれる便利なメソッドです。

自分でストリームを開く必要がないため、書き忘れによるファイルロックなどのトラブルを防げます。

【レベル6】応用編:非同期処理とイベント

現代のアプリ開発で欠かせない、UIをフリーズさせないための非同期処理(async/await)について学習しましょう。

問題11:非同期処理(Task.Delay)のシミュレーション

「データのダウンロード中…」と表示し、3秒間待機した後に「ダウンロードが完了しました!」と表示する非同期メソッドを作成し、それを呼び出してください。

待機中はメインスレッドをブロックしないようにしてください。

解答例と解説

asyncawait の使いかたをマスターします。

C#
using System;
using System.Threading.Tasks;

namespace CSharpPractice
{
    class Program
    {
        // Mainメソッドもasyncにすることが可能
        static async Task Main(string[] args)
        {
            Console.WriteLine("処理を開始します。");

            // 非同期メソッドの呼び出し
            await DownloadDataAsync();

            Console.WriteLine("すべての処理が終わりました。");
        }

        static async Task DownloadDataAsync()
        {
            Console.WriteLine("データのダウンロード中...");

            // 3000ミリ秒(3秒)待機。この間、スレッドは解放される
            await Task.Delay(3000);

            Console.WriteLine("ダウンロードが完了しました!");
        }
    }
}

解説: Thread.Sleep を使うとプログラム全体が固まってしまいますが、await Task.Delay を使うことで「待ち時間の間、他の処理を並行して行える」ようになります。

Web APIからのデータ取得やデータベース操作など、時間がかかる処理には必須の技術です。

スキルアップのためのアドバイス

問題を一通り解き終えた後、さらに実力を伸ばすために以下のステップに挑戦してみてください。

1. コードの共通化を考える

解答例のコードを見て、「この部分は別の関数にまとめられるのではないか?」と考えてみてください。

同じ処理を2回書かない(DRY原則:Don’t Repeat Yourself)を意識することで、保守性の高いきれいなコードが書けるようになります。

2. 公式ドキュメント(Microsoft Learn)を活用する

C#の仕様は非常に膨大です。

本記事で紹介した ListFile クラスには、他にも便利なメソッドがたくさんあります。

「他にどんなメソッドがあるのかな?」と気になったら、すぐに公式ドキュメントで検索する癖をつけましょう。

3. 小さなツールを自作する

練習問題はあくまでパーツの作り方です。

それらを組み合わせて、「TODO管理ツール」「簡易家計簿プログラム」「数当てゲーム」など、実際に使えるツールを作ってみることが、学習のモチベーション維持にもつながります。

まとめ

C#の練習問題を通じて、基本文法からオブジェクト指向、そして非同期処理といった応用的な概念までを駆け足で解説してきました。

プログラミング上達の道に近道はありません。

まずは「書いて、動かして、壊して、直す」というサイクルを繰り返してください。

最初はエラーメッセージに戸惑うかもしれませんが、エラーこそがあなたの知識を深めてくれる最良の教師です。

本記事で紹介した練習問題が、あなたのC#マスターへの第一歩となれば幸いです。

最新のC# 12や今後登場する新機能も取り入れながら、ぜひ楽しくプログラミングを続けていってください。