C# の概要
この章は C# の完全な入門ではありません。後の章で必ず使う構文を取り上げ、コードを読むときに困らないようにします。Java、TypeScript、Kotlin などの静的型付け言語を書いたことがあれば、大半はすぐに理解できるので、ざっと目を通すだけでよいでしょう。
ほかの章とは異なり、この章は「概要」として複数の小さな構文をまとめて紹介します。共通するテーマは、C# は静的型付け言語であり、コンパイラーは実行前に各値の型を把握し、それを使ってコードを検査することです。後の章はすべてこの前提に基づいています。
章全体のコードはコンソールプログラムです。
// 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 を置き換えてから実行します。
dotnet run出力例:
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
// 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 に直接アクセスするよう変更すると、コンパイル時に警告が出ます。
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
// 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 キーワードは省略可能)で、「データのまとまり」を表すのに適しています。便利な特徴が二つあります。
- 値による等価性:record のすべてのプロパティが等しければ、record 同士も等しいと判定されます。21 行目の
milk == new Todo(1, "Buy milk", false)は、別々のオブジェクトであってもTrueになります。 - 初期化後に再代入できない:この例で自動生成された
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
// 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 待機中にスレッドを占有しないようにします。
