C#
melangの各noteで他言語と比較した、C#の設計をまとめたページ。ここに書かれている内容は、各noteの比較表から自動で集めたものです。
型
型推論(var)はあるが、明示するスタイルも根強い。
int age = 20;int age = 20;
var name = "Alice"; // var による型推論`var`によるローカル変数の型推論が使えるが、型を明示するスタイルも広く使われる。Nullable参照型(`string?`)でNull許容を型として区別できる。
型注釈の位置
Javaと同様prefix方式。`var`による推論はローカル変数に限定。
int age = 20;int age = 20;
var name = "Alice"; // varによる推論
string Greet(string name) { // 引数・戻り値は常にprefixで明示
return $"Hello, {name}";
}`Type name`という前置構文が基本で、Javaと同じ設計思想を継承する。`var`はローカル変数の型推論に使えるが、メソッドの引数・戻り値には使えず、公開シグネチャは常に明示的な型を書くことになる。
型変換
失敗し得る変換にTry接頭辞を使う先行例を持つ。
int y = (int) x;long big = 3_000_000_000;
int a = (int) big; // 既定では黙って値が変わる
int b = checked((int) big); // OverflowException
int n = int.Parse("42");
int m = int.Parse("abc"); // FormatException
if (int.TryParse("abc", out int v)) {
// 成功したときだけvが使える
}失敗し得る変換に`TryParse`という名前を付け、`bool`と`out`引数で成功・失敗を返す慣習が古くからある。`try`を名前に含めて失敗可能性をコード上に示す点はmelangと同じ発想。ただし`bool`は失敗の理由を持たず、`out`引数のため書き方も冗長になる。数値のキャストは既定で検査されない。
暗黙の型変換
拡大変換は暗黙、縮小変換は明示的なキャストが必要。
int x = 10;int x = 10;
long y = x; // 暗黙にlongへ拡大変換される
short s = (short) x; // 縮小変換にはキャストが必要intからlong、floatからdoubleのような拡大変換は暗黙に行われる。ユーザー定義型でも`implicit operator`を定義すれば独自の暗黙変換を組み込める。縮小変換は明示的なキャストが必要。
Enum / Sum Type
enumはunderlying integral typeを持つ整数寄りの列挙型。
enum Status { Pending, Running, Completed }enum Status { Pending, Running, Completed }
switch (status) {
case Status.Pending: /* ... */ break;
case Status.Running: /* ... */ break;
} // Completedを書かなくてもコンパイルは通る`enum`はunderlying integral typeを持つ列挙型で、payloadを持てない。ADTAlgebraic Data Type(代数的データ型)の略。複数の型を組み合わせて新しい型を作る手法で、「AまたはB」のように取りうる形が閉じた集合になっているものをSum Type(直和型)と呼ぶ。Rustの`enum`やHaskellの`data`が代表例で、各variant(構成要素)が異なるフィールドを持てる点で、単純な整数の列挙とは異なる。表現には`record`の継承階層やパターンマッチを組み合わせる必要があり、`switch`の網羅性チェックは基本的にない。
値の不変性
既定は変更可能。`readonly`・`record`・`init`で部分的に制限する。
readonly T x; / record User(string Name);public record User(string Name); // initのみのプロパティ
var a = new User("alice");
// a.Name = "bob"; // コンパイルエラー
var b = a with { Name = "bob" }; // 新しい値を生成
private readonly List<string> tags = new();
// tags = new(); // コンパイルエラー(コンストラクタ外)
tags.Add("admin"); // OK: 参照先は変更できる
var im = ImmutableList.Create("a"); // System.Collections.Immutable`readonly`はフィールドの再代入を防ぐだけで、参照先は変更できる。`record`と`with`式で、新しい値を作る書き方が言語機能になっている。`ImmutableList`などの不変コレクションは別のライブラリで、使うかどうかは書き手が選ぶ。
名前付き引数
名前付き引数と位置引数を併用でき、オプション引数と組み合わせてよく使われる。
void Foo(int width, int height = 0)void Foo(int width, int height = 0) {}
Foo(width: 10, height: 20); // 名前付き
Foo(10, height: 20); // 位置引数の後に名前付きを続けられる
Foo(height: 20, width: 10); // 名前付きだけなら順序は自由C# 4.0で導入。位置引数の後ろに名前付き引数を続けられるが、名前付き引数の後に位置引数を書くことはできない。オプション引数と組み合わせ、多数の引数を持つAPIを呼びやすくする目的で使われる。
関数の引数
ref・out・inを、定義側と呼び出し側の両方に書く先行例。
void F(ref int x) / void F(out int x) / void F(in T x)void A(int n) {}
void B(ref int n) { n++; }
void C(out int n) { n = 1; }
void D(in BigStruct s) {}
int x = 0;
A(x);
B(ref x); // 呼び出し側にも ref が必要
C(out x); // 呼び出し側にも out が必要
D(in s); // in は呼び出し側では省略できる`ref` / `out`は、定義側と呼び出し側の両方にキーワードを書かないとコンパイルエラーになる。呼び出しを読むだけで、変数が書き換えられる可能性が分かる。書き換えの可能性を呼び出し側に明示する先行例。melangは書き換え自体をなくすため、この印が要らない。ただし`in`は呼び出し側で省略でき、通常の値渡しには印がない。
if 文/式
`if`はstatement。値の分岐には条件演算子`?:`やswitch式を使う。
if (cond) { ... } else { ... } / cond ? a : bvar x = cond ? 1 : 2;
if (cond)
{
Process();
}Javaと同様に、`if`はstatementで値を返せない。値の分岐は条件演算子と、パターンを扱えるswitch式で書く。
I/O の明示
`Task`を返す慣習はあるが、コンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。は強制しない。
async Task<User> FindUserAsync(UserId id)async Task<User> FindUserAsync(UserId id) { /* ... */ }
User GetUser(UserId id) {
FindUserAsync(id); // 待たずに呼べてしまう。呼び出し元がasyncでなくてもコンパイルは通る
...
}`Task`を返すメソッドを`async`を付けずに呼ぶこと自体は許される(結果を待たないだけ)。メソッド名に`Async`を付ける命名規約はあるが、コンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。は検査しない。同期的なファイルI/Oなどは、通常のメソッドと区別なく呼べる。
言語名
記号を含み、発音(シーシャープ)と表記が一致しない。
`#`(シャープ)を含み、音楽の調号を模した名前だが、ファイル名や多くの文脈では`#`をそのまま使えず`csharp`や`cs`に置き換えられる。発音は自然だが表記の面で常に変換が必要という、C++と同種の課題を持つ。