← melang

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 : b
var 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++と同種の課題を持つ。