本文へスキップ

C# の概要 ​

この章は C# の完全な入門ではありません。後の章で必ず使う構文を取り上げ、コードを読むときに困らないようにします。Java、TypeScript、Kotlin などの静的型付け言語を書いたことがあれば、大半はすぐに理解できるので、ざっと目を通すだけでよいでしょう。

ほかの章とは異なり、この章は「概要」として複数の小さな構文をまとめて紹介します。共通するテーマは、C# は静的型付け言語であり、コンパイラーは実行前に各値の型を把握し、それを使ってコードを検査することです。後の章はすべてこの前提に基づいています。

章全体のコードはコンソールプログラムです。

01-csharp-tour/Program.cs
cs
// 1. Static typing and type inference
int count = 2;
var owner = "Alex";

// 2. String interpolation
Console.WriteLine($"{owner} has {count} todo items");

// 3. Nullable reference types
string? nickname = null;
Console.WriteLine($"Nickname length: {nickname?.Length ?? 0}");

// 4. Lambda expressions
Func<int, int> square = x => x * x;
Console.WriteLine($"Square of 5 is {square(5)}");

// 5. Records
var milk = new Todo(1, "Buy milk", false);
var milkDone = milk with { Done = true };
Console.WriteLine(milk);
Console.WriteLine(milkDone);
Console.WriteLine($"Value equality: {milk == new Todo(1, "Buy milk", false)}");

// 6. Collections and LINQ
List<Todo> todos = [milk, new Todo(2, "Write code", true)];
var pending = todos.Where(t => !t.Done).Select(t => t.Title);
Console.WriteLine($"Pending: {string.Join(", ", pending)}");

// 7. async / await
var message = await LoadMessageAsync();
Console.WriteLine(message);

static async Task<string> LoadMessageAsync()
{
    await Task.Delay(100);
    return "Async operation complete";
}

record Todo(int Id, string Title, bool Done);

samples/01-csharp-tour ディレクトリで実行するか、「開発環境の準備」と同じ手順で dotnet new console を使ってプロジェクトを作り、Program.cs を置き換えてから実行します。

bash
dotnet run

出力例:

text
Alex has 2 todo items
Nickname length: 0
Square of 5 is 25
Todo { Id = 1, Title = Buy milk, Done = False }
Todo { Id = 1, Title = Buy milk, Done = True }
Value equality: True
Pending: Buy milk
Async operation complete

三つのグループに分けて見ていきましょう。

型、nullable、Lambda ​

01-csharp-tour/Program.cs
cs
// 1. Static typing and type inference
int count = 2;
var owner = "Alex";

// 2. String interpolation
Console.WriteLine($"{owner} has {count} todo items");

// 3. Nullable reference types
string? nickname = null;
Console.WriteLine($"Nickname length: {nickname?.Length ?? 0}");

// 4. Lambda expressions
Func<int, int> square = x => x * x;
Console.WriteLine($"Square of 5 is {square(5)}");

// 5. Records
var milk = new Todo(1, "Buy milk", false);
var milkDone = milk with { Done = true };
Console.WriteLine(milk);
Console.WriteLine(milkDone);
Console.WriteLine($"Value equality: {milk == new Todo(1, "Buy milk", false)}");

// 6. Collections and LINQ
List<Todo> todos = [milk, new Todo(2, "Write code", true)];
var pending = todos.Where(t => !t.Done).Select(t => t.Title);
Console.WriteLine($"Pending: {string.Join(", ", pending)}");

// 7. async / await
var message = await LoadMessageAsync();
Console.WriteLine(message);

static async Task<string> LoadMessageAsync()
{
    await Task.Delay(100);
    return "Async operation complete";
}

record Todo(int Id, string Title, bool Done);

静的型付けと var ​

2 行目では型 int を明示しています。3 行目では var を使い、右辺の値からコンパイラーに型を推論させています。owner の型は string で、その後もずっと string です。

var は「動的型付け」ではありません。後から owner = 42; と書くと、コンパイルに失敗します。型名を繰り返し書かずに済むだけで、型はコンパイル時に確定します。VS Code で変数にマウスを合わせると、推論された型を確認できます。

FastAPI との比較

Python の型ヒントは任意で、実行時にも検査されません。FastAPI は型ヒントを読み取り、データを変換します。C# の型は必須で、コンパイラーが使用箇所をすべて検査します。型が合わなければ実行できません。後で見るように、ASP.NET Core もこれらの型を読み取り、パラメーターのバインドやドキュメント生成に利用します。

文字列補間 ​

6 行目の文字列には $ が付いています。これは文字列補間(string interpolation)です。波括弧の中に変数や式を直接書くと、実行時にその値へ置き換えられます。Python の f-string や JavaScript のテンプレート文字列と同じ考え方です。

Nullable 参照型 ​

9 行目の string? は「この変数は null の可能性がある」という意味です。疑問符のない string は「null であってはならない」という意味です。この機能はnullable 参照型(nullable reference types)と呼ばれ、プロジェクトファイルの <Nullable>enable</Nullable> で有効になります。.NET のテンプレートでは既定で有効です。

なぜ必要なのでしょうか。 null のメンバーにアクセスするエラーは、実行時クラッシュの最も一般的な原因の一つです。nullable の注釈があれば、コンパイラーが問題を事前に見つけられます。たとえば 10 行目を nickname.Length に直接アクセスするよう変更すると、コンパイル時に警告が出ます。

text
Program.cs(10,19): warning CS8602: 解引用可能出现空引用。

10 行目の正しい書き方では、二つの演算子を使っています。

  • ?.(null 条件演算子):nickname が null なら .Length にアクセスせず、式全体の結果を null にします。
  • ??(null 合体演算子):左辺が null の場合に右辺の値を使います。ここでは 0 です。

注意

nullable の注釈が検査するのはコンパイル時で、実行時に null の発生を防ぐものではありません。たとえば JSON からデシリアライズしたデータでは、型を string と書いていても、フィールドがなければ null の場合があります。「リクエストボディ」の章でこの状況に出会い、「入力検証」の章で解決します。

Lambda 式 ​

13 行目の x => x * x は Lambda 式(無名関数)です。=> の左側がパラメーター、右側が戻り値です。Func<int, int> はその型で、「int を受け取り、int を返す関数」を表します。

次の章「最初の一歩」で使う app.MapGet("/", () => ...) の第 2 引数が Lambda です。Lambda の本体は波括弧で囲んだコードブロックにもでき、その中で return を使って値を返せます。後の章ではこの書き方が何度も登場します。

FastAPI との比較

Python の lambda x: x * x に相当します。ただし C# の Lambda は複数行の文を含められるため、エンドポイントの処理を Lambda の中に直接書けます。

record と LINQ ​

01-csharp-tour/Program.cs
cs
// 1. Static typing and type inference
int count = 2;
var owner = "Alex";

// 2. String interpolation
Console.WriteLine($"{owner} has {count} todo items");

// 3. Nullable reference types
string? nickname = null;
Console.WriteLine($"Nickname length: {nickname?.Length ?? 0}");

// 4. Lambda expressions
Func<int, int> square = x => x * x;
Console.WriteLine($"Square of 5 is {square(5)}");

// 5. Records
var milk = new Todo(1, "Buy milk", false);
var milkDone = milk with { Done = true };
Console.WriteLine(milk);
Console.WriteLine(milkDone);
Console.WriteLine($"Value equality: {milk == new Todo(1, "Buy milk", false)}");

// 6. Collections and LINQ
List<Todo> todos = [milk, new Todo(2, "Write code", true)];
var pending = todos.Where(t => !t.Done).Select(t => t.Title);
Console.WriteLine($"Pending: {string.Join(", ", pending)}");

// 7. async / await
var message = await LoadMessageAsync();
Console.WriteLine(message);

static async Task<string> LoadMessageAsync()
{
    await Task.Delay(100);
    return "Async operation complete";
}

record Todo(int Id, string Title, bool Done);

record ​

38 行目では、一行で Todo 型を定義しています。

  • 丸括弧内の int Id, string Title, bool Done はコンストラクターの引数であり、同名の init プロパティも自動生成します。値を設定できるのは初期化時だけです。
  • コンパイラーは ToString()(そのため 19 行目では Todo { Id = 1, ... } と表示されます)と、値に基づく等価比較も自動生成します。

この型を record(レコード型)と呼びます。この例は位置指定の record class(class キーワードは省略可能)で、「データのまとまり」を表すのに適しています。便利な特徴が二つあります。

  1. 値による等価性:record のすべてのプロパティが等しければ、record 同士も等しいと判定されます。21 行目の milk == new Todo(1, "Buy milk", false) は、別々のオブジェクトであっても True になります。
  2. 初期化後に再代入できない:この例で自動生成された init プロパティには、初期化時だけ値を設定できます。「変更」するには 18 行目の with 式で一部のプロパティを置き換えたコピーを作ります。元の milk は変わりません。

record 自体が完全な不変性を強制するわけではありません。set を持つプロパティも宣言できます。プロパティが可変リストを参照している場合、init が制限するのはリスト自体の置き換えであり、リスト内の要素の変更は防ぎません。これは浅い不変性(shallow immutability)と呼ばれます。

Web API が record をよく使うのはなぜでしょうか。 リクエストやレスポンスのデータは、基本的に「値のまとまり」です。JSON として受け取り、JSON として返すデータを、途中で誤って変更すべきではありません。record なら少ないコードでこの意図を表現できます。「リクエストボディ」の章から、すべてのデータモデルを record で定義します。

技術詳細

トップレベル ステートメントを使う Program.cs では、record Todo などの型定義をすべてのステートメントの後ろに書く必要があります。そのため各サンプルファイルの末尾に型定義があります。32 行目の static async Task<string> LoadMessageAsync() はローカル関数で、ステートメントの間にも後ろにも書けます。

FastAPI との比較

record は Pydantic モデルや @dataclass(frozen=True) に近く、フィールド宣言でデータの形を表します。record 自体は検証を行いません。検証は「入力検証」の章で ASP.NET Core に別途行わせます。

コレクションと LINQ ​

24 行目の角括弧 [milk, new Todo(2, "Write code", true)] はコレクション式(collection expression)で、List<Todo> を作成します。List<Todo> の <Todo> はジェネリック引数と呼ばれ、「Todo だけを格納できるリスト」を意味します。ほかの型の値を入れるとコンパイルに失敗します。

25 行目は LINQ(Language Integrated Query、統合言語クエリ)です。

  • Where(t => !t.Done):未完了の項目を抽出します。
  • Select(t => t.Title):タイトルだけを取り出します。

どちらも Lambda を引数に受け取り、パイプラインのようにつなげられます。LINQ は後でも頻繁に登場します。「クエリパラメーター」の章では絞り込みとページングに使い、「EF Core の基礎」では同じ書き方が SQL クエリに変換され、データベースへ送られる様子を見ます。

FastAPI との比較

25 行目は Python の [t.title for t in todos if not t.done] に相当します。

async / await ​

01-csharp-tour/Program.cs
cs
// 1. Static typing and type inference
int count = 2;
var owner = "Alex";

// 2. String interpolation
Console.WriteLine($"{owner} has {count} todo items");

// 3. Nullable reference types
string? nickname = null;
Console.WriteLine($"Nickname length: {nickname?.Length ?? 0}");

// 4. Lambda expressions
Func<int, int> square = x => x * x;
Console.WriteLine($"Square of 5 is {square(5)}");

// 5. Records
var milk = new Todo(1, "Buy milk", false);
var milkDone = milk with { Done = true };
Console.WriteLine(milk);
Console.WriteLine(milkDone);
Console.WriteLine($"Value equality: {milk == new Todo(1, "Buy milk", false)}");

// 6. Collections and LINQ
List<Todo> todos = [milk, new Todo(2, "Write code", true)];
var pending = todos.Where(t => !t.Done).Select(t => t.Title);
Console.WriteLine($"Pending: {string.Join(", ", pending)}");

// 7. async / await
var message = await LoadMessageAsync();
Console.WriteLine(message);

static async Task<string> LoadMessageAsync()
{
    await Task.Delay(100);
    return "Async operation complete";
}

record Todo(int Id, string Title, bool Done);

32~36 行目では非同期メソッドを定義しています。

  • async は、メソッド内で await を使用できることを示します。
  • 戻り値の型 Task<string> は「将来 string を返す操作」を表します。
  • 34 行目の await Task.Delay(100) は 100 ミリ秒待機しますが、スレッドをブロックしません。

29 行目では await を使ってメソッドの完了を待ち、結果を取り出します。慣例として、非同期メソッド名は Async で終わります。

Web サービスで非同期処理を使う理由は何でしょうか。 サーバーがリクエストを処理する時間の大半は、データベースの応答、ファイルの読み込み、別サービスからの応答を待つ時間です。同期コードは待機中もスレッドを占有します。非同期コードは待機中にスレッドをサーバーへ返すため、サーバーは別のリクエストを処理できます。同じマシンで同時に処理できるリクエストが大幅に増えます。「EF Core の基礎」から、データベース操作はすべて非同期になります。

FastAPI との比較

概念は Python の async def / await と同じです。ASP.NET Core では同期と非同期のハンドラーを混在させてもフレームワークが正しく処理します。「async 関数内で同期ブロッキング処理を呼ぶとイベントループが止まる」という種類の問題はありません。ただし、ブロッキング呼び出しはスレッドを浪費するため、I/O には引き続き非同期 API を使うべきです。

まとめ ​

  • C# は静的型付け言語です。var はコンパイラーに型を推論させるだけで、型はコンパイル時に確定します。
  • $"..." は文字列補間です。string? はnull 許容を示し、?. と ?? を組み合わせると null を安全に扱えます。nullable の検査はコンパイル時のみです。
  • x => ... の Lambda 式は無名関数です。MapGet などのメソッドでハンドラーとして使います。
  • 位置指定 record はデータ型を簡潔に定義でき、値による等価比較と with によるコピーが可能です。この例のプロパティは init を使いますが、record がすべてのメンバーを不変にするわけではありません。
  • LINQ は Where、Select などを連結してコレクションを処理します。async/await は I/O 待機中にスレッドを占有しないようにします。

次の章は最初の一歩です。最初の Web API を作ります。前の章:開発環境の準備。

.NET 10 と Minimal API を使用 · 各章に実行可能なサンプルを用意