ねののお庭。

かりかりもふもふ。

【C#】何故 C# を好むのか。~他の言語と比較しながら~

世の中には多くの C# に関する誤解が蔓延っています。 偏見にも満ちています。 そして技術的に正しい批判ではなく、根本的に技術的に誤った批判ばかりで正直悲しい。 技術的に正しい形の批判なら「お、そうだな。そしてそれの解決策はですねぇ...(ニヤニヤ)」となるのですが...。 そして C# 界隈から一歩出ると、「え、C# で作ってるの!?なんで??」とか言われる事が非常に多い始末。

C# 大好きマンとしては非常に嘆かわしい。 嘆かわしい限りなので、ここでなぜ C# を私が好むか、そして何故ソフトウェアの開発に向いているかを語りたいと思います。そして誤解が解けたら嬉しい。ついでに C# を書きたいと思ってくれたら嬉しい。

想定読者

  • C# でソフトウェアを開発していると聞いた時に「なんで C#?」と思う方
  • どの言語でソフトウェアを開発をしようか悩んでいる方
  • C# について知りたい方
  • プログラミング言語のオタク
  • 後方腕組おじさんしたい C#er

前書きという名の予防線

まず大前提として、プログラミング言語は宗教ではありません。 あくまでソフトウェアを開発するための道具です。 なのですが世の中のソフトウェアエンジニアという生き物は、自分が使っているプログラミング言語を批判されたりするとまるで自分が否定されたかのように怒り出す人がいます。 宗教じゃないんだから...。

「弘法筆を選ばず」なんて言葉もありますが、世の中の 99.99% のソフトウェアエンジニアは弘法ではありません。弘法でないなら、道具にはこだわるべきです。 なので「ワイにはこれしかないんや!だからこれが一番ええんや!」的スタンスは捨てるべきでしょう。 自分は C# が大好きですが、大好きなのは C# が現時点において他の言語に比べて優れていると考えているから、というだけです。

なので読者が使っているものに対して私が批判したとしても、あくまで道具について思うところを述べているだけであり、別に読者を否定していたりするわけではない事はこの場で明確にしておきたいと思います。

事前知識: C# と .NET

C# の記事を読むにあたっては、.NET という言葉を本来的には理解しておく必要があります。

C# は言わずもがなでしょう、プログラミング言語です。では .NET は何かというと、公式ではapplication platformやdeveloper platformと説明されています。まぁ、端的にいうならば C# (や F#, VB.NET) で記述されたプログラムを駆動させるための諸々です。つまり runtime や標準ライブラリ等々のことをまとめて .NET と呼称しています。

しかしこの記事では正確には .NET 側の話だよな、というところに関しても C# と呼称していたりします。これは C# や .NET に慣れていない方が読んだ時、おそらく理解を妨げるノイズになるからです。たとえば 「C# は高速な言語」といった時、プログラミング言語である C# そのものが速いのでなく、正確には C# で書かれたプログラムを駆動させる「.NET runtime が高速」という話です。他言語で例えると、「JavaScript は高速な言語 (例えばですよ!)」と言った時、より正確には「Node.js が速い」あるいは「Deno が速い」のであって、JavaScript 言語そのものが高速なわけではないわけです。 言語に対して1つの runtime の実装しか存在しないとこのあたりの意識が希薄になるところではありますが。言語仕様も大事ですが、言語仕様がどれだけ優れていても runtime の実装がしょぼいと遅くなりますし、逆に言語仕様がしょぼくても runtime を (莫大な資本を伴って) 狂ったように最適化することで高速にもなります。そんなわけなので正確には区別するべきなのですが、慣れない人にとってはノイズになると思われるので、そのあたりの言葉の使い分けはゆるふわに行きたいと思います。

わりと C#er は C# と .NET 側を明確に分離して理解しており随時言葉を使い分けしていると思います。 そのような方々にとっては違和感を持つ表記があるかもですが、そのあたりはそういう意図で書かれているのだと理解してください (ツッコまないでね)。

C# はパフォーマンスの高い言語

パフォーマンスは正義です。大正義。 プログラミング言語のパフォーマンスは主に3つに要素からなります。

  • 計算処理の速さ
  • 非同期 IO 性能
  • マルチコア・マルチスレッド性能

パフォーマンスが良い事はあらゆる面で嬉しいです。 Web サービスにおいては、ユーザに対するレスポンスが早い事は、ユーザ体験が良いだけでなく、売り上げ等に直結するという事実は近年ではわりと広く知られたお話かと思います。 そしてパフォーマンスが良いという事は当然同量のリクエストを少ないサーバの台数で抑えられるので、クラウドを使っている場合はクラウド費用を抑えられますし、オンプレであれば必要なサーバの台数や電力を抑えられます。 ユーザにとっても、サービス提供者側にとっても嬉しい。パフォーマンスが高くて嫌な人はないでしょう。

そして C# は非常に上記3つのどの側面をとっても優れた、パフォーマンスの良い言語です。 具体的にどれくらいパフォーマンスが優れているか、いくつかのベンチマークをみて行きましょう。

まずは単純な計算処理のベンチマークである Benchmarks Game のベンチマーク結果から。ご覧の通り、C# (左から4番目) は C, C++, Rust の次に高速な言語であるというベンチマーク結果になっています。

そして次に TechEmpower が公開している Web Framework Benchmarks のベンチマークから。 Web Framework のベンチマークは単なる計算処理だけでなく、非同期 IO 性能やマルチコア・マルチスレッド性能も重要になってきます。

asp.net core と書かれているのが C# の Web Framework ですが、Composite Framework Scores (なぜか記事公開時点では見えなくなってしまっていますが、そのうち復活するでしょう)では 18 位に位置しています。順位でいうとぶっち切りで高い...というふうには見えないかもしれませんが、上位に位置づけている Web Framework のそこそこ数がベンチマーク特化で、プロダクションでは使われていないものだったりします。なのでプロダクションで広く使われている Web Framework の中では asp.net core はめちゃくちゃ早い方でしょう。asp.net core より上位で著名なやつ、ntex (Rust), libh2o (C), axum (Rust), quarkus(Java), drogon (C++) とかでしょうか。

ちなみに著名どころかつプロダクションでも使われていそうな他のフレームワークだと以下の通りです。特に Ruby on Rails や Laravel 等は文字通り桁違いに遅いです。

  • hono (JavaScript): 51 位
  • goframe (GO): 54 位
  • express (JavaScript): 62 位
  • ralis (ruby): 125 位
  • laravel (PHP) : 152 位
  • django (Python): 159 位

また gRPC のベンチマーク では C# が Rust の実装を上回り1位に輝いていたりします。

「Rust は早い」という先入観は多くの皆さん持っていると思いますが (実際のところ Rust もどう実装するかで相当変わるんですけどね!)、C# も Rust に負けないくらい早かったりします。 そしてなにより多くの人にとって C# は Rust より圧倒的に書きやすいでしょう。

もう少しいうと、わざわざ遅い言語やフレームワークを用いてパフォーマンスに問題を感じて最適化をしたり、並列台数を増やしてクラウド費用を溶かすより、最初から高速な言語と高速なフレームワークを選択して実装したいとは思いませんか? という事で、やはり皆さん C# を書きませんか?

C# はビルドも高速

ちなみに C# はビルドも非常に高速です。ただビルドが高速である事を示すベンチマークとかあまりないので、私の手元で著名どころの OSS をビルドして計測してきました。計測のレギュレーションは以下の通り。

  • GitHub から clone し、レポジトリ内の既定で有効になっているコード全てをビルド
    • コマンド的には$ dotnet build Xxx.sln
    • 得にオプションとかつけない
      • 当然複数の .NET バージョン (TFM) に対するビルドが走る
        • .NET 8 用 / .NET 9 用でそれぞれビルドが走る、といった具合
    • ただし事前にパッケージの復元 ($ dotnet restore) は実施しておく
  • ビルドは Debug build
    • C# には Debug build と Release build の2種類が存在します
      • その名の通り開発時用とプロダクションとしてリリースする用です
    • Release build は開発時用ビルドより最適化がかかるため少々時間がかかる
      • とはいえ早いです。C++ みたいなエグいビルド時間にはなりません
    • 今回はローカルでの開発体験的なところに焦点を当てているため、Debug build で計測
  • 計算機環境
    • CPU: Ryzen 9 7950X3D
    • メモリ: 128GB

そしてビルド結果が以下の通り。

後半とか結構重量級だと思いますが、フルビルドかけてこんなもんですからね。 C# のビルドは非常に高速であることが分かるでしょう。

さらに、自分たちのアプリケーションを開発している際には以下のような条件になるので、より短い時間でビルドが終わることでしょう。

  • ビルドが必要なプロジェクト (後述) のみビルドが走る
    • キャッシュが効く
    • コードを変更していないプロジェクトはビルドが発生しない
  • ライブラリの場合複数の TFM (Target Framework Moniker, 要は .NET のバージョン) が基本ですが、アプリケーションはライブラリほど複数の TFM に対してビルドしない (アプリケーションは殆どの場合単一 TFM なんじゃないかな)
    • ようするに Node.js 18 向け、Node.js 20 向けでビルド...みたいなことはしない、というようなイメージ。
    • なので実務でのアプリケーション開発は同量のコード量であればライブラリより高速にビルドされるはず。

そして何より C# はビルドで困る事が殆どありません。 C++ や Python などの GitHub に転がっている OSS は git clone してから一発でビルドが通ったりエラーなく実行されるなんて事ぶっちゃけほぼほぼ無いのですが (一発で動いたら歓喜モノ)、C# はそのような事でフラストレーションを抱えることが殆どありません。

C# はオープンソースかつクロスプラットフォーム

これは一生誤解が解けないので言い続けなければならないのですが、C# はオープンソースです。そしてクロスプラットフォームです。なので当然、Windows だけでなく Linux や Mac OS でも動作します。

本当にびっくりするくらい誤解が解けないかというか、いまだに「C# は Windows 用の言語である!」という印象が今も色濃く残ってしまってしまっています (とくに2000年代の知識のままアップデートする事を忘れてしまった おじさん 業界歴の長い方に)。そもそも .NET Framework は windows 専用かつオープンでは無かったわけですが、runtime としてはクロスプラットフォームかつオープンな Mono があったりしたのですけどね...?

また、WEB アプリケーションフレームワークである ASP.NET Core や Windows の GUI を組むためのフレームワークである WPF や WinUI なども OSS となっています。

言語的利点

静的型付け

C# は静的型付け言語です。静的型付け言語はさまざまな面で動的型付け言語に比べてメリットがあります。

よく言われる静的型付けのメリットは以下のようなところでしょう。動的型付けと比べながらみていきましょう。

  • コンパイル時型チェック
    • 実行する前に型の不整合があった場合にコンパイラが怒ってくれる
    • 100%信頼できる
      • 動的型付けでも型アノテーションができますが、所詮アノテーション。100%ではありません。アノテーションミスで実行時の型とアノテーションされている型が地味に異なるとかよくある話です。アノテーションを信頼し、裏切られ、磨耗する事のなんと多い事か...。
        • 動的型付け用の静的解析ツール等を導入しても 100% の解析は不可能であり、正直漏れが多い。
  • コンパイル時最適化
    • パフォーマンスは正義
  • ドキュメントとしての型
    • 引数にせよ返り値にせよ一時変数にせよ、コード中に現れるあらゆるオブジェクトの型が常に明確であり、結果的にそのオブジェクトにはどのようなプロパティやメソッドが定義されているか、即座に理解が可能です。そしてそれは 100% 信頼できます。
      • 動的型付けの場合は実行してみたり、ドキュメントを読み込んでみたり、はたまたテストを見に行かなければ実際に渡される値がどのような型を期待しているか全く分かりません。
      • そしてドキュメントやテストだけでは十分ではありません。なぜなら動的型付けにおいては「言語レベルではメソッドの引数から返り値など、あらゆるオブジェクトの型の可能性が無限に存在」するから。any ã‚„ void* で全部やりとりされているようなものです、恐怖しかありません。
      • 型アノテーションを 100% 信頼する事は出来ませんし、JavaScript の JSDoc ã‚„ Python の docstring なども当然 100% の信頼をおくことはできません。
  • 補完ビリティ
    • エディタや IDE は「このオブジェクトはこのメソッドやプロパティを持っているよ」というのを「.(ドット)」を押すと候補をいろいろ出してくれますが、これはそのオブジェクトの型がなにかコーディングしている最中に解析可能だから実現できることです。動的型付け言語でも多少の補完はしてくれますが、基本的には実行時にそのオブジェクトがどのような型になるかをエスパーし、そのエスパーが完了したらメソッド名などのタイピングを頑張る必要があります。静的型付けならエスパーは不要であり、補完候補から選択すればいいだけなのでタイピングも不要です。

静的型付け言語は 100% の精度で多くの事を保証してくれます。この 100% というのがとてもうれしい。コンパイル時型チェックはもちろん、型をドキュメントとしてもみてもやはり 100% 正しい。 ソフトウェア開発では、基本的にコードは書くより読む事の方によっぽど時間を使うわけですが、この 100% という前提がないとさまざまな可能性を常に考慮しながら読んでいく必要がありますから、読解のハードルが跳ね上がります。それに比べて型が明確に記述されたコードは圧倒的に理解しやすく、さまざまな条件を型に押し付けられるので、脳に収まりやすく価値があります。

そもそも「多くの事を人力ではなく仕組みにしてしまおう」というのがコーディングをする人間にとっては当たり前な事であり美徳なわけですが、なぜ型を書くのがめんどくさいからといってあらゆる事を人力で保証しなければならない動的型付け言語を好むのでしょうか?動的型付けを選択した瞬間に仕組みで保証できるさまざまな事柄が何1つもなくなるのにも関わらず、です。

テスト面

現代のソフトウェア開発においてテストは必須でしょう。なぜ必須か?それは間違いなく「コードの振る舞いを保証し続けたい」からでしょう。なのでテストを重要だと考える事は、「コードの振る舞いを保証し続ける事」を重要だと考える事です。ここで大事なのは保証するという事です。

しかし動的型付け言語の場合どうでしょうか?コード中に現れるそのインスタンスの型は?パラメータの型は?返り値の型は?そう、何も保証がないのです。「コードの振る舞いを保証し続ける事」を大事だと考えているのに、100% の精度で保証してくれる型を無視する事により、自ら苦しい道に突入しているといっていいでしょう。

個人的な体感からすると、動的型付け言語でコーディングをしている際に起きる問題、とくにゼロから全て自分自身で記述しているではなく、他人が書いた既存のコードに変更を加えて起きる問題のそのほとんどはロジックのミスよりも先に型絡みです。開発中に気づける類ならまだマシなほうで、本番環境において何か事故が起きた際も、外部のライブラリが特定の条件下において予期していた型と異なる型の返り値を返すがために、障害が発生するなんて事があります (こんな事あるんか?と思うかもしれませんが Python の場合広く使われているライブラリでもこのような振る舞いをするものは当たり前のように存在します)。これらは静的型付け言語であれば実行するより前の段階で網羅的にコンパラがチェックしてくれる事に対して、わざわざテストで自分自身でテストを書いて実行時に気づかないといけません。ましてや本番環境で型まわりで問題が起きるなんて事は静的型付け言語であれば絶対にありえません。

ではこれらの問題をテストで防ごう!となるかもしれません。しかしながら動的型付け言語におけるテストというのは、どれだけテストケースを増やしカバレッジを上げたところで、本質的には全く網羅的ではなく、本当にごくごく一部の事しか保証してくれません。それは何故でしょうか?動的型付けにおけるテスト、例えば関数のテストの場合「渡される引数の型の可能性が無限に存在し、そしてそのごく一部のケースが正しく動作する事」を保証しているに過ぎないからです。テストは正常系にせよ異常系にせよ「想定している状況で、想定した入力を与えた時に、期待した答えが返ってくる」事を保証するものですが、動的型付け言語は「想定していない状況、あるいは想定していない入力」が与えられる可能性を何1つ排除できません。何も保証がないわけです。先ほども述べた通り、実質全てが any や void* に相当しますからね。なのでどれだけ頑張ってテストを書いたところで、それは何も保証がない上で、ごく一部の想定されうるパターンが問題ないかを確認しているに過ぎないのです。非常に苦しい。

もちろん引数で受け取った値やオブジェクトなどの型を厳密にチェックすればできなくもないですが (いわゆるショットガンパーシングと呼ばれるアンチパターン)、そんな事するなら最初から静的型付けを使ってコンパイラに頼ったほうが完全で網羅的で、かつ安上がりでもあります。静的型付け言語は多くの事柄をコンパイル時に保証してくれます。

最近だと AI にコード書かせることが流行っていますが、そもそも AI に書かせたコードをどのように信頼するのでしょうか?それは「型」と「テスト」です。「テスト」のみでは片手落ちも良いところです。前述の通り、動的型付け言語におけるテストでは「何も保証できない中で、本当にごく一部の事が保証する仕組み」に過ぎないのですから。

GUI のテストはまた事情が別ですけどね!

シリアライズ面

シリアライズ/デシリアライズはサービスを構築する上で避けては通れません。 シリアライズ/デシリアライズ面でも静的型付け言語である事は多くのメリットが存在します。

たとえば以下のような JSON があったとしましょう。

{
  "id": "ae5ca30d-8f05-480b-97d2-9cae8ed455aa",
  "blogUri": "https://blog.neno.dev",
  "dateTime": "2025-02-08T10:15:45.2658564Z",
  "intValue": 99
}

JSON の primitive type は string, number, boolean, null しかないので、当然ながら上記の JSON の id, blogUrl, dateTime のメンバの値の型は string です。ですが実際のところ、string の値は上記のように UUID, URI, DateTime などの値が期待される事が非常に多いです。しかしデシリアライズされた段階で単なる string だと非常に不便です。また intValue は int である事が期待されますが、JSON の primitive 型には number 型しか存在しないため、デシリアライズされたタイミングで number だと不便です。int なんだか float なんだがハッキリしてくれ、という。

さて、C# ではどのようにデシリアライズするでしょうか? まず C# の型は以下のように定義します。 ここで string 型のプロパティは存在せず、Guid, Uri, DateTimeOffsetなどの何を表現しているか一目瞭然な型のみが使われている事に注目してください。

class C
{
    // Guid は UUID 相当の型
    public required Guid Id { get; init; }

    [JsonPropertyName("blogUri")]
    public required Uri Uri { get; init; }

    public required DateTimeOffset DateTime { get; init; }

    public required int IntValue { get; init; }
}

そしてこの型を使い、先ほどの JSON を次のようにデシリアライズできます。

var json = """
    {
      "id": "ae5ca30d-8f05-480b-97d2-9cae8ed455aa",
      "blogUri": "https://blog.neno.dev",
      "dateTime": "2025-02-08T10:15:45.2658564Z",
      "intValue": 99
    }
    """;

//  C# の標準ライブラリには JsonSerializer が存在する
var c = JsonSerializer.Deserialize<C>(json, JsonSerializerOptions.Web);

動的型付けだとデシリアライズしただけでは JSON の primitive type である string, number, boolean, null のいずれかにマッピングされるだけですが、C# ではデシリアライズしたタイミングで定義した型に従い、string 以外の適切な型 (Guid, Uri, DateTimeOffset 等々) にマッピングされます。嬉しい。

逆に JSON が期待した形式ではない場合は、デシリアライズに失敗します。そのため、デシリアライズが成功した時点でデシリアライズされたオブジェクトは期待したフォーマットである事が保証されます。嬉しい。

var json = """
    {
      "id": "hoge",
      "blogUri": "huga",
      "dateTime": "piyo",
      "intValue": 3.14
    }
    """;

// class C と型が合わないのでデシリアライズに失敗する。
// 今回の例は全ての JSON のメンバと C# の型が一致しないが、
// JSON のメンバのうちの一つのメンバでも C# の型と一致しなかったら失敗する。
var c = JsonSerializer.Deserialize<C>(json, JsonSerializerOptions.Web);

また、JSON をシリアライズ/デシリアライズする際、JSON のメンバ名と C# のプロパティ名を一致させたくない時がぼちぼちあります。このような場合は [JsonPropertyName("blogUri")] のような形で attribute をプロパティにアノテーションしてあげればシリアライズ/デシリアライズする際には別の名前を与える事ができます。

また C# のオブジェクトにする際、このプロパティは必須だ、というものが存在するでしょう。そのような場合は required キーワードを用います。例えば以下のように class C のオブジェクトを new できるのですが、これにより required キーワードを付けたプロパティをオブジェクトに初期化時にそのプロパティも初期化する事が義務付けられます。

var c = new C
{
    // required keyword が定義に含まれているので、
    // どれか一つでも new する際に不足しているとコンパイルエラー
    Id = Guid.NewGuid(),
    Uri = new Uri("https://blog.neno.dev"),
    DateTime = DateTimeOffset.UtcNow,
    IntValue = 99
};

この required keyword はデシリアライズ面でも嬉しい事があります。それは、required keyword が付いているプロパティが JSON に存在する事を保証してくれます。たとえば、以下のように required keyword が付いてる C# のプロパティに対応する JSON のメンバが存在しない場合、デシリアライズに失敗します。

var json = """
    {
      "id": "ae5ca30d-8f05-480b-97d2-9cae8ed455aa"
    }
    """;

// required keyword が定義に含まれているプロパティが JSON のメンバに存在しないので、
// デシリアライズに失敗する。
// required keyword が存在しなければ、デシリアライズは成功する。
var c2 = JsonSerializer.Deserialize<C>(json, JsonSerializerOptions.Web);

このように、シリアライズ/デシリアライズ面で C# が静的型付けである事にはメリットが生じます。 今回の例では、Guid, Uri, DateTimeOffset, int などは標準ライブラリに含まれる型や primitive 型ですが、勿論ユーザ定義型でも同様にデシリアライズ時に適切でなければ弾くように出来たりもします。たとえば PostCode のような郵便番号を表現する型を定義し、JSON のメンバの型が string で郵便番号のフォーマットに従った string ではない場合はデシリアライズで失敗するようにする、等です。

フロントエンド界隈だと最近 Zod 等のライブラリが流行っており、このようなライブラリだと自らスキーマ定義のオブジェクトを作成しデシリアライズ後のオブジェクトをパースする...などを行う必要がありますが、C# であれば型を定義し、素朴にデシリアライズする事で型を保証できます。 なお Zod を否定しているわけではありません。むしろフロントエンドにおいて Zod + Form 系ライブラリのインテグレーションはかなり便利だと感じています。ただバックエンドでわざわざ Zod 等を使うなら最初から静的型付け言語で書き、そのメリットを享受した方がよいのではないでしょうか。型は別にシリアライズ/デシリアライズ面だけでなく、他のさまざまな面で役立ちますしね。

ちなみに。記述量が多いわ!と思ったそこのあなた。 後述の record を使えば記述量を減らし、より簡単に書くことが出来ます。

var json = """
    {
      "id": "ae5ca30d-8f05-480b-97d2-9cae8ed455aa",
      "blogUri": "https://blog.neno.dev",
      "dateTime": "2025-02-08T10:15:45.2658564Z",
      "intValue": 99
    }
    """;

_ = JsonSerializer.Deserialize<C>(json, JsonSerializerOptions.Web);

var c = new C(
    Guid.NewGuid(),
    new Uri("https://blog.neno.dev"),
    DateTimeOffset.UtcNow,
    99
);

_ = JsonSerializer.Serialize(c, JsonSerializerOptions.Web);

// class ではなく record というキーワードが使われている事に注目
// record については後述
record C(
    Guid Id,
    [property:JsonPropertyName("blogUri")]
    Uri Uri,
    DateTimeOffset DateTime,
    int IntValue
);

値型 (struct) と参照型 (class)

C# は値型 (struct) と参照型 (class) を明確に区別します。C++er なんかは一番気になるポイントだったりするかもしれません。C# ではユーザ定義型も値型として定義可能です。これはパフォーマンスを出す上で極めて重要です。参照型は必ずヒープにオブジェクトを作成するため、GC で回収されるゴミが発生してしまうからですね。現代の C# の GC は極めて高速ですが、GC に余計な仕事させない方が早いのは明らか。勿論、値型は別の変数に代入されるとコピーが発生するので、巨大な値型を極力定義しないとか、巨大な値型をパラメータとして渡すときにコピーが発生しないように ref keyword を用いて参照を渡すようにしたり、boxing を起こさないようにするなど、実装上気を付ける必要はそこそこあります。が、そのあたりはパフォーマンスを出すために必要な事です。問答無用で全てのオブジェクトがヒープに確保され、無駄に GC に回収されるゴミが発生してしまう事の方が辛い。辛いのです。

C# 以外の言語、例えば C# と似た言語としてよく挙げられる Java では (書き味からして全然違うんですけどね...!)、primitive type しか値型ではありません。 ユーザ定義の値型を定義する事はできません。辛い。加えて Java の generics は object (参照型) としてしか扱わないため (type erasure によって実行時型情報も取れない!)、primitive type だろうと何だろうと問答無用で必ず boxing が発生してしまいます。この辺りは JVM の JIT 時最適化でパフォーマンス劣化しないように頑張っている模様ですが、辛いものは辛い。

動的型付け言語は言うまでもなく、ユーザ定義型は全て参照型として扱われるため、不要なゴミがそれなりに発生し GC に負担をかける事になってしまい、パフォーマンスに悪影響を及ぼします。辛すぎますね。

ref struct

さらに現代の C# では、ref struct という、絶対にスタックにのみ存在する事しかできない (=どんな形であろうと絶対にヒープに持ち出せない) 型が存在します。 スタックにのみ存在する事が許され、ヒープに存在する事が許されないという事は、GC で回収されるゴミにならないという事です。これらを駆使する事で、パフォーマンスを向上させる事ができます。現代の C# のパフォーマンスが優れているのは runtime, web フレームワークともに ref struct が駆使されている事が1つの要因だったりします。さらに、コレクションも要素数が小さければヒープではなくスタックに確保可能なのでゴミが出ません。

record class/struct

2000年代はオブジェクト指向全盛でした。しかし今現在はどうでしょうか?特にバックエンド。

事前に予防線貼っておくと、オブジェクト指向は全く悪いものではありません。フレームワークやライブラリなんかの実装には今もなお変わらず強力な考え方です。オブジェクト指向には幾つかの側面がありますが、最も重要なのは「内部の状態を隠蔽して振る舞いを公開する」ことにあるでしょう (これをカプセル化というわけです。次点で大事なのは多態性と呼ばれるものでしょう)。しかしながら現代のアプリケーション、特にバックエンドにおいては「JSON などのデータをどこからか受け取り、それをあれこれ処理して、 DB に書き込んだり、他のサービスにシリアライズして投げたり」して一連のトランザクションを完結させるわけです。なので「内部の状態を隠蔽して振る舞いを公開する」という考え方をするよりも、「データ (メッセージ) とロジック (サービス) でそれぞれコードを分離し、データは隠蔽せずに素朴に表現する」方が何かと理解しやすいのです。とまぁこれが現在流行りのデータ指向プログラミングと呼ばれるものの正体なわけです。

C# もこのデータ指向プログラミングの流れにのっています。というわけで C# にはデータ型を表現するために record という言語機能が存在します。使い方は以下の通り。

// class keyword ではなく record keyword を用いる。
// MyData1 は参照型。
// 以下のような記法を primary constructor と呼び、
// MyData1 は Value というプロパティを持つ。
record MyData1(int Value);

// primary constructor を使わず以下のように
// 直接プロパティを定義しても OK。
// MyData1 とはオブジェクトの初期化の書きっぷりが異なるが
// それ以外の挙動はほぼ同じ。
record MyData2
{
    public required int Value { get; init; }
}

// class を付ける事も可能。
// 単に record と書くのと意味は同じ。
record class MyData3(int Value);

// 参照型ではなく値型がよければ record struct と書く。
record struct MyData4(int Value);

// struct の場合は readonly 修飾が可能。
// struct の場合、基本的には readonly 修飾はつけておく事を推奨。
readonly record struct MyData5(int Value);

record が嬉しいのはそのインスタンスがデータとして扱われるようになる事です。 これはどういう事かというと、普通の class で比較をした場合、特に何もしなければ (例えば== 演算子のオーバーロードするなどしなければ) 参照比較になる一方で、record の場合はデータとして等しいか、つまり内部に抱えているデータが全て一致しているか、という形で比較されるようになります。

var c1 = new C(99);
var c2 = new C(99);

// 参照比較
Console.WriteLine(c1 == c2); // false

var r1 = new R(99);
var r2 = new R(99);

// 内部のデータ
Console.WriteLine(r1 == r2); // true

class C
{
    public int Value { get; } = value;

    public C(int value)
    {
        Value = value;
    }
}

record R(int Value);

勿論、record を使わずとも record と同様の挙動を通常の class で実現する事は当然できるのですが (実際 record が登場する C# 8.0 以前はそのための実装をいろいろ手書きしていました)、結構手間がかかります。record を使う事でそのあたりの手間から解放されたわけです。そして、record を使う事で実装的にもデータとして扱えるだけでなく、record と class を使い分ける事で、意味的にもデータとロジックの型を分離して表現する事ができるようになりました。現代のデータ指向プログラミングが最適なバックエンドのビジネスロジックの実装的には非常にうれしいわけです。また、record を使う事で辞書のキーなんかも容易に作成できるので素朴な実装面においても非常に快適です。

勘違いして欲しくないのでもう一度いいますが、別にオブジェクト指向は全く悪いものではありません。もうちょっというと、データ指向プログラミングはオブジェクト指向プログラミングに完全に置き換わるようなものではありません。それぞれ良し悪し考えて使い分けると良いでしょう。結局自分の頭で考えて、状況に応じて not for me と考えたら突っぱねればいいし、突っぱねる他ないのです。オブジェクト指向も教科書的に正しいかどうかみたいな事ばかり考え、教科書的に正しくあろうと頑張って実装しようとしている人が苦しんでいる印象があります。自分の頭でいろいろ考えましょう (AI にこのあたり任せても良い事ないと思います)。

Null 安全

C# は (ほぼ) null 安全な言語です。

え、そうなの?と思う方もいるかもしれません。実際 C# はもともと null 安全な言語ではなかったのですが、C# 8.0 の時点で nullable reference type という機能が導入され、null 安全な言語になりました。ちなみに記事公開時点 (2025-04) の最新は C# 13 です。かれこれ null 安全になってから 6 年以上経っています。

簡単には、以下のような形で「?」 を付けないと null を代入する事は許容されません。

// コンパイラに怒られない (? がついているため)
string? hoge = null;

// コンパイラに怒られる (? がついていないため)
string piyo = null;

なお C# の null 安全はフロー解析ベースのものになっています。この辺りは同じ Microsoft が開発している TypeScript もフロー解析によってさまざまな言語機能を実現しており、null 安全もフロー解析によって担保しています。C# にはこのあたりの TypeScript 側の技術が輸入された形ですね。

さて、(ほぼ) null 安全と書いたのはフロー解析ベースだからですね。TypeScript も同様ですが、フロー解析はいろいろな形で握りつぶしたりコンパイラをちょろまかす事はできるので、厳密に null 安全といっていいのか?というとどうなんやろうね、という感じなので一応 「(ほぼ)」 という修飾 (?) を付けています。これは経験ベースになってしまいますが、nullable reference type が導入されて以降ぬるぽは見ていないので、null 安全と言い切っても差し支えない気がします。

非同期

並列並行問わず、非同期との闘いは全ソフトウェアエンジニアが直面する問題です。そして C# は昔から非同期との闘いと真剣に向き合ってきた言語であり、実は過去を振り返ってみると常にあらゆる言語の中で先端を走っている言語だったりします。

ひと昔前一世を風靡し、あらゆる言語に波及した Reactive Extensions (Rx) の元祖は C# だったりします。そして現代では当たり前となりさまざまな言語に実装されている async/await もやはり元祖は C# だったりします。C# に async/await が入ったのは C# 5、時期的には 2012 年と今となっては結構昔の話なのですが、だからと言って古びているわけではなく、現代でも async/await に関する改善は熱心に行われており、改善され続けパフォーマンスは向上の一途をたどっています。過去すごかっただけでなく、現在もすごいですし、現在進行形でさまざまな形で改善が行われています。

もちろん単に書き心地がいいだけでなく、使い勝手も良いです。現代のマシンは基本的にマルチコア・マルチスレッドで動作しますから、並列で実行して CPU を酷使したくなります。そんな時は Task.Run() を用いて ThreadPool に job をディスパッチする事ができます。たったこれだけで CPU を酷使できます。そして CPU を酷使して得られた計算結果などをさくっと await で待機し、それらの計算結果を取得できます。便利。

var cts = new CancellationTokenSource();

// 実際には計算が重たい処理を Task.Run に投げる
var t1 = Task.Run(() => 1 + 2, cts.Token);
var t2 = Task.Run(() => 3 + 4, cts.Token);

// 別の thread に投げた job が完了するのを簡単に待機可能
var results = await Task.WhenAll(t1, t2);

非同期 IO についても効率的です。C# では非同期 IO を行った際、非同期 IO が生じたスレッドは即座に開放され、そのスレッドは他のスケジューリングされている計算処理が走ります。そのためスレッドが無駄になる事はありません。そして非同期 IO が完了した場合、継続の処理 (await 以降の処理) がスケジューリングされ、どこかのスレッドで実行される事になります。C# の非同期 IO について詳しく知りたい方はこちらの記事を参照してください。

また非同期を行う上で、キャンセルの仕組みは必要不可欠です。C# では非同期処理を行う API には統一的にキャンセルする仕組みとして CancellationToken という型が存在します。非同期処理を行う API にはすべて CancellationToken というキャンセルするためのトークンを投げられるようになっています。上記の例で登場しているCancellationTokenSource は CancellationToken を作るための代物で、cts.Token の型が CancellationToken となっています。なお C# の web framework であるところの ASP.NET Core では TCP のコネクションが切れたり、HTTP/2 のリクエストであれば RST_FRAME フレームが飛んで来たらキャンセルが発火する CancellationToken をフレームワーク側が提供してくれます。また HttpClient サイドであれば、CancellationToken が発火したら RST_FRAME フレームがサーバに飛んでいきます。このような形で CancellationToken を使ったキャンセルの仕組みが整っています。

CancellationTokenを渡すお作法は C# の標準ライブラリに限らず、すべての API が従うお作法となっており、市中に出回っている OSS 等のライブラリでも非同期 API であれば必ず CancellationToken を渡せるようになっています。エコシステム全体で統一されており、キャンセルの仕組みをうだうだ考える必要がありません。非常に明快で良いことですね。もし市中に出回っているライブラリの非同期の API が CancellationToken を渡せない場合、殆どの場合実装者の腕が足りていないという事なので採用を見送って OK です (ごくまれに CancellationToken を渡せない事が技術的に正当性が認められるものも存在しますが、そういったライブラリは少数派です)。

JavaScript などでは近年ようやく AbortController などの API が登場しましたが、C# でははるか昔からキャンセルの統一された仕組みが存在しているわけですね (ちなみにAbortController は C# の CancellationToken をかなり参考にしている模様)。

また C# の runtime が標準で備える ThreadPool 及び非同期ランタイムは非常に洗練されており、使いやすく、効率的です。Rust なんかは非同期ランタイムが標準で用意されておらず、公式ではない OSS を利用する必要があったりします (Rust が組み込みなどで使われる事を想定している事を考えると、このような仕組みになっている意図は理解できますが...)。標準で優れたものが非同期ランタイムが内包されているのは嬉しい。もちろん C# でもカスタムの非同期ランタイムを導入する事は可能ですが、まぁ殆どの場合不要でしょう。

また C# で async/await を使う事は安全です。逆に Python などでも async/await を使う事は可能ですが、割と危険だったりします。具体的には async/await で統一しないといけないところを同期 API を使ってしまうと、ある論理制御フローで用いられていたインスタンスが他の論理制御フローで参照され、制御が狂ってしまう事があります (以前著名なライブラリを使っている際、開発時に軽く負荷をかけたらこの事例を踏んで中指を立てていました。ライブラリ内部で ContextVar 使っていればこんな事にならない気もするのですが...)。C# の場合もし同期 API が用いられたとしても CPU 効率が悪くなるだけで、他の論理制御フローで実行しているものが漏れ出す事はありません (C# でも ThreadLocal を使う事でこのような問題を発生させる事ができますが、現代において ThreadLocal を使う事はよっぼどの事でもないとなく、殆どの論理制御フロー上から漏れない AsyncLocal を用いるので発生しないと言って良いでしょう)。

C# の async/await やマルチスレッド、非同期 IO については技術的に詳細かつ deep に解説をしている記事を別途書いているので、興味がある方はぜひ覗いてみてください。

blog.neno.dev blog.neno.dev blog.neno.dev

拡張メソッド

C# には拡張メソッドという文法が存在します。C#er は皆大好きな文法です。これはどのような文法かというと、任意の型に任意のメソッドを生やすことができる文法です。

例えば、以下のようなコードを書くことで、本来 int には存在しないメソッドを叩くようにする事ができます。

var value = 99;

// IntExtensions.AddSeven(value) と等価
var value2 = value.AddSeven();

Console.WriteLine(value2); // 106

// 拡張メソッドを定義する class は static class である必要がある。
// class 名はなんでもよいが、XxxExtensions とつけるのが慣例。
internal static class IntExtensions
{
    // static method である必要がある
    // 引数の先頭に this をつけると拡張メソッドになる
    public static int AddSeven(this int value)
    {
        return value + 7;
    }
}

拡張メソッドによって、f(obj) ではなく obj.f() と書けるようになるわけです。これは書きっぷり的に嬉しいだけではありません。.(ドット) を打つことで IDE や editor が補完候補を出してくれるわけです。ちなみに現代の C# は拡張メソッドがめちゃくちゃ多様されています。

この拡張メソッドはさまざまな点でメリットが大きいです。

先ほどお見せした通り、データ型には record を使って型を定義するわけですが、あまりデータ型に対してあれこれメソッドを追加したくありません。こういう場合は拡張メソッドの出番です。

var character = new Character
{
    Name = "Miorine Rembran",
    StudentNumber = "LS001",
};

var marriedCharacter = character.GetMarried();

// record を使っておくとオブジェクトを文字列化 (.ToString()) した時にいい感じになる
Console.WriteLine(character); // Character { Name = Miorine Rembran, StudentNumber = LS001 }
Console.WriteLine(marriedCharacter); // Character { Name = Miorine Mercury, StudentNumber = LS001 }

// Character はイミュータブル
public record Character
{
    public required string Name { get; init; }
    public required string StudentNumber { get; init; }
}

public static class CharacterExtensions
{
    public static Character GetMarried(this Character source)
    {
        // with を使うことでプロパティの一部を書き換えらたインスタンスが作られる
        return source with
        {
            Name = $"{source.Name.Split(' ')[0]} Mercury"
        };
        // 上記の with を使ったコードは以下のコードと等価
        // with を使うと新しいインスタンスが作られる
        // return new Character
        // {
        //     Name = $"{source.Name.Split(' ')[0]} Hosimi",
        //     StudentNumber = source.StudentNumber
        // };
    }
}

書き心地的な観点でも、基本的に f(obj) ではなく obj.f() と書きたいわけです。しかしながら、責務的に record そのものに対してメソッドを追加するのは好ましくない、というような事は非常に多いわけです。そのような場合に拡張メソッドは極めて有効な手段となります。

この拡張メソッド、実はチーム開発においてもかなり嬉しいものになっています。開発をしているとさまざまなメソッドを定義するわけですが、チームで開発していると似たようなものが別の場所に実装されてしまっている、なんて事はよくある事なのではないでしょうか?チーム開発している際には自分たちのコードベースを全て把握する事は現実的ではありませんし、設計とかちゃんとしていても生じてしまうものは生じてしまうものです。しかし拡張メソッドでメソッドを定義しておけば、obj のあとに .(ドット) を押せば使えるメソッドの一覧が IDE 上に表示されますから、何が使えるかな?とさくっと眺めて既存の実装で似たようなものがないかを確認し、実装に入る事ができます。ある種ドキュメントのような役割を持っているといっても良いでしょう。また .(ドット) を押すことによって補完がばっちり効きますから、タイピングする必要がありません!やったね。

ちなみにこの拡張メソッド、後述する C#er はみんな大好きな LINQ という機能でめちゃくちゃ多用されています。拡張メソッドを制するものが C# を制するといってもよいでしょう。

Source Generator

C# にはコンパイル時にユーザが記述したコードを構文解析・意味解析し、それをもとにライブラリ等がコードを生成する機能が存在します。それが Source Generator です。簡単には以下の図がわかりやすいでしょう。

Introducing C# Source Generators から引用

Source Generator はコンパイル時にユーザが記述したコードの特定の部分を見つけてきて、そこから必要なコードを生成できます。特定の部分というのは、attribute が追加されている class や method であったり、特定の method が呼び出されている箇所だったりします。構文解析、意味解析した結果から任意の箇所を見つけてくる事ができるので、だいぶやりたい放題できます。現実的には、大半の Source Generator は特定の attribute が付与されているコードを探して、それを起点に何かしらのコードを生成します。

以下の例は現実的にはまるで意味がないコードですが、こんなコードが書けます。

public partial class Hoge
{
    // メソッドを定義し、attribute ([Hoge]) を付与する。
    // 人間はメソッドの定義のみ行い、実装は行わない。
    // 実装は Source Generator がやってくれる
    [Hoge("なんか適当なメッセージ")]
    public partial void M(int value);
}

// C# の Attribute は単なるメタデータを付与するもの。
// つまり TypeScript や Python のデコレータとは全くの別もの。
// Attribute 自体はなにもしない単なるメタデータなので、
// 概ね実行時にリフレクションで取得されて挙動に影響を与えたり
// (例えばシリアライズの項で利用した [JsonPropertyName] のように)、
// コンパイル時に Analyzer (linter) や Source Generator が見つけて
// 警告出したり、コード生成したりするのが主な利用のされ方。
// また [AttributeUsage] でどこにその attribute を付与できるか指定できる。
[AttributeUsage(AttributeTargets.Method)]
public class HogeAttribute : Attribute
{
    public string Message { get; }

    public HogeAttribute(string message)
    {
        this.Message = message;
    }
}

例えば上記のような書くと、Source Generator がコンパイル時に attributeが付与されているコード (上の例でいうと [Hoge("なんか適当なメッセージ")]が付与されているメソッド) を探してきて以下のようなコードが吐き出されます (吐き出されるといっても、ビルド時に内部でおきる出来事であり、git 管理されるような領域に吐き出されるわけではありません。吐き出すようにするオプションは存在しますが)。また Source Generator は Source Generator でライブラリが提供しているものでなければ、自前で実装する必要があります。

public partial class Hoge
{
    // Source Generator がコンパイル時に Attribute ([Hoge]) が付与されているコードを探してきて
    // Source Generator がコンパイル時に以下のようにメソッドが実装される。
    public partial void M(int value)
    {
        Console.WriteLine($"なんか適当なメッセージ {value}");
    }
}

上記の例が実用レベルになると、コンパイル時ログコード生成などのテクニックが存在します。これによってコンパイル時にそのメッセージを出力するための専用の実装を出力し、効率的なログ出力が可能になっています。

public static partial class HogeLogger
{
    // [LoggerMessage] は logging パッケージが提供してくれている attribute。
    // attribute に渡す文字列のプレースホルダ内の文字列 ({PiyoId}) と 引数名 (piyoId)を一致させる事で、
    // 構造化ログをいい感じにやってくれる。
    // 効率的に logging するためのコードを Source Generator が
    // 以下の LogFuga というメソッドを実装する形で生成してくれる。
    [LoggerMessage(1, LogLevel.Information, "LogMessage {PoyoId} {PiyoValue}")]
    public static partial void LogFuga(ILogger logger, Guid piyoId,  int piyoValue);
}

Logger の例は本当に一部です。Source Generator はユーザが記述したコードや文字列に合わせて何かをコードを生成したい場合に威力を発揮します。ようするに動的コード生成したくなるような部分です。[LoggerMessage] は attribute に渡された引数とそこに含まれるユーザが記述した文字列に合わせて最適なコードを出力しているわけです。

もうすこし実例を見てみましょう。たとえば私が公開している OSS である TypedSignalR.Client の例で見ていきましょう。世の中には SignalR というリアルタイム通信用のライブラリが存在するのですが、素の SignalR の client が強く型付けされていません。つらい。そこで TypedSignalR.Client は SignalR のクライアントをユーザが定義した interface に従って強く型付けする機能を提供します。型は正義。

たとえばユーザが以下のような interface を定義したとします。

public interface IMyHub
{
    Task Add(int x, int y);
}

ライブラリユーザは以下のように CreateHubProxy<>() を叩く事により、強く型付けされた SignalR の client を取得し、利用する事ができます。

// HubConnection は SignalR のコネクションを表現する型
HubConnection hubConnection = ...;
CancellationToken cancellationToken = ...;

var hubProxy = hubConnection.CreateHubProxy<IMyHub>(cancellationToken);

var result2 = await hubProxy.Add(1, 2);

ちなみに CreateHubProxy<>() も拡張メソッドです...! なのでCreateHubProxy<IMyHub>(hubConnection, cancellationToken) ではなくhubConnection.CreateHubProxy<IMyHub>(cancellationToken) と書けるわけですね。

で。この場合 Source Generator は何をするかというと、CreateHubProxy<>() が用いられているところをコンパイル時に探し出し、型引数に渡されている型 (今回の場合は IMyHub) を解析し、以下のようなコードを生成します。これによって、ユーザは快適な書き心地を得られます。

// 実際の TypedSignalR.Client はもっと色々な事を考慮したコードが吐き出されますが
// 簡略化するとこんな感じ。
internal sealed class GenereatedMyHubProxy : IMyHub
{

    private readonly HubConnection _connection;
    private readonly CancellationToken _cancellationToken;

    public GenereatedHogeHub(HubConnection connection, CancellationToken cancellationToken)
    {
        _connection = connection;
        _cancellationToken = cancellationToken;
    }

    public Task<int> Add(int x, int y)
    {
        return _connection.InvokeCoreAsync<int>(nameof(Add), [x, y], _cancellationToken);
    }
}

このように Source Generator はユーザコードに合わせて適切なコードを吐き出したい場合に非常に有効です。標準ライブラリの中だと、とくにシリアライザや正規表現などで活用されています。実行時にユーザが定義した型の情報をみて動的コード生成したり、実行時にユーザが記述した正規表現を解析してそれにそったコードを動的にコード生成するより、コンパイル時に諸々生成してしまった方が実行時に高速ですし、なにより実行時に諸々解析して動的コード生成するよりも圧倒的にデバッグしやすいです。 出力された C# コードを読めばいいだけですからね。

標準ライブラリ

C# は標準ライブラリが分厚い言語です。

標準ライブラリが分厚い事はもちろんメリデメあります。これは言語の主な用途にかなりよります。 標準ライブラリが分厚い事にはさまざまなメリットがあります。

  • 不要な外部ライブラリを利用する必要がない (依存する必要がない)
  • 一度学習してしまえば多くの箇所で知識が使いまわせる
    • 一時の流行のライブラリの使い方を把握するよりも、遥かに価値があります...!
  • 自分たちのコードや OSS 等を読む際にも標準ライブラリが多用されるので、一貫性があり圧倒的に理解しやすい

デメリットとしては runtime が大きくなることでしょうか。とはいえ、.NET の runtime は 30 MB 程度なので、組み込みとかでも無ければ特に問題になるようなサイズでは無いでしょう。 一方で Rust のような組み込み等で使う事が強く想定されているような言語だと、標準ライブラリが分厚かったらそれはそれでノーサンキューという感じでしょう。

C# の標準ライブラリにはさまざまなものが存在します。

例えばプログラムを組むうえで欠かせないコレクションも非常に多く用意されています。基本的なコレクションである List<T>, Dictionary<TKey, TValue>, HashSet<T>, Stack<T> , Queue<T>, PriorityQueue<TElement,TPriority>や、イミュータブルなコレクションである ImmutableArray<T>, ImmutableDictionary<TKey,TValue>, FrozenDictionary<TKey,TValue>, マルチスレッドプログラミングする際に必須はスレッドセーフな ConcurrentDictionary<TKey,TValue>, ConcurrentQueue<T>,ConcurrentStack<T> などが存在します。用途に応じて適切なコレクションを選択する事で、効率のいいプログラムを組むことができます。そして当然ながら標準ライブラリに含まれているということは、このようなコレクションを都度自前で実装したり、外部のライブラリを選定して利用する必要がありません。標準ライブラリにあるライブラリが物足りない場合、要するにユースケース特化の最速のコレクションが欲しくなったりするかもしれませんが、それはごくごく稀なお話であり、殆ど標準ライブラリのコレクションを使っていれば事足りるでしょう。

またプログラミングを組む上でほぼ必須である重要な型として Guid (UUID を表現する型), DateTime, DateTimeOffset, TimeSpan, Uri などが存在します。このような型が標準ライブラリに存在するため UUID や DateTime や URI を単なる文字列でやりとりしたりするといったナンセンスな行為は発生しませし (でも世の中いっぱいある!)、外部のライブラリに依存する必要がないため、エコシステム全体で統一された形で強く型付けされています。非常に嬉しい。

また何処かと通信しようと思ったらほぼほぼ必須な HttpClient や JsonSerializer も標準ライブラリに存在します。

そして非同期プログラミングをする上で重要な Task/Task<T> (Promise 相当), それらを裏で支える ThreadPool や, スレッド間で安全かつ効率的に情報のやり取りするための Channel なども標準ライブラリに存在します。

まぁ本当にここにあげているのはごく一部でしかないのですが、とにかく色々なものが用意されています。 しかし C# の標準ライブラリを語る上で絶対に語らないといけないのが、なんといっても LINQ です。

LINQ (Language Integrated Query)

プログラムを組んでいると、コレクションをガシガシ操作する事は非常に多いでしょう。 その場合に有効なのが LINQ です。

R[] array = [
    new R(1),
    new R(2),
    new R(3),
    new R(4)
];

var items = array
    .Where(x => x.Value % 2 == 0)
    .Select(x => new R(x.Value * 2))
    .OrderByDescending(x => x.Value);

foreach (var it in items)
{
    Console.WriteLine(it);
}

public record R(int Value);

// 出力
// R { Value = 8 }
// R { Value = 4 }

Where() とか Select() が LINQ のオペレータになります。 LINQ のオペレータはかなりの種類があるので、これらを駆使する事でコレクションを明快かつ自由自在に操作できます。

で、単にこれだけだと「他の言語にも map やら filter やら似たようなのあるじゃん!」になってしまうので、LINQ の真価を語っていきたいと思います。

まず第一に、LINQ は遅延評価されます!どういう事かというと、単に Where() や Select() 等のオペレータを用いてメソッドチェーンしただけでは、評価が走りません。そのため不要なメモリアロケーションが発生しません。逆に JavaScript の配列ではmap や filter は遅延実行ではなく即時評価されます。そのため取り扱う配列が巨大な場合、例えば配列の要素が 10000 とかあった場合、array.map(...) したら再度要素数が 10000 の配列が確保されてしまいます。C# の LINQ は基本的に遅延評価されますから、このような事は発生しません。

var items = array // なんか巨大な int 配列
    .Where(x => x % 3 == 0)
    .Select(x => x * 2)
    .Take(20); // 先頭 20 個だけ引っ張ってくる

// 遅延評価なので 20 個分の要素を取得したら終了
// 即時評価の場合巨大はな配列分のアロケーションが発生してしまうが、
// C# の LINQ は遅延評価なので不要なメモリアロケーションが発生しない
foreach (var item in items)
{
    Console.WriteLine(item);
}

第二に、C# の LINQ はすべて IEnumerable<T> という interface に対する拡張メソッドとして定義されています。そして C# の標準ライブラリが提供しているコレクションにはすべて IEnumerable<T> が実装されているので、すべてのコレクションに対して LINQ を使う事ができます。またコレクションが struct で定義されているものに関しては、interface に対する拡張メソッド経由で叩くと boxing が発生してパフォーマンスが低下してしまうので、LINQ に対する一発目のメソッドはすべてその struct に対する拡張メソッドとして定義されており、パフォーマンスの事も十分に考えられています。

たとえば Select() は以下のように定義されています。

// 標準ライブラリはこんな素朴な実装ではなく、しっかりと最適化されています
// 実際の実装は GitHub 参照の事
// https://github.com/dotnet/runtime/blob/v9.0.2/src/libraries/System.Linq/src/System/Linq/Select.cs#L13
public static class Extensions
{
    public static IEnumerable<TResult> Select<TSource, TResult>(
        this IEnumerable<TSource> source,
        Func<TSource, TResult> selector)
    {
        foreach (var item in source)
        {
            yield return selector(item);
        }
    }
}

そして拡張メソッドで定義されているという事は、自前でいくらでも拡張できるということです。 たとえば以下のように Custom() というメソッドを定義すると、とても簡単に自作 LINQ オペレータを作成でき、LINQ のメソッドチェーンに挟み込む事ができます。

R[] array = [
    new R(1),
    new R(2),
    new R(3),
    new R(4)
];

var items = array
    .Where(x => x.Value % 2 == 0)
    .Select(x => new R(x.Value * 2))
    .Custom() // 自作オペレータを簡単に挟める
    .OrderByDescending(x => x.Value);

foreach (var it in items)
{
    Console.WriteLine(it);
}

// 出力
// R { Value = 108 }
// R { Value = 104 }

public record R(int Value);

public static class Extensions
{
    // 以下のように自作 LINQ オペレータを定義。
    // この例だと Select 使えよという話ではありますが、
    // 実際にはここで結構複雑な事を行い、
    // LINQ のメソッドチェーン本体をすっきりさせ、意味的にも分かりやすくする事ができます
    public static IEnumerable<R> Custom(this IEnumerable<R> source)
    {
        foreach (var item in source)
        {
            yield return new R(item.Value + 100);
        }
    }
}

LINQ は別にコレクション、集合に対するものだけではありません。標準ライブラリに組み込まれているコレクションに対する LINQ を LINQ to objects と呼称するのですが、他にも LINQ to SQL, LINQ to events などが存在します。LINQ to SQL は Entity Framework Core、LINQ to events は R3 などのライブラリを組み込む事になります。

これらの LINQ to xxx は基本的にすべて似た意味のオペレータは同一の名前が付けられていますから、だれが見ても何をしているか非常に理解しやすいものとなっています。もちろんそれぞれに固有なオペレータは存在しますけどね、例えば Rx なんかだと イベントのフィルタリングのために ThrottleFirst/ThrottleLast というオペレータがあったりしますが、これはイベントの処理固有であり集合に対する操作と対応するものは存在しませんからね。

OSS 等のライブラリ事情

現代のソフトウェア開発において OSS を利用する事は殆どの場合避けて通れないでしょう。

そんな現代のソフトウェア開発において、皆さんはどのように利用する OSS を選定しているでしょうか?利用する際どのような事に気を付けているでしょうか?

OSS とは基本的に AS IS です。そこにあるものがそのまま提供されているだけであり、そこにないならないですねの精神。ホビーでやっている個人開発や研究では雑に使ってみればよいと思います。が、プロダクションでの利用は当然事情が変わってきます。AS IS で提供されているものを無邪気に信じてプロダクションで利用するのは難しい。

というわけで、私は OSS を利用する際には以下のような事を考えています。

大規模 or 読み解く難易度が高い 読み解ける範疇
デファクトになっている or
広く使われている
- 開発元は信用できそうか
- サポート期間やライフサイクルは明示されているか
- あるいは心中を覚悟で利用するか
- コア部分は読んでから利用する
- 無邪気に利用する (困ったら後からでも読み解けばよいため)
広く使われていない - 開発しているのが会社の場合はサポートが得られるかどうか
- 個人の場合は...見送りかな...
- 一通り読んでから利用する
- 開発元は信用できそうか

開発元がある程度信用でき、デファクトスタンダードないし広く使われており、サポート期間とライフサイクルが明示されているものについては、純粋に機能や性能のみを考えて採用すればよいでしょう。それでもなお死んだらもうどうしょうもない。死なばもろとも。とはいえその手のやつが死ぬとみんな困るから死なないようにコミュニティがいろいろ動くでしょうし、サポート期間を明示するようになるくらい育った OSS はそうそう死なないでしょう (希望的観測)。

問題はそれ以外のパターン。

まず、大規模かつデファクトスタンダードないし広く使われているが、サポート期間とかないやつ。 というかむしろサポート期間とか明示されているの方が珍しいですからね。サポート期間やライフサイクルが明示されているの、各言語の SDK とか runtime, 或いは巨大なフレームワークとかぐらいなものでしょう。サポート期間などが明記されていないもう心中覚悟で使うか、社内にコミッター抱えるなり、死なないようにその OSS をスポンサーとして金を出しておくしかないでしょう。

次に大規模かつ広く使われていないパターン。これ大概は新進気鋭の新しい企業が爆誕させた OSS だったりするでしょう。これはその企業からサポートが得られるかどうかで採用の可否は変わってくるでしょう。そして大規模ではあるが個人が作ったものの場合は、余程の革新性が無ければ殆どスルーされるでしょう。なので個人開発の OSS はスリムがベスト (そうでないと使われない...!)。

次に読み解ける範疇でデファクトスタンダードないし広く使われているパターン。個人的にはコア部分は読んでから利用する方が良いとは思うし、実際自分なんかは割と読むようにしているのですが、まぁ無邪気に使っても良いかと思います。利用していて困ったとしても、読める範疇のコード量や難易度であれば後からでも読み解けばよいですからね。

最後に読み解ける範疇で広く使われていないパターン。これは基本的に一通り読んでから利用するべきでしょう。「hogehoge がやりたいんだけど GitHub にこんな OSS 転がってたからこれ使って実現します!」は3流のやること。アジャイルが「動くソフトウェア」を作る事をどれだけ推進してもこういう雑にパッケージを追加するムーブは到底受け入れられるものではありません。あとは開発元が信頼できるかどうかでも変わってきます。わりと OSS あるあるだと思うのですが「この人が作っているなら大丈夫でしょ」みたいなのはあるので、そういうアンテナを元に判断を下すのもありでしょう。まぁ「信頼できるでしょ!」の場合でも読める範疇なら読んでしまったほうがいいとは思うのですが (勉強にもなりますからね!)。まぁなので、個人で OSS を公開するパターンは殆どこのパターンだと思いますから、とにかく徹底的に読みやすいコードを書いておく事は非常に重要なのですね。いい感じの機能が提供されていても読みづらい OSS は絶対に採用されないと思ったほうがよいでしょう (個人的な所感です)。

で、これはあくまで OSS の選定事情であり、プログラミング言語事情にあまり関係ないのでは?と思う方もいるかもしれません。しかしながら、これは大きくかかわってきます。

要するに、圧倒的に信頼できるものや心中を覚悟する OSS を除けば、基本的に OSS とは読んでから利用するべきものだと私は考えています。これは理想論であり、現実的ではないと言及される気もしますが、理想的にはやはり開発チームの誰かしらは読んでおくべきだと考えています。 原則的に OSS が AS IS である事を忘れてはいけないのです。 そして AS IS という事はそんなに無邪気に信頼できないのです (OSS 使った開発するなら利用する OSS や利用しそうな OSS を読むコストも追加で載せるべきだと常々思っています)。

そして原則的に OSS を読んでから利用する、という立場にたったとき、C# は非常にうれしいポイントがあります。それは公開されている OSS のライブラリそれぞれが外部のパッケージに依存していない、あるいは依存していたとしても推移的依存含めてたいした数のパッケージに依存していない、ということです。要は C# では .NET runtime を一番下の基盤としたときに、以下のような状況にほとんどの場合陥らない、ということです。 TypeScript や Python は依存関係地獄が避けられず、苦しい思いをする事になるわけですが(本当に苦しい、助けてくれ)、C# ではそのような事態に陥りません。そのため読まないといけないライブラリが読みたい対象のライブラリにのみ閉じる事が多いです。

xkcd から引用

npm のように推移的依存によってパッケージが無限に追加されるともうどうしょうもありません。とてもじゃないけど把握しきれません。

もはや元ネタがどこか分からんミーム

なぜ C# の OSS が npm で公開されているようなパッケージ達のような残念な状態にならず、理想に近い状態になっているかというと概ね2つの理由があると私は考えています。

まず第一に C# の標準ライブラリが分厚いため、各種ライブラリは余計な外部パッケージに依存する必要がない事が多いから。標準ライブラリで何かが足りない場合は大概「標準ライブラリにあるものよりユースケースに特化した何某が欲しい!」なのでやはり外部のパッケージに依存せず自前実装する事になるので、余計な依存が発生しません。

そして第二に、おそらく C# が長らくエンタープライズな領域で使われる言語だったからだと考えられます。エンタープライズなソフトウェア開発だと推移的依存関係によって余計なパッケージが追加される事を毛嫌いすると思うので、このような文化が出来上がったのだと推測しています。

C# の OSS はそのような文化の上になりたっているので、非常に OSS を読むのが捗ります。ついでに標準ライブラリの知らかなった活用方法なんかも勉強できます。 一石二鳥。

個人的には他の言語のライブラリ、標準ライブラリだけで成立している事が本当に少ないので、とある OSS の実装を読もうとしても、その OSS が依存している別の OSS の実装を読まないといけないので、めちゃくちゃ読みにくいんですよね。多分こういう挙動するんだろうな、みたいな予測はついても。

プロジェクトという単位

C# ...というか .NET にはプロジェクトという単位が存在します。このプロジェクトというのは1つのパッケージないしアセンブリ (dll) の単位になります。このアセンブリはパッケージのこともあれば、実行可能なアプリケーションのこともあります。 このプロジェクトという単位がかなり嬉しい。

パッケージの場合

現代的な構成の場合、多くの場合パッケージは「抽象(インターフェース等)」と「実装」に分かれています。 言葉で説明しても分かりづらいと思うので、具体例を見てみましょう。

たとえば、現在 C# で logging をする際には Microsoft.Extensions.Logging というパッケージを用いるのがデファクトになっているのですが、以下のように最低限の抽象と実装でパッケージが分離されています。

  • Microsoft.Extensions.Logging.Abstractions
    • ILogger interface ã‚„ LogLevel を表現する enum などの最低限の型のみが含まれるパッケージ
  • Microsoft.Extensions.Logging.Console
    • コンソールにデバッグ用のシンプルなログや JSON 構造化ログを出力するためのパッケージ
    • Microsoft.Extensions.Logging.Abstractions に依存している
  • OpenTelemetry
    • OpenTelemetry のパッケージ。OpenTelemetry はログも出力するので、当然ログ関する実装も含まれている。
    • Microsoft.Extensions.Logging.Abstractions に依存している

このような具合です。その他さまざまなライブラリでログを出力するためのクチが用意されている場合でも Microsoft.Extensions.Logging.Abstractions にのみ依存しており、実際にどのような形式でどこにログを出力するかについてはライブラリユーザが別途ライブラリを追加する事でカスタマイズできるようになっています。

Clean Architecture

さて、多くの人が大好きな Clean Architecture。そして無限に誤解の生まれる Clean Architecture。是非も無限に問われる Clean Architecture。

C# における「プロジェクト」という単位は、Clean Architecture に沿ったアーキテクチャを組むのにも非常に向いています。

まず Clean Architecture の核心は「依存の方向を制御フローに引っ張られないように、アプリケーションのコア (entity, interface, ビジネスロジック等) は何にも依存しないようにしつつ、各コンポーネントをそのコアに向かって一方向に依存させるようにしよう」というだけの話です。 もう少しかみ砕くと「アプリケーションのコアが各コンポーネントに依存するのではなく、各コンポーネントがコアに対して依存するようにするようにしよう」という事であり、とどのつまり「制御フローとコンポーネントの依存関係をひっくり返すといい感じ!」という事です。 なので Clean Architecture とは所詮 SOLID 原則の D (Dependency Inversion Principle) の延長に過ぎないわけです。SOLID 原則大事。 Clean Architecture のやたらと分厚い本とか読まないでもこれだけ抑えておけば OKOK。 Bob おじがなんて言ってるか気になる方はこのページとか読んでおきさえすれば良いと思います。 あの同心円の図や UseCase とか Interactor 等の方法論は死ぬほどどうでもいいのです。

まぁともかく大事なのは制御フローとコンポーネントの依存関係をひっくり返すことです。そして C# のプロジェクトを使うことで、非常に明確なコンポーネントの分離と依存関係の制御が可能となっています。たとえば C# の Clean Architecture に沿ったアーキテクチャを組む場合、以下のような構成になります。

ディレクトリ的には以下のような感じで (一部省略しています)、.csproj が含まれるディレクトリ配下が1つのプロジェクトとして取り扱われます。そして .sln がそのプロジェクト達をまとめて管理している感じです。

Root
├─ MyApp.sln
└─src
    ├─ MyApp.Core
    │  ├─ MyApp.Core.csproj
    │  ├─ Entities
    │  │    └─ MyEntity.cs
    │  ├─ Repositories
    │  │    └─ IMyRepository.cs
    │  └─ Services
    │       └─ IMyService.cs
    │
    ├─ MyApp.Infrastructure.PostgreSql
    │  ├─ MyApp.Infrastructure.PostgreSql.csproj
    │  └─ Repositories
    │        └─ MyRepository.cs
    │
    └─ MyApp.WebApi
        ├─ MyApp.WebApi.csproj
        ├─ Program.cs
        └─ Controllers
             └─ MyController.cs

MyApp.Core は何に依存せず、entity 型や各種 interface などをが定義されています。 MyApp.Infrastructure.PostgreSql は MyApp.Core に依存しており、MyApp.Core で定義された entity を参照し、interface に必要な実装が行われています。 そして MyApp.WebApi は MyApp.Core および MyApp.Infrastructure.PostgreSql に依存しており、それらを組み合わせ DI(Dependency Injection) を構成しています。このように構成する事により、制御フローとコンポーネントの依存関係をひっくり返す事ができます。つまり、ビジネスロジック (=MyApp.Core) が DB に対する実装 (=MyApp.Infrastructure.PostgreSql) に依存するのではなく、DB に対する実装がビジネスロジック側に依存するようになっています。これが Clean Architecture の核心です。

プロジェクト単位で実装を分離し、依存関係を明示的にする事はさまざまなメリットが存在します。 たとえばMyApp.Core は MyApp.Infrastructure.PostgreSql に依存していませんから、当然MyApp.Infrastructure.PostgreSql で定義された諸々を参照することは絶対にできません。プロジェクトのような単位がないと、ひょんなことで参照の方向がおかしくなったりすることがあり、そして一度崩れるとずるずる変な方向に引っ張られてせっかく一方向にしていた依存がぐちゃぐちゃになる...。このようなことは絶対に「仕組み」によって起こり得なくなります。レビューによって防ぐとかしなくて済みます。またどう頑張っても正しく抽象を作らないとまともにソフトウェアを開発する事が出来なくなるので、必然的に設計者が意図した通りのアーキテクチャのまま開発が続くようになります。大変喜ばしい。

もちろんプロジェクトという単位は Clean Architecture だけにとって嬉しいものではありません。最近だとマイクロサービスに疲れてモジュラモノリスへの移行みたいな話がちらほら流れてきますが、モジュールに分けるという点でも C# のプロジェクトは非常に有用です。というか C# では昔からデプロイ自体はモノリスではあるが、プロジェクト (モジュール) は複数に切られているなんて当たり前の話だったので、C#er からすると少々今更感があったりするのですが...。(そもそもモジュラモノリスに回帰可能なら最初からマイクロサービスなんて不要なのである...!)

後方互換

C# はかれこれ25年選手ですが、言語仕様の破壊的変更はほぼありません。 自分が知る限りにおいては、殆どバグ扱いみたいな形で修正され実質的に破壊的変更となってしまったものが1つありますが、その程度です。

また C# の標準ライブラリも後方互換をものすごい考えられて作られており、殆ど破壊的変更がありません (ゼロではありません)。破壊的変更といっても実害のあるものは非常に限られていますし、殆どの人には影響しないものが大半です。基本的に .NET のバージョンあげたら壊れるのではなく、単にパフォーマンスが向上するだけです。

ちなみにインターネッツだと C# は後方互換性がない!とかいっている C# エアプ勢がぼちぼちいるのですが、これはおおむねフレームワークのバージョンをガツンとあげたりしているから起きることです。C# の言語そのものや標準ライブラリには圧倒的な後方互換があるわけですが、さすがにフレームワーク側が全ての機能について後方互換を持っているわけではないので、そちらで引っかかっているのでしょう。というか世の中に出回っているフレームワークで後方互換を100%維持している代物なんて存在しないといっていいですからね、こればっかりはしょうがない。またそういったフレームワーク都合で壊れる場合でも、その殆どが実行時エラーという形ではなく、コンパイル時に死んでくれるので半端に動いたりすることがないので嬉しい。

開発体験

さて、開発環境は開発速度および開発体験に直結するため非常に重要です。

C# で開発するにあたっては、Visual Studio か Rider (Jetbrains) を用いる事になります。 最近ではちょこちょこ Visual Studio Code 派も存在しますが (AI 絡みの機能を求めて VS Code 使っている人が増えているような気がします)、まぁ殆どの C#er が IDE を使っているといって良いような状態です。Visual Studio と Rider は双方ともに商用利用でなければ無料で利用できます。C# の開発環境は、基本的に IDE と .NET SDK さえインストールしてしまえば、とくに面倒な手順もなく開発環境が整います。デバッガなどを利用するための面倒な手順もせずすぐに使えるようになります。

ここで IDE 無料じゃないからいやだ!という声が聞こえてきますが、皆さんが湯水のように溶かしているクラウド費用や自分達の人件費を考えてみてください。生産性が低い開発環境で開発し、動的型付け言語でがんばってテストを書き、型で挙動を保証できない事に時間と自分自身の精神を削り、人件費を垂れ流し、パフォーマンスが低い言語を用いる事によりサーバーの並列台数を増やし無駄に計算資源を必要とし結果としてクラウド費用を溶かす...。Ruby on Rails や Django がパフォーマンスが悪いことは一番初めにお示しした通りですからね、クラウド費用じゃぶじゃぶドブに捨てているといっても過言ではないでしょう。IDE を使って快適かつ高速に開発し、C# を使ってパフォーマンスいいソフトウェアを開発すれば、すぐに元は取れるでしょう。新規のプロジェクトで Rails とか Laravel などのパフォーマンスが出ないものを採用して並列台数誇っているのどうかと思います。というかそんなんだから「結局 OSS が好きとか言ってるけど無料なのが好きなだけでしょw」とか煽られるのです。クラウド費用などもちゃん加味して考えたほうがいい。

IDE を使うことによって、あらゆる事がリアルタイムに、そして正確に指摘され、.(ドット)を押せば使えるメソッド一覧が出てくるのでそれを tab で選択して使って...などの体験は非常にリズミカルな開発を実現してくれます。静的型付け言語と IDE を使うことで、縦横無尽にコードを駆け巡り、修正も IDE の機能経由で行うことにより、ファイルをあっちこっち飛び回ってちまちま手作業で修正するなどの虚無を行う必要がなくなります。 ちなみにコーディング中にいろいろ解析してリアルタイムに開発者にあれこれ指摘してくれる機能、現代では Language Server および Language Server Protocol がさまざまな言語向けに提供される事によって、IDE ではない Visual Studio Code などのエディタでもある程度実現しています。Language Server の多くが内部的には red green trees というデータ構造を用いて実装していたり、そうでない場合でも red green trees に大きな影響を受けています。実はこの red green trees という代物、C# のコンパイラである Roslyn の開発チームが C# の開発体験を向上させるために発明したものだったりします。C# は常に開発生産性を重視して開発されていますから、このあたりも昔から非常に考えらえて作られており、先進的。async/await に限らず、こういう面でもどの言語も真似をしたくなるくらい優れているわけです。

そしてなによりデバッガです。特に面倒な設定もせず、IDE のボタン1つですぐに使えます。デバッガを使う事により、1行1行ステップ実行しながら処理の中身を確認し、処理を追っかける事ができますし、変数の中身もさくさく確認できます。インタラクティブにオブジェクトの中身を見る事ができるので、事前に問題箇所を推定してそれが間違っていたとしても、周辺を含めて調査できるので非常に快適です。また実行時にその場の変数を用いる式を記述しデバッグする事もできます (watch 式 といいます)。自分自身が書いたコードではない、外部のライブラリが提供する型のオブジェクトもデバッガを使うことでどのような状態を持ちどのように振舞っているのか明解になります。print debug とかやってると、何度も怪しい箇所を推定してオブジェクトの中身をコンソールに出力して、それでも足りない事が判明してまた print する箇所をする箇所を増やして再ビルドして再実行してそれでも分からないからまた print する箇所増やして...という虚無な開発ループを回して時間を無駄にする必要がでてきますが、デバッガを使えばそんな事にはなりません。print デバッグは最後の最後の最後の手段くらいに思ったほうが良いでしょう。print デバッグをするために print を適切な箇所に差し込める事が開発者にとって重要なスキルだ、などと主張する方もいますが、そんな20年は時代遅れな戯言は無視してデバッガを使いましょう。

またデバッガは自分が書いたコードが期待した通りに動いているか確認したりデバッグしたりするだけでなく、他人が書いたコードを読むのにも大きな価値をもたらしてくれます。他人が書いたコードの場合、どのような処理をして望んだ振る舞いを実現しているのか、概ね分からないところからスタートします。このような場合、まずはデバッガをアタッチしながら実行し、ステップ実行によって具体的な処理を追っかけてしまうのが理解への早道です。複雑なものなら猶更です。私は OSS を読む際にはデバッガをフル活用しています。というか OSS を読み解くのにデバッガ無しは理解の速度がガクッと落ちてしまうので、かなりシンドイです。またテストをデバッガをアタッチしながらステップ実行し、気になる箇所を1行1行実行しながら読む事によって、関心の対象となっている処理が実際どのように振舞っているのかも簡単に理解できるようになります。コードを読んで脳みそで制御フロー考えるのも大事ですが、こと他人が書いたコードに関してはデバッガを使って理解を深めていってしまったほうが早いです。

それはそうとちらほらテストを書くからデバッガいらない派閥の人に出会うのですが(これ驚くことに1人や2人じゃないんですよね。どこ由来の教義なのかは不明ですが...)、全員ロンドン学派なのでしょうか?個人的にロンドン学派は虚無そのものだと思うのですが、まぁそういう教義でやっているならデバッガはもしかしたらいらないかもしれません。 しかし古典学派のスタイルでテスト書くなら絶対にデバッガは有用だと思います。 一般的に実装の詳細をテストする事はアンチパターンとして知られています。 一方でデバッガは実装の詳細そのものを追っかけるためのものです。 なので担うべき役割がまったく異なるわけです。 テスト書いているからデバッガ要らない派閥の人々は割と真剣に実装の詳細をテストするというアンチパターンを踏んでいる可能性を考慮した方が良いのではないでしょうか? そしてテスト書くからデバッガいらんとか言っている人に限って print デバッグとかいう虚無をやっている事が多い気がします。つべこべ言わずにデバッガを使いましょう。C# に限らず他の言語や環境も同様で、React とかでフロントエンド開発しているなら React Developer Tools とかも活用しましょう。console.log や console.debug によるデバッグとか虚無です、虚無。 便利なものは使っていきましょう。 デバッガに対してやたらとアレルギー反応起こす方がいるのですが、本当に何故なんでしょうね...デバッガ以外の便利なものは普通にいろいろ使っているでしょうに...。

加えて世の中では永遠と IDE とエディタ論争が行われている事は周知の事実かと思います。 個人的な意見としては、エディタをカスタマイズしまくる人は別にエディタ使えばいいと思います。 ですがぶっちゃけ世の中の大半のエンジニアはそんなエディタのカスタマイズに熱意を注ぎません...! VSCode を使っている場合でも、エディタとしての基本的な機能ですら知らず生産性の低い開発をしている人が多いというのが個人的な体感です。 なので大半の人には最初から IDE を渡し、快適な開発体験をしてもらったほうが良いと思います。 そこからエディタに興味をもった場合は移行してみたりすればいいだけです。 結局、便利なものを知らないと生産性の低い開発について何も疑問を抱かないものですが、便利なものを一度知ってしまったらもうそれらが使えないと苦痛で苦痛でたまらなくなるものですから、エディタでも同等以上の開発体験を構築するべく頑張るかもですが、そもそも知らないとどうにもなりません。 そういう意味でも、一度 IDE を渡してしまえばいいと思います。 またペアプロとかする時は IDE の使い方も一緒にいろいろ教えるようにしていますが、エディタの場合あの拡張いれて云々みたいなのが発生せざるを得ませんが、そういった手間が一切発生せず「このショートカット叩いてみ~」みたいなので完結するので良いです。

C# は世界的に人気

あんまり人気だのどうのというのは言いたくないのですが「え、なんで C# で書いているの!?」という、まるで C# がマイナー言語であるかのような事を言われる事が一度や二度ではないので、世界的に人気がある言語であって別にマイナーな言語でもなんでもないのだぞ、というのだけは示しておこうかと思います。

GitHub が出している2024年の調査では C# は5位に位置付けています。 AI 絡みで何某するなら Python、web フロント書くなら JavaScript/TypeScript みたいな現代において必需品となっている言語が上位に来ています。その次に根強い人気の Java が来て、次点で C# という感じ。

stack overflow が毎年公開している人気調査 でも似たような感じで、WEB フロントに必須な JavaScript/TypeScript、AI で必須な Python が来て、その次に Java, そして C# といった感じです。Rust や Go より人気であり、実世界でガンガン使われている事が伺えます。

Web framework においてはどうでしょうか?上位に位置付けているのはほぼフロントエンドのフレームワークですから、バックエンドのフレームワーク人気的には事実上2位に位置付けています。

そしてそれ以外のフレームワークにおいては .NET が堂々の第一位です。

C# とそのフレームワーク達は世界的には非常に人気があるのです。世界的に考えても「え、なんで C# で書いているの!?」とか言われるような言語ではありません...!

他言語と比較して

さて、ここまで読んだ皆さんの中には「紹介されている C# の言語機能 (あるいは標準ライブラリ)、別の言語にも存在するだろ!」などと言いたくなる方もいるかもしれません。

実際、record 相当の機能は Java にもあります。 拡張メソッドであれば、Kotlin の場合は拡張関数、Swift の場合は extensions などがありますし、Rust なんかもトレイトを駆使すれば C# の拡張メソッドほど自由ではありませんがある程度似たようなことができます。 LINQ も同様で、Rust なんかのイテレータに対して同等の遅延評価を行うメソッドも存在しますし、Kotlin でも Iterable interface に対する拡張関数として LINQ 相当のものが標準ライブラリに用意されており、一工夫すれば遅延評価もできます。

しかしながら全体をみれば、やはり C# に落ち着きます。 たとえば Java/Kotlin などの JVM 系はユーザ定義の値型を作成できなかったり、ジェネリクスの型情報はtype erasure によって吹き飛びます。そして Rust は標準ライブラリがとにかく薄いので外部のパッケージをどしどし追加しなければなりません。しかも非同期ランタイムという基盤にあたるところから。 まぁ GC が許容されない環境や仕様が求められるのであれば Rust を書く他ないのですが (少なくとも私はもう C/C++ には戻れない、戻りたくない)、そうでないなら C# がやはり良いでしょう。また C# 以外の言語の外部パッケージは大概推移的依存がヤバイ事になっていますから (npm がその筆頭)、利用している外部パッケージの理解やメンテといった面でも実用上優れています。

ちなみにここまであまり Go lang に触れていないのですが、少し触れておきたいと思います。 個人的に Go lang の言語機能はあまりにも薄く、苦痛を感じるレベルです。また Go には中央集権なパケ管が存在せず、外部のパッケージ使うにしても GitHub 上のレポジトリ等を uri で指定する方式です。中央集権なパケ管がないので GitHub 上からそのソースコードが消し飛ばされたら即時に利用できなくなります。他言語のパケ管だとよほどの事 (セキュリティとか法的な理由とか) がないと簡単にパッケージを消せませんから、そういったリスクを抱えずに済むのですが...。中央集権が嫌だ、という気持ちは理解できないでもないですが、中央集権にしない事のデメリットの方が上回っていると感じます。 さらにパッケージのバージョン指定というごくごく基本的な事でさえ、Go lang の登場からだいぶ時間を置いた後やっとこさ実現されたという始末。 実用上不便でしかありません。 generics も同様で、Go lang に generics が導入されるまでの間「Go には generics がないからいいんだ」と豪語していた方々もいましたが、結局後から導入されましたし、導入されたらされたでなんやかんや皆さん喜んでいたりするという。 Go lang はこの手の「他の言語には当然あるが、現状足りないものを受け入れてまで使う」くらいのモチベーションが無い限り実用は正直しんどいものがあると感じています。 主要なプログラミング言語としてはかなり後発なのに、どうして現代において必須と分かり切っている機能の導入がやたらと遅いのか。 さらに Go の言語仕様は薄くて良い!ということいっている人々がいますが、言語仕様が薄いことのメリットなんて言語のコンパイラやらランタイム実装しない殆どの人々にとって嬉しいのかは非常に懐疑的です (もちろん C++ のような複雑な仕様が無限にあると他人が書いた黒魔術に近いコード読めないつらいとかはありますが、そんなの C++ くらいなものでしょう)。 チーム開発においてチームメンバーがまともなコードを書いてくれないことに絶望して Go lang を選択するというのは非常にありそうな話ですが...。

動的型付けは...言わずもがな。

とはいえ、言語の価値はパフォーマンスやその特性ばかりが全てではありません。 その言語を中心に構成されているエコシステムも重要です。 Python なんかそれが顕著で、言語自体やパケ管などはあまりにも渋いですが、numpy を中心とした機械学習系のライブラリはもはや唯一無二で機械学習にかかわるならもはや避けて通れませんし、それはそれで非常に価値があります (とはいえ Python で書くのもパッケージのメンテもあまりも辛いのですが。誰か助けてくれ)。 Go lang なんかも k8s などと向き合い始めたら必須ですし、ネイティブまわりの諸々にかかわるなら C/C++ は必須な上に過去の膨大な資産を抱えていますから、そういった面での価値もあります。

まぁ、あくまでもプログラミング言語は道具ですからね。 パフォーマンスよく、不要な苦しみを抱えず、課題が解決できれば良いでしょう。 私は C# がさまざまな面で優れていると考えているので C# を推しますが、エコシステム的な面はどうしても他言語が最適な場合は存在します。 そういった場合はエコシステム的に目的に適した言語を選べばよいでしょう (穏便な結論)。

ちなみに最近だと TypeScript のコンパイラが TypeScript から Go lang に port され10倍高速になった事が話題になりました。 動画でも Anders Hejlsberg が「JavaScript runtime は UI やブラウザに最適化されており、コンパイラ等の計算中心ワークロードに最適化されていない」という発言している事からも分かる通り、別に JavaScript (および TypeScript) は計算処理の面において高速なわけではありません。 Go に port されて10倍高速になっている事からもそれは明らかでしょう。ちなみに御仁もパフォーマンスが悪い事について「金を浪費している」と動画の中でも発言されていたりします。不要な金は溶かしたくないですからね、やはりパフォーマンスは大事。 そして!なにやら C# でも Rust でもなく Go lang が用いられている事から、アンチ C# 勢からは「TypeScript は Microsoft の言語なのに C# じゃなくて Go lang 使ってるじゃん!C# オワコンwww」とか「C# の生みの親である Anders Hejlsberg 自身が C# じゃなくて Go lang 採用しているじゃん!C# オワコンwww」などと煽られるのですが、ちゃんと動画をみてください (というか面白いので見るといいと思います)。 Anders Hejlsberg が「port (移植) であり rewrite (再実装) ではない」事を強調している事に注目してください。 既存の TypeScript の実装をそのまま port してパフォーマンスを向上させたい、という強いモチベーションがあったようです。 そして様々な言語でプロトタイピングした結果、port に最も適しているのが Go lang だったので、Go lang を採用する事にしたそうです。 実装も殆ど同一で (何しろ port なので)、なんと標準出力に対するエラーメッセージまで同じ形式になっています。 実際全ての互換を維持したまま rewrite するのは非常に困難でしょうから、当然といえば当然。 そのうえ標準出力に対する表示なども現状維持しようと思うとそりゃ rewrite なんか選択しないか...という感じ。 御仁、「どうして C# や Rust や C++ 等 (の任意の自分の好きな言語) で実装されていないんだ!」というリアクションが来る事は分かっていたようで、このような形で動画内にカウンターが用意されていました。 プログラミング言語は道具である、適切なものを選ぼう、というのを体現している Anders Hejlsberg。うーん流石だ...。

何故 C# を好むのか

C# にはこれだけの利点が存在します。パフォーマンスがよく、他の言語を利用している際に発生する様々な苦痛が存在しません...というのは言い過ぎかもですが、圧倒的に快適である事は間違いありません。ということで、皆さんも C# で開発しませんか?

  • 優れたパフォーマンス
    • 計算処理の速さ
    • 非同期 IO 性能
    • マルチコア・マルチスレッド性能
    • サービスの場合は...
      • ユーザも嬉しい!
      • 運営もクラウド/オンプレ費用が抑えられて嬉しい!
    • パフォーマンス命なゲームも問題なく開発できる
      • これは Unity ã‚„ Godot 等で作られた多くのゲームが存在する事から証明されています
  • オープンソース
    • やはりオープンソースだと気になったらソースコード読めるので嬉しい
  • クロスプラットフォーム
    • 当然 Linux でも動きます。コンテナビリティも抜群です
    • Windows でしか動かないという認識は早急に捨ててください
  • 言語使用上の利点
    • struct/class/ref struct/record
    • null 安全
    • 拡張メソッド
  • 分厚い標準ライブラリ
    • 多くの箇所で標準ライブラリが使われるため、他人が書いたコードを読むのも標準ライブラリを把握していれば高速に理解可能
      • OSS のライブラリでも標準ライブラリが利用される事が多いため、どこのライブラリ由来の API かも分からないコードを読む必要がないので、圧倒的にリーディングが捗る
    • 通常のコレクションやコンカレントなコレクション、LINQ などは挙動が変わる事なく、.NET の version があがるとともに高速化が図られていたりする
  • 不要な依存がない OSS 群
    • npm のような推移的依存関係地獄に陥らない
      • パッケージ管理の苦しみから解放される
    • 利用したいライブラリのコードのみの理解に努めればよい
  • 圧倒的後方互換
    • 安心して C# 及び .NET のランタイム version もあげられる
  • 優秀な非同期ランタイム
  • 優れた開発環境
    • IDE と runtime をインストールすればすぐに開発可能
    • 高速なビルド
    • 煩雑な設定をしなくてもすぐ使える優秀なデバッガ