目次
はじめに
プログラムは基本的に上から下へ順番に実行されます。しかし、実際のアプリケーションでは「条件によって処理を変える」「同じ処理を繰り返す」「処理をまとめて再利用する」といったことが必要になります。
このレッスンでは、プログラムの流れを制御する方法を学びます。
┌─────────────────────────────────────────────────────────┐
│ プログラムの基本構造 │
├─────────────────────────────────────────────────────────┤
│ │
│ 順次処理 条件分岐 繰り返し │
│ ───────── ───────── ───────── │
│ ┌───┐ ┌───┐ ┌───────┐ │
│ │ A │ │条件│ │ │ │
│ └─┬─┘ └─┬─┘ │ ┌───┐ │ │
│ ↓ / \ │ │ A │ │ │
│ ┌───┐ ┌───┐ ┌───┐ │ └─┬─┘ │ │
│ │ B │ │ A │ │ B │ │ ↓ │ │
│ └─┬─┘ └───┘ └───┘ │ 条件 │←─┐ │
│ ↓ │ ↓ │ │ │
│ ┌───┐ └───────┘ │ │
│ │ C │ │ │ │
│ └───┘ └──────┘ │
│ │
└─────────────────────────────────────────────────────────┘
1. 条件分岐(if文)
if文とは
if文は「もし〜ならば」という条件によって、実行する処理を切り替える構文です。これはプログラミングで最も基本的かつ重要な制御構造の一つです。
デバイス管理システムでの活用例:
- デバイスが有効かどうかで表示を変える
- ユーザーの権限によってアクセス可能な機能を制限する
- 入力値が正しいかどうかを検証する
基本構文
int deviceCount = 50;
// if文の基本形
// if (条件式) { 条件が真のときの処理 }
if (deviceCount > 100)
{
Console.WriteLine("デバイス数が多いです");
}
else if (deviceCount > 50)
{
// 最初の条件が偽で、この条件が真のとき
Console.WriteLine("デバイス数は中程度です");
}
else
{
// すべての条件が偽のとき
Console.WriteLine("デバイス数は少ないです");
}
実行の流れ:
- まず
deviceCount > 100を評価 → 50 > 100 はfalse - 次に
deviceCount > 50を評価 → 50 > 50 はfalse - すべての条件が偽なので
elseブロックが実行される - 「デバイス数は少ないです」が出力される
単一行の場合
1行だけの処理の場合、波括弧を省略できますが、省略しないことを推奨します。後からコードを追加する際にバグの原因になりやすいためです。
// 波括弧を省略可能(非推奨)
if (deviceCount > 0)
Console.WriteLine("デバイスがあります");
// 三項演算子(シンプルな条件向き)
// 構文: 条件 ? 真の場合の値 : 偽の場合の値
string status = deviceCount > 0 ? "あり" : "なし";
三項演算子の使いどころ:
- 単純な値の選択には三項演算子が適している
- 複雑な条件や処理が必要な場合はif文を使う
// ○ 三項演算子が適切な例
string displayStatus = isActive ? "有効" : "無効";
// × 三項演算子が不適切な例(読みにくい)
string result = a > b ? (c > d ? "A" : "B") : (e > f ? "C" : "D");
複合条件
複数の条件を組み合わせる場合、論理演算子を使用します。
| 演算子 | 意味 | 例 | 説明 |
|---|---|---|---|
&& |
AND | a && b |
aとbの両方が真のとき真 |
|| |
OR | a || b |
aまたはbのどちらかが真のとき真 |
! |
NOT | !a |
aが偽のとき真 |
string os = "Windows";
bool isActive = true;
int version = 11;
// AND条件:両方の条件を満たす必要がある
if (os == "Windows" && isActive)
{
Console.WriteLine("アクティブなWindowsデバイス");
}
// OR条件:どちらかの条件を満たせばよい
if (os == "Windows" || os == "Linux")
{
Console.WriteLine("サポート対象OS");
}
// 複合条件:括弧で優先順位を明確にする
// 「Windows 10以上」または「macOS 12以上」
if ((os == "Windows" && version >= 10) || (os == "macOS" && version >= 12))
{
Console.WriteLine("最新OSです");
}
注意:短絡評価(ショートサーキット)
&& と || は短絡評価を行います。つまり、結果が確定した時点で残りの条件は評価されません。
// deviceがnullの場合、device.Nameを評価するとエラーになる
// しかし、&&の左側がfalseなら右側は評価されないので安全
if (device != null && device.Name.Length > 0)
{
Console.WriteLine(device.Name);
}
null条件とパターンマッチング
C#ではnull(値がない状態)を安全に扱うための様々な方法があります。
string? deviceName = GetDeviceName(); // ?はnullの可能性を示す
// nullチェック(従来の方法)
if (deviceName != null)
{
Console.WriteLine(deviceName.ToUpper());
}
// パターンマッチング(推奨)
// より読みやすく、意図が明確
if (deviceName is not null)
{
Console.WriteLine(deviceName.ToUpper());
}
// null条件演算子(?.)
// deviceNameがnullでなければToUpper()を実行、nullならnullを返す
string? upper = deviceName?.ToUpper();
// null合体演算子(??)
// deviceNameがnullなら"不明"を使用
string displayName = deviceName ?? "不明";
型パターン:オブジェクトの型をチェックして変数に代入
object value = 123;
// valueがint型なら、その値をnumber変数に代入してブロックを実行
if (value is int number)
{
Console.WriteLine($"整数です: {number}");
}
// numberはこのスコープ内で使用可能
プロパティパターン:オブジェクトのプロパティをチェック
// deviceNameがnullでなく、かつLengthが0より大きい場合
if (deviceName is { Length: > 0 })
{
Console.WriteLine("空でない文字列です");
}
// より複雑なパターン
if (device is { IsActive: true, Os: "Windows" })
{
Console.WriteLine("アクティブなWindowsデバイス");
}
2. 条件分岐(switch文)
switch文とは
switch文は、一つの値に対して複数の分岐先がある場合に使用します。if-else文の連鎖よりも読みやすく、効率的なコードになります。
switch文を使うべき場面:
- 一つの変数の値によって処理を分岐させる場合
- 分岐が3つ以上ある場合
- 値が列挙型や固定の文字列の場合
基本構文
string osType = "Windows";
switch (osType)
{
case "Windows":
Console.WriteLine("Windows OSです");
break; // breakは必須:これがないと次のcaseに進んでしまう
case "macOS":
Console.WriteLine("macOSです");
break;
case "Linux":
Console.WriteLine("Linuxです");
break;
default:
// どのcaseにも一致しない場合
Console.WriteLine("不明なOSです");
break;
}
重要:breakを忘れない
C#ではbreakを忘れるとコンパイルエラーになります(一部の言語では次のcaseに「落ちる」動作をしますが、C#では明示的な制御が必要です)。
複数ケースのグループ化
同じ処理を複数の値に対して行いたい場合、caseを連続して書くことができます。
string os = "Ubuntu";
switch (os)
{
case "Windows":
case "Windows Server":
// "Windows"または"Windows Server"の場合
Console.WriteLine("Microsoft製品");
break;
case "Ubuntu":
case "CentOS":
case "Debian":
// Linuxディストリビューションの場合
Console.WriteLine("Linux系OS");
break;
default:
Console.WriteLine("その他");
break;
}
switch式(C# 8.0以降、推奨)
C# 8.0からは、より簡潔に書けるswitch式が導入されました。値を返す式として使えるため、変数への代入が直接できます。
int statusCode = 1;
// switch式(式として値を返す)
// 構文: 対象変数 switch { パターン => 値, ... }
string statusName = statusCode switch
{
0 => "無効",
1 => "有効",
2 => "保留",
3 => "削除済み",
_ => "不明" // _(アンダースコア)はdefaultを表す
};
Console.WriteLine($"状態: {statusName}"); // 出力: 状態: 有効
従来のswitch文との比較:
// 従来のswitch文(冗長)
string statusName;
switch (statusCode)
{
case 0:
statusName = "無効";
break;
case 1:
statusName = "有効";
break;
default:
statusName = "不明";
break;
}
// switch式(簡潔)
string statusName = statusCode switch
{
0 => "無効",
1 => "有効",
_ => "不明"
};
パターンマッチングとswitch
switch式では、型や条件を組み合わせた高度なパターンマッチングが可能です。
object data = 42;
// 型パターン:型によって分岐
string description = data switch
{
int i when i > 0 => $"正の整数: {i}", // whenで追加条件
int i when i < 0 => $"負の整数: {i}",
int => "ゼロ", // 条件なしのint
string s => $"文字列: {s}",
null => "null",
_ => "その他の型"
};
// 範囲パターン(関係パターン):数値の範囲で分岐
int score = 85;
string grade = score switch
{
>= 90 => "A", // 90以上
>= 80 => "B", // 80以上90未満
>= 70 => "C", // 70以上80未満
>= 60 => "D", // 60以上70未満
_ => "F" // 60未満
};
// 注意:上から順に評価されるので、順序が重要
タプルパターン
複数の値を組み合わせて分岐する場合、タプルパターンが便利です。
// 2つの条件を組み合わせた分岐
string GetRecommendation(string os, bool isManaged)
{
return (os, isManaged) switch
{
("Windows", true) => "Intune管理を推奨",
("Windows", false) => "Intune登録を推奨",
("macOS", true) => "MDM管理済み",
("macOS", false) => "Jamf登録を推奨",
("Linux", _) => "手動管理", // _は任意の値にマッチ
_ => "サポート対象外"
};
}
// 使用例
Console.WriteLine(GetRecommendation("Windows", true)); // Intune管理を推奨
Console.WriteLine(GetRecommendation("Linux", false)); // 手動管理
タプルパターンの活用場面:
- 状態遷移の判定(現在の状態とイベントの組み合わせ)
- 2つ以上の条件の組み合わせによる処理の決定
- 多次元のswitch分岐
3. ループ処理
ループ処理とは
ループ(繰り返し)は、同じ処理を複数回実行するための構文です。プログラミングで最も頻繁に使用される制御構造の一つです。
デバイス管理システムでの活用例:
- デバイス一覧をすべて表示する
- 各デバイスに対してステータスをチェックする
- CSVファイルの各行を処理する
┌─────────────────────────────────────────────────────────┐
│ ループの種類と選択基準 │
├─────────────────────────────────────────────────────────┤
│ │
│ for文 回数が決まっている場合 │
│ インデックスが必要な場合 │
│ │
│ foreach文 コレクションの全要素を処理する場合 │
│ 最も直感的で安全 │
│ │
│ while文 条件が真の間繰り返す場合 │
│ 回数が不定の場合 │
│ │
│ do-while文 最低1回は実行が必要な場合 │
│ │
└─────────────────────────────────────────────────────────┘
for文
回数が決まっている繰り返しや、インデックス(番号)が必要な場合に使用します。
// 基本のfor文
// for (初期化; 継続条件; 更新処理)
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"カウント: {i}");
}
// 出力: カウント: 0, 1, 2, 3, 4
// 実行の流れ:
// 1. i = 0 で初期化
// 2. i < 5 をチェック → trueなのでループ本体を実行
// 3. i++ でiを1増加
// 4. 2に戻る(i < 5がfalseになるまで繰り返し)
逆順のループ:
// 10から1へのカウントダウン
for (int i = 10; i > 0; i--)
{
Console.WriteLine($"カウントダウン: {i}");
}
ステップを変更:
// 10ずつ増加
for (int i = 0; i <= 100; i += 10)
{
Console.WriteLine($"進捗: {i}%");
}
// 出力: 0%, 10%, 20%, ... 100%
foreach文
コレクション(配列、リストなど)のすべての要素を順番に処理する場合に使用します。最も安全で読みやすいループ形式です。
// 配列のループ
string[] devices = { "PC-001", "PC-002", "PC-003" };
foreach (string device in devices)
{
Console.WriteLine($"デバイス: {device}");
}
// 出力: デバイス: PC-001, PC-002, PC-003
インデックスが必要な場合:
foreachではインデックスは直接取得できませんが、LINQのSelectメソッドを使うことで取得できます。
// インデックス付きでループ
foreach (var (device, index) in devices.Select((d, i) => (d, i)))
{
Console.WriteLine($"{index + 1}: {device}");
}
// 出力: 1: PC-001, 2: PC-002, 3: PC-003
Dictionaryのループ:
var deviceStatus = new Dictionary<string, bool>
{
{ "PC-001", true },
{ "PC-002", false },
{ "PC-003", true }
};
// KeyValuePairとして取得
foreach (var kvp in deviceStatus)
{
Console.WriteLine($"{kvp.Key}: {(kvp.Value ? "有効" : "無効")}");
}
// 分解構文(デコンストラクト)でより読みやすく
foreach (var (name, status) in deviceStatus)
{
Console.WriteLine($"{name}: {(status ? "有効" : "無効")}");
}
while文
条件が真の間、繰り返しを続けます。繰り返し回数が事前に決まっていない場合に適しています。
// 条件が真の間ループ
int count = 0;
while (count < 5)
{
Console.WriteLine($"カウント: {count}");
count++; // これを忘れると無限ループになる!
}
無限ループ(breakで抜ける):
// リトライ処理の例
int attempt = 0;
while (true) // 無限ループ
{
attempt++;
Console.WriteLine($"試行: {attempt}");
// 条件を満たしたらbreakで抜ける
if (attempt >= 3)
{
Console.WriteLine("最大試行回数に達しました");
break; // ループを終了
}
}
注意:無限ループの危険性
条件が常に真のままだと、プログラムが永久に終わらなくなります。必ず終了条件を設けてください。
do-while文
while文と似ていますが、最低1回は必ず実行される点が異なります。
// 1〜10の数字を入力させる
int input;
do
{
Console.Write("1〜10の数字を入力: ");
input = int.Parse(Console.ReadLine() ?? "0");
} while (input < 1 || input > 10); // 条件が真なら繰り返す
Console.WriteLine($"入力値: {input}");
while文との違い:
int x = 10;
// whileの場合:条件を先にチェックするので実行されない
while (x < 5)
{
Console.WriteLine("while実行"); // 実行されない
x++;
}
// do-whileの場合:最低1回は実行される
do
{
Console.WriteLine("do-while実行"); // 1回実行される
x++;
} while (x < 5);
ループ制御
ループの流れを制御する特別なキーワードがあります。
| キーワード | 動作 | 使いどころ |
|---|---|---|
break |
ループを即座に終了 | 条件を満たしたので終了したい場合 |
continue |
現在の反復をスキップ | 特定の条件の要素は処理しない場合 |
// break: ループを終了
Console.WriteLine("breakの例:");
for (int i = 0; i < 100; i++)
{
if (i == 5)
{
break; // iが5になったらループ終了
}
Console.WriteLine(i); // 0, 1, 2, 3, 4 のみ出力
}
// continue: 次の反復へスキップ
Console.WriteLine("continueの例:");
for (int i = 0; i < 10; i++)
{
if (i % 2 == 0)
{
continue; // 偶数はスキップ
}
Console.WriteLine(i); // 1, 3, 5, 7, 9 のみ出力
}
ネストしたループからの脱出:
通常、breakは最も内側のループしか終了しません。外側のループも終了したい場合は、ラベルとgotoを使用します(ただし、gotoの使用は一般的に推奨されません)。
// gotoを使った方法(非推奨だが知っておくと良い)
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
if (i == 1 && j == 1)
{
goto EndLoop; // 外側のループも抜ける
}
Console.WriteLine($"i={i}, j={j}");
}
}
EndLoop:
Console.WriteLine("ループ終了");
// より良い方法:メソッドに切り出してreturnを使う
bool ProcessDevices(List<Device> devices)
{
foreach (var device in devices)
{
foreach (var attribute in device.Attributes)
{
if (attribute.IsInvalid)
{
return false; // すべてのループを抜けて戻る
}
}
}
return true;
}
4. メソッドの基礎
メソッドとは
メソッドは、関連する処理をひとまとめにして名前を付けたものです。同じ処理を何度も書く代わりに、メソッドとして定義すれば呼び出すだけで実行できます。
メソッドを使う利点:
- 再利用性: 同じ処理を何度も書かなくてよい
- 可読性: 処理に名前が付くことでコードが理解しやすくなる
- 保守性: 処理の変更が1箇所で済む
- テスト容易性: 単体テストがしやすくなる
┌─────────────────────────────────────────────────────────┐
│ メソッドの構造 │
├─────────────────────────────────────────────────────────┤
│ │
│ アクセス修飾子 戻り値の型 メソッド名(引数) │
│ ────────────────────────────────────── │
│ public string GetDeviceName(string id) │
│ { │
│ // 処理 │
│ return "デバイス名"; // 戻り値 │
│ } │
│ │
└─────────────────────────────────────────────────────────┘
メソッドの定義
public class DeviceService
{
// 引数なし、戻り値なし(void)
public void PrintWelcome()
{
Console.WriteLine("デバイス管理システムへようこそ");
}
// 引数あり、戻り値なし
public void PrintDeviceName(string name)
{
Console.WriteLine($"デバイス名: {name}");
}
// 引数あり、戻り値あり
public int GetDeviceCount(string[] devices)
{
return devices.Length; // 戻り値を返す
}
// 式形式(1行で書ける場合)
// メソッド本体が1つの式だけの場合、=> で簡潔に書ける
public bool IsValidDevice(string name) => !string.IsNullOrEmpty(name);
}
アクセス修飾子の種類:
| 修飾子 | 意味 | 使いどころ |
|---|---|---|
public |
どこからでもアクセス可能 | 外部に公開するAPI |
private |
同じクラス内のみ | 内部処理 |
protected |
同じクラスと派生クラス | 継承先で使用 |
internal |
同じアセンブリ内 | プロジェクト内のみ |
メソッドの呼び出し
var service = new DeviceService();
// 引数なしメソッドの呼び出し
service.PrintWelcome();
// 引数ありメソッドの呼び出し
service.PrintDeviceName("PC-001");
// 戻り値を受け取る
string[] devices = { "PC-001", "PC-002" };
int count = service.GetDeviceCount(devices);
Console.WriteLine($"デバイス数: {count}");
// 戻り値を直接使用することもできる
Console.WriteLine($"デバイス数: {service.GetDeviceCount(devices)}");
静的メソッド
静的メソッド(staticメソッド)は、クラスのインスタンスを作成せずに呼び出せるメソッドです。ユーティリティ関数やヘルパーメソッドに適しています。
public static class DeviceHelper
{
// staticメソッド(インスタンス不要で呼び出し可能)
public static string FormatDeviceName(string name)
{
return name.ToUpper().Trim();
}
public static bool ValidateDeviceId(string id)
{
// GUIDは36文字(ハイフン含む)
return !string.IsNullOrEmpty(id) && id.Length == 36;
}
}
// 呼び出し(new不要でクラス名から直接呼べる)
string formatted = DeviceHelper.FormatDeviceName("pc-001"); // "PC-001"
bool isValid = DeviceHelper.ValidateDeviceId("550e8400-e29b-41d4-a716-446655440000");
staticを使うべき場面:
- インスタンスの状態を持たない処理
- ユーティリティ関数、ヘルパーメソッド
- 拡張メソッド
- ファクトリメソッド
5. 引数と戻り値
引数の種類
C#には複数の引数の渡し方があります。それぞれの違いを理解することは重要です。
┌─────────────────────────────────────────────────────────┐
│ 引数の渡し方 │
├─────────────────────────────────────────────────────────┤
│ │
│ 値渡し(デフォルト): 値のコピーが渡される │
│ ───────────────────────────────────── │
│ 元の変数 ──コピー──→ メソッド内の引数 │
│ 10 10(変更しても元に影響なし) │
│ │
│ 参照渡し(ref): 変数自体が渡される │
│ ───────────────────────────────────── │
│ 元の変数 ←──同じ──→ メソッド内の引数 │
│ 10 10(変更すると元も変わる) │
│ │
│ 出力引数(out): メソッドから値を受け取る │
│ ───────────────────────────────────── │
│ 元の変数 ←─設定─── メソッド内で必ず値を設定 │
│ ? 設定された値 │
│ │
└─────────────────────────────────────────────────────────┘
public class ParameterExamples
{
// 値渡し(デフォルト)
// 引数の値のコピーがメソッドに渡される
public void ModifyValue(int x)
{
x = x * 2; // 元の変数には影響しない
}
// 参照渡し(ref)
// 変数自体への参照が渡される
// 呼び出し側で事前に初期化が必要
public void ModifyRef(ref int x)
{
x = x * 2; // 元の変数が変更される
}
// 出力引数(out)
// メソッド内で必ず値を設定する必要がある
// 呼び出し側での初期化は不要
public bool TryParseDeviceCount(string input, out int count)
{
return int.TryParse(input, out count);
}
// 入力専用参照(in)
// 読み取り専用の参照渡し
// 大きな構造体をコピーせずに渡す場合に有効
public void ProcessLargeData(in LargeStruct data)
{
// dataは読み取り専用(変更不可)
Console.WriteLine(data.Value);
}
}
// 使用例
var examples = new ParameterExamples();
int value = 10;
examples.ModifyValue(value);
Console.WriteLine(value); // 10(変わらない)
examples.ModifyRef(ref value);
Console.WriteLine(value); // 20(変わる)
// TryParseパターン:成功/失敗をboolで返し、結果をoutで返す
if (examples.TryParseDeviceCount("50", out int count))
{
Console.WriteLine($"デバイス数: {count}");
}
デフォルト引数
メソッドの引数にデフォルト値を設定できます。呼び出し時に省略されると、デフォルト値が使用されます。
public class SearchService
{
// デフォルト引数(末尾の引数から指定)
public List<string> SearchDevices(
string? keyword = null, // デフォルトはnull
int pageSize = 20, // デフォルトは20
bool activeOnly = true) // デフォルトはtrue
{
Console.WriteLine($"検索: {keyword}, サイズ: {pageSize}, 有効のみ: {activeOnly}");
return new List<string>();
}
}
// 呼び出し方法
var search = new SearchService();
// すべてデフォルト値を使用
search.SearchDevices();
// 一部だけ指定(前から順に)
search.SearchDevices("Windows");
search.SearchDevices("Windows", 50);
// 名前付き引数で特定のものだけ指定
search.SearchDevices(pageSize: 100);
// 名前付き引数は順序を変えられる
search.SearchDevices(activeOnly: false, keyword: "Mac");
名前付き引数のメリット:
- どの引数に何を渡しているか明確になる
- 引数の順序を気にしなくてよい
- 可読性が向上する
params配列
可変長引数を受け取りたい場合、paramsキーワードを使用します。
public class Logger
{
// paramsは最後の引数にのみ使用可能
public void Log(string message, params object[] args)
{
string formatted = string.Format(message, args);
Console.WriteLine($"[LOG] {formatted}");
}
}
// 使用例
var logger = new Logger();
// 引数なし
logger.Log("処理開始");
// 引数1つ
logger.Log("デバイス {0} を処理中", "PC-001");
// 引数複数
logger.Log("{0} のステータスを {1} に変更", "PC-001", "有効");
// 配列を直接渡すことも可能
string[] data = { "PC-001", "有効" };
logger.Log("{0} のステータスを {1} に変更", data);
複数の戻り値
C# 7.0以降では、タプルを使って複数の値を返すことができます。
public class DeviceAnalyzer
{
// タプルで複数の値を返す
public (int total, int active, int inactive) GetDeviceStats(List<bool> statuses)
{
int active = statuses.Count(s => s);
int inactive = statuses.Count(s => !s);
return (statuses.Count, active, inactive); // タプルを返す
}
// 名前付きタプル
public (string Name, bool IsOnline, DateTime LastSeen) GetDeviceInfo(string deviceId)
{
// 実際にはDBなどから取得
return ("PC-001", true, DateTime.Now);
}
}
// 使用例
var analyzer = new DeviceAnalyzer();
// タプルとして受け取り
var stats = analyzer.GetDeviceStats(new List<bool> { true, false, true });
Console.WriteLine($"合計: {stats.total}, 有効: {stats.active}, 無効: {stats.inactive}");
// 分解して受け取り(デコンストラクト)
var (name, isOnline, lastSeen) = analyzer.GetDeviceInfo("device-1");
Console.WriteLine($"{name}: {(isOnline ? "オンライン" : "オフライン")}");
// 一部だけ受け取り(不要な値は_で破棄)
var (deviceName, _, _) = analyzer.GetDeviceInfo("device-1");
タプルを使うべき場面:
- 2〜3個の関連する値を返す場合
- 一時的なデータ構造として
- メソッドの結果と状態を同時に返す場合
クラスを使うべき場面:
- 4個以上の値を返す場合
- 同じ構造を複数箇所で使う場合
- 関連するロジックも一緒に持たせたい場合
6. メソッドのオーバーロード
オーバーロードとは
同じ名前で異なる引数を持つメソッドを複数定義することをオーバーロード(多重定義)と言います。呼び出し側は引数によって自動的に適切なメソッドが選ばれます。
オーバーロードの条件:
- メソッド名が同じ
- 引数の数または型が異なる
- 戻り値の型だけが異なるのはNG(コンパイルエラー)
public class DeviceFormatter
{
// 引数なし
public string Format()
{
return "デバイス情報なし";
}
// 文字列1つ
public string Format(string deviceName)
{
return $"デバイス: {deviceName}";
}
// 文字列2つ
public string Format(string deviceName, string osType)
{
return $"デバイス: {deviceName} ({osType})";
}
// 異なる型
public string Format(int deviceCount)
{
return $"デバイス数: {deviceCount}台";
}
// 配列
public string Format(string[] deviceNames)
{
return $"デバイス一覧: {string.Join(", ", deviceNames)}";
}
}
// 使用例:引数によって呼び出されるメソッドが決まる
var formatter = new DeviceFormatter();
Console.WriteLine(formatter.Format()); // "デバイス情報なし"
Console.WriteLine(formatter.Format("PC-001")); // "デバイス: PC-001"
Console.WriteLine(formatter.Format("PC-001", "Windows")); // "デバイス: PC-001 (Windows)"
Console.WriteLine(formatter.Format(50)); // "デバイス数: 50台"
オーバーロードのメリット:
- 同じ機能を異なる入力で使える
- メソッド名を覚えやすい
- API設計がシンプルになる
7. ローカル関数とラムダ式
ローカル関数
ローカル関数は、メソッドの中で定義する関数です。外部に公開せず、そのメソッド内でのみ使用する処理をカプセル化できます。
ローカル関数を使うメリット:
- 補助的な処理を近くに定義できる
- メソッドのスコープ内でのみ有効(他から呼べない)
- 外側のメソッドの変数にアクセスできる
public class DataProcessor
{
public List<string> ProcessDevices(List<string> devices)
{
// ローカル関数(このメソッド内でのみ使用可能)
string Normalize(string name)
{
return name.Trim().ToUpper();
}
bool IsValid(string name)
{
return !string.IsNullOrEmpty(name) && name.Length > 0;
}
// ローカル関数を使用
var result = new List<string>();
foreach (var device in devices)
{
if (IsValid(device))
{
result.Add(Normalize(device));
}
}
return result;
}
}
ラムダ式
ラムダ式は、無名関数(名前のない関数)を簡潔に書くための構文です。主にLINQやイベントハンドラで使用されます。
┌─────────────────────────────────────────────────────────┐
│ ラムダ式の構文 │
├─────────────────────────────────────────────────────────┤
│ │
│ (引数) => 式 式形式(1つの式) │
│ (引数) => { 文; 文; } ブロック形式(複数の文) │
│ │
│ 例: │
│ x => x * x xを受け取り、x * xを返す │
│ (a, b) => a + b a, bを受け取り、合計を返す │
│ () => Console.WriteLine() 引数なし │
│ x => { ... } 複数行の処理 │
│ │
└─────────────────────────────────────────────────────────┘
// 式形式:1つの式のみ
Func<int, int> square = x => x * x;
Console.WriteLine(square(5)); // 25
// ブロック形式:複数の文
Func<int, int, int> add = (a, b) =>
{
int result = a + b;
return result; // returnが必要
};
Console.WriteLine(add(3, 4)); // 7
// 引数なし
Action sayHello = () => Console.WriteLine("Hello");
sayHello(); // Hello
// 型を明示(コンパイラが推論できない場合に必要)
Func<string, bool> isLong = (string s) => s.Length > 10;
デリゲート型
デリゲートは、メソッドを変数のように扱うための型です。ラムダ式を代入できます。
組み込みデリゲート:
| デリゲート | 説明 | 例 |
|---|---|---|
Action |
戻り値なし |
Action<string> - stringを受け取り戻り値なし |
Func |
戻り値あり |
Func<int, string> - intを受け取りstringを返す |
Predicate |
boolを返す |
Predicate<int> - intを受け取りboolを返す |
// Action: 戻り値なし
Action action1 = () => Console.WriteLine("実行");
Action<string> action2 = (msg) => Console.WriteLine(msg);
Action<string, int> action3 = (msg, count) => Console.WriteLine($"{msg}: {count}");
// Func: 戻り値あり(最後の型が戻り値)
Func<int> func1 = () => 42; // 引数なし、int戻り
Func<int, int> func2 = x => x * 2; // int引数、int戻り
Func<int, int, int> func3 = (a, b) => a + b; // int2つ、int戻り
Func<string, bool> func4 = s => !string.IsNullOrEmpty(s);
// Predicate: bool戻り値(Func<T, bool>と同等)
Predicate<int> isPositive = x => x > 0;
LINQでの活用
ラムダ式は、LINQ(Language Integrated Query)と組み合わせて使うと非常に強力です。
var devices = new List<string> { "PC-001", "PC-002", "Mac-001", "PC-003" };
// Where: 条件に一致する要素をフィルタリング
var pcDevices = devices.Where(d => d.StartsWith("PC"));
// d => d.StartsWith("PC") は「dがPCで始まるならtrue」を意味
// Select: 各要素を変換
var upperNames = devices.Select(d => d.ToUpper());
// 結果: "PC-001", "PC-002", "MAC-001", "PC-003"
// OrderBy: ソート
var sorted = devices.OrderBy(d => d);
// FirstOrDefault: 条件に一致する最初の要素(なければnull)
var first = devices.FirstOrDefault(d => d.Contains("Mac"));
// 結果: "Mac-001"
// Any: 条件を満たす要素が1つでもあるか
bool hasMac = devices.Any(d => d.StartsWith("Mac"));
// 結果: true
// All: すべての要素が条件を満たすか
bool allValid = devices.All(d => d.Length > 0);
// 結果: true
// Count: 条件を満たす要素数
int pcCount = devices.Count(d => d.StartsWith("PC"));
// 結果: 3
// メソッドチェーン:複数のLINQメソッドを連結
var result = devices
.Where(d => d.StartsWith("PC")) // PCで始まるもの
.Select(d => d.ToUpper()) // 大文字に変換
.OrderBy(d => d) // ソート
.ToList(); // Listに変換
8. 演習問題
問題1: 成績判定メソッド
点数(0〜100)を受け取り、成績(A/B/C/D/F)を返すメソッドを作成してください。
- 90以上: A
- 80以上: B
- 70以上: C
- 60以上: D
- 60未満: F
ヒント: switch式の範囲パターンが使えます。
解答例
public static string GetGrade(int score)
{
// 入力チェック
if (score < 0 || score > 100)
{
throw new ArgumentOutOfRangeException(nameof(score), "点数は0〜100の範囲で指定してください");
}
// switch式で範囲パターンを使用
return score switch
{
>= 90 => "A",
>= 80 => "B",
>= 70 => "C",
>= 60 => "D",
_ => "F"
};
}
// テスト
Console.WriteLine(GetGrade(95)); // A
Console.WriteLine(GetGrade(72)); // C
Console.WriteLine(GetGrade(45)); // F
問題2: デバイスフィルタリング
デバイス名のリストから、指定されたOSタイプで始まるデバイスのみを抽出するメソッドを作成してください。
ヒント: LINQのWhereメソッドとStartsWithを使います。大文字小文字を区別しないようにしましょう。
解答例
public static List<string> FilterDevicesByOs(List<string> devices, string osPrefix)
{
// nullや空のリストのチェック
if (devices == null || devices.Count == 0)
{
return new List<string>();
}
// LINQで大文字小文字を区別せずフィルタリング
return devices
.Where(d => d.StartsWith(osPrefix, StringComparison.OrdinalIgnoreCase))
.ToList();
}
// テスト
var allDevices = new List<string>
{
"Windows-PC001",
"Windows-PC002",
"Mac-001",
"Linux-Server01"
};
var windowsDevices = FilterDevicesByOs(allDevices, "Windows");
foreach (var device in windowsDevices)
{
Console.WriteLine(device);
}
// 出力:
// Windows-PC001
// Windows-PC002
問題3: デバイス統計
デバイスリストを受け取り、以下の情報をタプルで返すメソッドを作成してください。
- 総数
- 有効なデバイス数(名前が空でない)
- 最も長い名前
ヒント: タプルで複数の値を返します。LINQのCount、OrderByDescending、FirstOrDefaultを使います。
解答例
public static (int Total, int ValidCount, string? LongestName) GetDeviceStats(List<string?> devices)
{
// 空のリストの場合
if (devices == null || devices.Count == 0)
{
return (0, 0, null);
}
int total = devices.Count;
// 有効なデバイス数(nullでも空でもない)
int validCount = devices.Count(d => !string.IsNullOrEmpty(d));
// 最も長い名前
string? longestName = devices
.Where(d => !string.IsNullOrEmpty(d)) // 有効なものだけ
.OrderByDescending(d => d!.Length) // 長さで降順ソート
.FirstOrDefault(); // 最初の1つ(最長)
return (total, validCount, longestName);
}
// テスト
var devices = new List<string?>
{
"PC-001",
null,
"Windows-Desktop-001",
"",
"Mac-001"
};
var (total, valid, longest) = GetDeviceStats(devices);
Console.WriteLine($"総数: {total}"); // 5
Console.WriteLine($"有効数: {valid}"); // 3
Console.WriteLine($"最長名: {longest}"); // Windows-Desktop-001
問題4: ページング処理
デバイスリストをページングして返すメソッドを作成してください。
- pageSize: 1ページあたりの件数
- pageNumber: ページ番号(1始まり)
ヒント: LINQのSkipとTakeを使います。Skip((pageNumber - 1) * pageSize)で開始位置を計算します。
解答例
// ジェネリックメソッドにすることで、どの型のリストでも使える
public static List<T> GetPage<T>(List<T> items, int pageNumber, int pageSize = 10)
{
// 空のリストの場合
if (items == null || items.Count == 0)
{
return new List<T>();
}
// 引数の補正
if (pageNumber < 1)
{
pageNumber = 1;
}
if (pageSize < 1)
{
pageSize = 10;
}
// Skip: 先頭からスキップする数
// Take: 取得する数
return items
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToList();
}
// 総ページ数を取得するヘルパー
public static int GetTotalPages<T>(List<T> items, int pageSize = 10)
{
if (items == null || items.Count == 0)
{
return 0;
}
// 切り上げ除算
return (int)Math.Ceiling((double)items.Count / pageSize);
}
// テスト
var devices = Enumerable.Range(1, 25).Select(i => $"Device-{i:D3}").ToList();
// Device-001, Device-002, ... Device-025
int pageSize = 10;
int totalPages = GetTotalPages(devices, pageSize); // 3ページ
for (int page = 1; page <= totalPages; page++)
{
Console.WriteLine($"--- ページ {page}/{totalPages} ---");
var pageItems = GetPage(devices, page, pageSize);
foreach (var item in pageItems)
{
Console.WriteLine(item);
}
}
// 出力:
// --- ページ 1/3 ---
// Device-001 〜 Device-010
// --- ページ 2/3 ---
// Device-011 〜 Device-020
// --- ページ 3/3 ---
// Device-021 〜 Device-025
問題5: メソッドチェーン
以下の処理をメソッドチェーンで実装してください。
- デバイス名のリストを受け取る
- 空の要素を除外
- 大文字に変換
- アルファベット順にソート
- 先頭5件を取得
ヒント: Where → Select → OrderBy → Take の順でチェーンします。
解答例
public static List<string> ProcessDeviceNames(IEnumerable<string?> devices)
{
return devices
.Where(d => !string.IsNullOrEmpty(d)) // 1. nullと空文字を除外
.Select(d => d!.ToUpper()) // 2. 大文字に変換(!はnullでないことを明示)
.OrderBy(d => d) // 3. アルファベット順
.Take(5) // 4. 先頭5件
.ToList(); // 5. Listに変換
}
// テスト
var devices = new List<string?>
{
"zebra-001",
null,
"alpha-001",
"",
"beta-002",
"gamma-001",
"delta-001",
"epsilon-001"
};
var result = ProcessDeviceNames(devices);
foreach (var device in result)
{
Console.WriteLine(device);
}
// 出力(アルファベット順の先頭5件):
// ALPHA-001
// BETA-002
// DELTA-001
// EPSILON-001
// GAMMA-001
まとめ
このレッスンで学んだこと:
| カテゴリ | 学習内容 | ポイント |
|---|---|---|
| 条件分岐 | if文 | 条件によって処理を分岐 |
| 条件分岐 | switch文・switch式 | 1つの値で多分岐、パターンマッチング |
| ループ | for/foreach/while | 繰り返し処理、foreachが最も安全 |
| メソッド | 定義と呼び出し | 処理の再利用、可読性向上 |
| 引数 | 値渡し/ref/out | データの受け渡し方法 |
| 引数 | デフォルト引数/params | 柔軟なメソッド設計 |
| 戻り値 | タプル | 複数の値を返す |
| オーバーロード | 同名メソッド | 引数によって処理を切り替え |
| ラムダ式 | 無名関数 | LINQとの組み合わせ |
次のレッスンでは、クラスとオブジェクト指向プログラミングについて学びます。