プログラミング言語オタクとして改めてC#を語りたい
皆さんはC#、使っていますか?
世界的には人気の言語であるC#ですが、残念ながら日本ではあまり流行っていないというのが現状です。というわけで色々なプログラミング言語をかじっている身としては、ちゃんとC#の良さを知ってもらいたい!ということで改めて筆を取った次第です。
C#だけでなくGoやRust、Swiftなどの様々な言語の話を盛り込んでいるので、普段からC#を使っているという方もそうでない方も、是非一度この記事に目を通してもらえると嬉しいです。
この記事を書いたきっかけ
私がこの記事を書き始めたのは、何縫ねの。さんによる下の記事がきっかけだったりします。
内容に関してはC#に関わるあらゆる良さが語られていて非常に良い記事なので、是非とも一読することをお勧めします。特に事前知識: C# と .NETあたりの項目はこの記事では前提知識として説明を省略しているため、.NETが何かをよく知らない方はその辺りを参照していただけるとよいでしょう。
とはいえ、正直な感想としては、C#を持ち上げることを前面に出しすぎていて、他言語との比較に関してはやや雑なように思えます。特にGoへの批判は言語機能の薄さに終始していて、他の部分の比較がないのはいかがなものかと...(ぶっちゃけ私もGoは嫌い寄りなんですが、Goにしかない良さや、C#よりもGoを選ぶべき場面は理解しているつもりなので、悪い面だけ見て比較するのもフェアではないかな、と)
というわけで、この記事ではもう少しちゃんとした比較を用意した上で、改めてC#の優れている点や使いどころについてを説明できたらな、と思っています。私もC#は大好きなので...!
[注意] UnityとC#
この記事を読む方の中には、普段UnityでC#を書いている方も多いかと思われます。しかしながら、UnityのC#は古い部分が多く、現代の.NETのC#とはかなり別物です。
例えばUnityはマルチプラットフォームの実現のためランタイムにMonoを利用していますが、これは.NET Frameworkの時代にC#がWindowsしか対応していなかった名残です。現在の.NETはマルチプラットフォームかつ非常に高速なCoreCLRというランタイムを使っています。Unityは随分前からCoreCLRへの移行を進めていますが、最近になってようやく実現の目処が立ったような状況です。
また、UnityのC#バージョンは長らくC# 9.0で固定されています。実は内部のRoslynバージョンは上がっているため、ちょっと改造すれば最新の言語機能を使えたりもするんですが、ランタイムの更新が必要なコレクション式などの機能はどうやっても動かすことができません。ちなみに現在の最新バージョンはC# 14(先日はC# 15のプレビューが出されました)なので、随分遅れていることがわかるでしょう。
さらに、Unityは標準でNuGet(.NETのパッケージマネージャー)をサポートしていません。そのため、エコシステムに関しても通常の.NET開発とは事情が異なります。この記事で話すのは通常の.NET開発におけるC#の話なので、混同しないように気をつけていただけると幸いです。
C#の良いところ
それでは改めて、C#の良いところを語っていきましょう。
マルチプラットフォーム
ここは未だに誤解している方が多いので改めて強調しておきますが、現在の.NET(C#のランタイム)はオープンソースかつマルチプラットフォームです。Windowsだけでなく、macOS、Linuxでも問題なく動作します。(昔はMonoという別のランタイムが必要でしたが、今では.NETが標準でサポートしています)
これは 「あくまでWindowsがメインで、一応Linuxでも動かせる」 程度のものではなく、「Linuxを第一級ターゲットとしている」 という意味です。現在のC#がメインとしている領域はLinuxで動くサーバーサイドあたりで、むしろWindowsのAPIを叩く用途で使われることの方が少ないように思えます。
そして、開発に関してもWindows以外で問題なく行えます。Visual Studio for Macのサポートは終了してしまいましたが、現在ではVSCode拡張のC# Dev Kitによって必要十分な機能が提供されているほか、有料であればJetBrainsのRiderも利用できます。
私もC#による開発を行っていますが、Windows PCは一切所持しておらず、 全て手元のMacBook+VSCodeで行っており、特に不都合を感じたこともありません。(というかVisual StudioもRiderもあんまり好きじゃないので、VSCodeで全部やってます。Riderは悪くないけど動作が重いのが個人的に微妙で...)
パフォーマンス
C#の特に優れている点として、パフォーマンスの高さが挙げられます。.NET Framework時代はパフォーマンスを指向してこなかったこと、JVMに近いアーキテクチャをしているところなどから誤解されがちですが、現在の.NETランタイムは極めて高速で、gRPCなどのいくつかのベンチマークではRust製のフレームワークに匹敵する速度を叩き出しています。

これはランタイム(というかJIT)が非常に優秀であることもそうですが、C#の言語機能自体に最適化のための機能が豊富に含まれていることに起因しています。仮想メソッド呼び出しを避けるためのGenerics、ヒープアロケーションを避けスタックを活用するためのstruct、スライスを扱うためのSpan<T>/Memory<T>、効率的な非同期呼び出しを実現するValueTask、プラットフォーム抽象化されたSIMDのためのVector<T>、などなど...
なお現代の言語ではデフォルトで速度が出ることが重視されており、それが良い方針であることは同意します。例えばGoはエスケープ解析によって不要なヒープアロケーションを回避しており、Rustは厳格な所有権管理を強いることによって実行時のオーバーヘッドをゼロにしています。とはいえ、ライブラリなどのパフォーマンスが重要な場面では限界までチューニングする際の書きやすさは重要で、その点に関してC#は他言語と比べて圧倒的に優れています。Rustのunsafeなんかは結構書きづらかったりするので...
もちろん.NET自体も進化しています...!現在の世代別GCでは小さめのアロケーションのGCはほぼノーコストになり、Devirtualizationによって不要な仮想メソッド呼び出しが削減され、JVMにもあるObject Stack Allocationなどの最適化も導入されています。こういった細かな積み重ねが圧倒的な速度を実現しているわけですね。
[余談] AOTとJIT
なお、一般的なイメージではVM言語よりもAOT(事前コンパイル)言語の方が高速だと思われますが、決してそんなことはなく、むしろ長時間稼働するアプリケーションに関してはJITが優秀なVM言語の方が高速なことは十分ありえます。 というのもJITには実行時の情報を用いて最適化を行えるというAOTに勝る利点があり、一度温まれば非常に高速です。またC#の実行時型情報はJavaScriptやJVMのそれと比べても豊富であり、最適化が行いやすいようになっています。
もちろんコールドスタートの速度に関してAOTが有利なのは間違いありません。あくまで得手不得手があるということであり、必ずしもAOTが優れているというわけではない、ということです。
コンパイルの速さ
Rustを書いているとコンパイルが遅くてしんどい(キャッシュは効くものの、デプロイしたときに死ぬほど遅いのが本当につらい)みたいなことがありますが、C#のコンパイルはかなり高速です。コンパイルが速いことでお馴染みな言語といえばGoですが、体感ではほぼ同じくらいの時間で終わります。そのくらい速いです。
最上級の開発体験
Visual Studioとともに育ってきたことからもわかるとおり、C#は最初期からIDEとの連携を念頭に置いて設計されてきた言語です。それ故に開発体験の良さに関しては他言語の追従を許さないパワーを持ちます。地味なところではありますが、補完のレスポンスの速さやリアルタイムでのエラー表示の安定感は数ある言語の中でもC#が圧倒的に優れている点です。(Rustはrust-analyzerが遅すぎるのと、Xcode+Swiftはエラーの表示が微妙に遅れるのが不満で...Goはサクサク動いてくれるので好き)
また、C#はデバッガが強力なのも嬉しいところです。ステップ実行や実行中の変数の中身の確認がサクサク出来るため、デバッグがとても捗ります。
あと、やたらC#erはVisual StudioやRiderのようなIDEが好きなイメージがありますが、個人的にはこの手のIDEがあまり好きではないので、VSCodeで何が不満やねん、と思ってます(一昔前のC#の拡張機能の出来が微妙だった印象が強いのでしょうが...)。先程も述べたとおり、今となってはmacOS+VSCodeでも十分すぎるほど快適に開発ができます。必ずしもリッチなIDEが必須ではないというのは強調しておきたいポイントです。(ただしC# Dev Kitに関してはライセンスの問題があるのですが、これに関しては後述します)
モダンなツールチェーン・エコシステム
C#の嬉しいポイントとして、近年の言語によくあるモダンなツールチェーンが整備されているところです。レガシーな言語ではビルドツールが乱立していたりしがちですが、現在の.NETはちゃんとcargoのような現代的なコマンド体系が整備されています。
dotnet newでプロジェクトを作成、dotnet runで実行、dotnet addでパッケージの追加、dotnet buildでビルド...などなど、必要な機能がdotnetに集約されており、直感的に扱えます。JavaやJSなどのようにビルドツールが乱立しているようなことはないため、迷いづらいようになっています。
また、近年ではTop-level Statements、ASP.NET CoreのMinimal API、dotnet run App.csなどの機能が追加され、さらに扱いやすさが向上しています。例えば簡単なHTTPサーバーであれば、以下の1ファイルをApp.csとして配置し、dotnet run App.csで実行するだけでOKです。どうでしょう、Goに勝るとも劣らないシンプルさではないでしょうか。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/hello", () => "Hello, World!");
app.Run();
また、NuGet(.NETのパッケージマネージャー)を中心としたエコシステムも充実しています。npmやcargoほど...とまでは言いませんが、GoやJavaあたりとは結構いい勝負してる気がしますが、どうでしょうか。
さらにC#のエコシステムの良い点として、多くのライブラリが最小限の依存関係で構築されている点を挙げておきたいです。npmやcargoなどでは小さなライブラリが膨大な依存関係を構築しており、依存性地獄に陥りがちですが、C#では標準ライブラリが手厚いこともあり、基本的にそれぞれのライブラリが独立しています。
また、DIやLoggingなどの標準的な機能に関してはMicrosoft.Extensionsという準標準的な立ち位置のライブラリ群によってインターフェースが規定されており、多くのライブラリはこれに沿って実装されています。そのため、実装を差し替えることが容易なのも嬉しいところです。
後方互換性の高さ
C#は後方互換性を重視している言語です。これは言語仕様から標準ライブラリに至るまで徹底しており、10年以上前のコードだろうとそのまま動作してくれます。
後方互換性を重視、というとレガシーな仕様に苦しめられるイメージがどうしても付き物ですが(JavaScript...)、C#は地道な改善や便利な機能の追加は続けられているものの、言語自体に特段大きな仕様変更が起こったことはなく、そこまで大きな落とし穴に嵌ることはありません。また、IDEがより良い書き方を提案してくれるので、それに沿ってコード修正を押すだけで直してくれたりもします。嬉しい。
優れた非同期ランタイム
Rxやasync/awaitがC#発祥であることは知られているようで意外と知られていなかったりもしますが、現在においてもC#の非同期ランタイムは最先端を進んでおり、数ある言語の中でもC#の非同期周りに関しての完成度は群を抜いています。
C#のデフォルトの非同期ランタイムはThreadPoolを活用する設計になっており、Task.Run()で簡単に非同期処理をスケジューリングすることが可能です。
var cts = new CancellationTokenSource();
// 別スレッドに重い処理を逃がす
var task1 = Task.Run(() => Foo(), cts.Token);
var task2 = Task.Run(() => Bar(), cts.Token);
var task3 = Task.Run(() => Baz(), cts.Token);
// 結果を並列で待機
var results = await Task.WhenAll(t1, t2);
また、上のコードでも使っていますが、キャンセルに関しても「非同期メソッドの最後の引数としてCancellationTokenを受け取る」ことがエコシステム全体で統一されています。そのため、基本的には上から下へCancellationTokenのバケツリレーをしていけばOKです。具体的なキャンセル時のハンドリングに関しては少々難しいですが、このあたりはライブラリやフレームワーク側の仕事になるため、ユーザーコードでキャンセルする際は上流からCancellationTokenSourceのCancel()を叩けば十分です。
先程あえて「デフォルトの」非同期ランタイムという表現をした通り、C#の非同期ランタイムは差し替えが可能です。例として最も有名なのがUniTaskで、シングルスレッドで動くUnityに合わせて余計なマルチスレッドのための処理を全てバイパスし、PlayerLoop上で非同期ランタイムを駆動させることで最高効率のasync/awaitを実現しています。このあたりはユーザーが触る場所ではないですが、ライブラリ実装者にとっては最高にクールな機能だと思います。
とはいえ、基本的にはデフォルトの非同期ランタイムが非常に優秀であるため、それをそのまま使えばOKです。標準の非同期ランタイムが存在しないRustとは異なり、最初から優秀な非同期ランタイムが組み込まれているのはありがたいところです。
メタプログラミング
C#のメタプログラミングは大きく分けてリフレクション、Expression Tree、IL生成、Source Generatorの4種類に分類されます。このうち、現在最も一般的に用いられる手法がSource Generatorです。
まずリフレクションですが、こちらは実行時の型情報を扱うための機能で、C#以外にも色々な言語に搭載されています。ただ、当然ながら実行時に動的にアクセスするためパフォーマンスは低くなるほか、正直なところC#のリフレクションの扱いやすさはそこまでよくありません。(このあいだライブラリを作る用途でGoのリフレクションを触ったときは、かなり良くできていて感動しました。最初期からある機能であるため仕方がないとはいえ、C#のリフレクションもこのくらいまともだったら...)
Expression Treeは構文木から動的にメソッドを錬成するための機能で、ある程度の扱いやすさを持ちつつ、コンパイル自体はかなり重いものの、キャッシュさえすれば通常のメソッド呼び出しと変わらない速度で動作してくれます。残念ながらAOT環境で動作しないため、使いどころは限定されますが...
IL生成は、.NETの中間言語であるILを用いて動的に型やメソッドを錬成する黒魔術です。ILはかなり低級な言語であるほか、そもそもこれをやる人間は相当限られているため、他人の書いたIL生成のコードはメンテ不可能なのではないかというレベルで保守がしんどくなります。また、こちらも当然ながらAOT環境では動作しません。というわけで、現在ではSource Generatorを用いるのが基本になります。(もちろんILでしか出来ないことはあるので、完全な代替というわけではないですが)
というわけで前置きが長くなりましたが、イチオシしたいのがSource Generatorという機能です。これはRoslynというC#のコンパイラに統合されている機能で、属性やメソッド呼び出しなどの適当なコードをフックとしてコンパイル時コード生成を行うことができます。例えば著名なMessagePackシリアライザであるMessagePack-CSharpでは、[MessagePackObject]属性から必要なシリアライザのコードをSource Generatorで生成します。
using MessagePack;
[MessagePackObject]
public record Person
{
[Key(0)]
public required string Name { get; init; }
[Key(1)]
public required int Age { get; init; }
}
生成コード (長いので折りたたみ)
// <auto-generated />
#pragma warning disable 618, 612, 414, 168, CS1591, SA1129, SA1309, SA1312, SA1403, SA1649
#pragma warning disable CS8669 // We may leak nullable annotations into generated code.
using MsgPack = global::MessagePack;
namespace MessagePack {
partial class GeneratedMessagePackResolver {
internal sealed class PersonFormatter : MsgPack::Formatters.IMessagePackFormatter<global::Person>
{
public void Serialize(ref MsgPack::MessagePackWriter writer, global::Person value, MsgPack::MessagePackSerializerOptions options)
{
if (value == null)
{
writer.WriteNil();
return;
}
MsgPack::IFormatterResolver formatterResolver = options.Resolver;
writer.WriteArrayHeader(2);
MsgPack::FormatterResolverExtensions.GetFormatterWithVerify<string>(formatterResolver).Serialize(ref writer, value.Name, options);
writer.Write(value.Age);
}
public global::Person Deserialize(ref MsgPack::MessagePackReader reader, MsgPack::MessagePackSerializerOptions options)
{
if (reader.TryReadNil())
{
return null;
}
options.Security.DepthStep(ref reader);
MsgPack::IFormatterResolver formatterResolver = options.Resolver;
var length = reader.ReadArrayHeader();
var __Name__ = default(string);
var __Age__ = default(int);
for (int i = 0; i < length; i++)
{
switch (i)
{
case 0:
__Name__ = MsgPack::FormatterResolverExtensions.GetFormatterWithVerify<string>(formatterResolver).Deserialize(ref reader, options);
break;
case 1:
__Age__ = reader.ReadInt32();
break;
default:
reader.Skip();
break;
}
}
var ____result = new global::Person() { Name = __Name__, Age = __Age__ };
reader.Depth--;
return ____result;
}
}
}
}
Source Generatorが吐くコードはC#であるため、コードのデバッグやメンテナンスが楽なのが嬉しいところです。また、生成コードへはちゃんとIDEでジャンプ出来るようになっているほか、生成コードにブレークポイントを設定してデバッガを動作させることも可能です。このあたりが本当に素晴らしい。
こういったコンパイル時生成の機能はコンパイル時間の増大に繋がりがちですが、現在のIncremental Source Generatorはその名の通りインクリメンタルに動作することで余計な解析を回避することができるようになっています。これをちゃんとインクリメンタルに動作させるようにするには少しコツがいりますが、APIの使い勝手自体はかなり良くできているため、慣れればそう苦労なく書けるはずです。
また、これはマクロと何が違うの?というところですが、コンパイラ自体と密に統合されているため、IDEとの相性が抜群に良いという長所があります。例えばRustのproc_macroは柔軟性が高く極めて強力な機能ですが、自由度が高すぎるあまり、マクロの呼び出し内部で補完が一切効かなくなる事がよくあります。Source Generatorではこういったことは起こりえません。
さらに、生成ファイルはメモリ上で完結しているため、余計なコード生成が不要なのもgoodです。コード生成系のツールは生成コードのgit管理に困るケースが結構あったりするんですが、その手の問題が発生しないのも嬉しいですね。
C#の微妙なところ
せっかくなので、C#の微妙なところも挙げておきましょう。やはり好きな言語であるからこそ、良くない部分も見つめてあげる必要がある、でしょう...?
学習コストがやや高い
これは言語機能が豊富であることの裏返しですが、Goなどのシンプルな言語に比べると、学習コストはやや高くなります。また長年の進化で書き方が変わっていることもあり、最新の機能に追従していくためのコストはどうしても生まれてしまうでしょう。
特に最近のC#のstruct周りの機能は複雑で、それ故にハマりやすい罠もあります。C#を書く上で必須となる機能というわけではないですが、このあたりを使いこなせるようになるまでが少し大変でしょうか...
とはいえRustやHaskellのような高難易度(個人の感想です)の言語と比べれば、習得ははるかに容易でしょう。少なくとも、何らかのプログラミング言語を扱ったことがある方であれば、少し時間をかければ十分書けるようになるはずです。
OOPの部分
今の時代、Java以後の純粋なOOPが時代遅れの概念であることは、最新の言語設計を追っている人間にとっては周知の事実だと思います。
もちろんOOPから生まれたカプセル化などの概念は今でも有効ですが、クラスによるサブタイプ多相(継承)の表現力はあまりに貧弱であり、CPUの性能が頭打ちになってきた現代では、GC任せのカジュアルなヒープアロケーションによる速度低下の問題もバカになりません。そのため、後発の言語であるGoやRustは既にオブジェクト指向を捨て去っています。
また、C#を含めた現代のOOP言語はinterfaceをサポートしており、継承を使う場面はほとんどありません。C#に関しては拡張メソッドなどの機能も充実しているため、正直なところ現在のC#がOOPである必要性は(下位互換性などの問題を除けば)ほとんどないと思っています。今となってはバットプラクティスを誘うノイズでしかないでしょう。
C# Dev Kitのライセンス
先ほどはMacでも快適に開発できるとは言ったものの、開発環境のライセンスという問題があります。というのもC# Dev KitはOSS化されておらず、利用に関してはVisual Studioと同じライセンスが適用されます。一応個人〜中規模程度の開発であればライセンスに引っかかることはほとんどないと思われますが、心象的にここはマイナスポイントでしょうか...
一応C#の拡張機能自体はOSSかつMITライセンスであり、機能的にもこちらだけで十分開発は出来ます。Visual Studio並みの機能が欲しい場合はC# Dev Kit、という使い分けになっているため、ライセンスがなければ全く開発ができないわけではない、という点には注意してください。
Blazorが微妙
C#には、MicrosoftイチオシのBlazorというフレームワークがあります。これはC#とRazorという記法を用いてフロントエンドを記述するためのもので、雰囲気的にはVueに近い感じです。この言い方ではかなり良い感じに聞こえるのですが、はっきり言って使い勝手はかなり微妙です。
理由は単純で、当然ながらC#はJSに比べてエコシステムの面で圧倒的に不利です。そもそもC#はフロントエンド用の言語ではないため、この領域でJSに勝てるはずもなく...
さらに、いくらC#で書けるといってもDOM操作などはJSを通じておこなう必要があるため、結局はJSを書く羽目になります。こうなるともはや、最初からVueかReactでいいじゃん、という。
また、歴史的経緯によりBlazor Web App、Blazor WebAssemblyなど、Blazorと付く名前のものが乱立しており、正直私もどれが何だか理解しきれていません。ある程度追っている側の人間でこれなので、初学者がこれをまともに選択できるかというと...
技術的に見てBlazorは面白いっちゃ面白いんですが、無理やり動かしている感が強いです。というのもBlazorはC#をフロントエンドで動かすために、IL(.NET VMの中間形式、JVMバイトコードのようなもの)を実行するインタプリタ自体をWebAssemblyにコンパイルし、それを通してILそのものをインタプリタ実行します。あまりにも富豪的すぎる...(一応最近ではNativeAOT-LLVMを用いてC#を直接WebAssemblyにコンパイルするプロジェクトも進んでいます。とはいえ、実用レベルになるまではまだしばらくかかりそうです...)
とはいえ、バックエンド・フロントエンドでC#の型定義を統一できるというメリットはあり、ちょっとした開発用のツールを組む分には便利だったりします。ただ、それ以外の用途で使うことはないかな、と...
他言語との比較
Go
数ある言語の中で、C#と最も立ち位置が近い言語は個人的にはGoだと思っています。サーバーサイドをメインターゲットとし、アプローチは違えど優れた並行処理の機構を持ち、コンパイルが高速で、手厚い標準ライブラリがあり、エコシステムも充実している。(あまり知られていませんが、C#もGo同様に単一バイナリへの事前コンパイルが可能です。Go同様にランタイムごと載るためバイナリサイズは増加しがちですが...)
まずGoは本当に書きやすい。C#もそうですが、優れた静的解析によって非常に良い書き味を発揮しています。構文自体がlightweightなこともあってとにかくサクサク書いていけます。書き方が限定されているおかげで迷いづらいのも有難いところです。
また、とにかくGoroutineが素晴らしい。雑にgoと書くだけで並列に処理できる、なんとエレガントでしょう。C#以降の言語がasync/await一強の中、あえて異端のアプローチをとって成功を収めている点は本当に凄い。
さらに、GCを採用していながらもデフォルトでかなりの速度が出るのが嬉しいところです。以前はGCの遅さが取り沙汰されてRustに移行した話なんかもありましたが、GoのGCの性能改善に関しては精力的に取り組まれていて、現在のGreen Tea GCであればほとんどの場面で必要十分な速度は出せるでしょう。
とはいえ、苦痛なレベルで言語機能が少ないという点には同意します。もちろんそれがGoの哲学であることは理解しますし、その割り切ったシンプルさがコンパイラの複雑さや初学者の学習コストを抑える上で大いに役立っていることには間違いないと思いますが、やはり機能面の不足が苦痛な場面は多くあります。nil安全でない点や頻出するerr != nilなどの冗長なコードもそうですし、とにかく型の表現力の貧弱さが辛い。特にenumをiotaで代用する辺りが本当に苦痛で、最低限C-likeなenumくらいは用意した方が良かったのでは...と思ってしまいます。
また、言語自体の進化が遅いのも良くない点でしょう。機能を増やしすぎてGoの良さが失われるのは困るという意見には賛成ですが、それにしても流石に歩みが遅すぎます。少なくともジェネリクスの導入が遅れたことに関しては完全に失敗だったでしょう。C#が進化を続けながら完璧な後方互換性を保っているのを見ると、ここに関してはもう少しなんとかなって欲しかったな...と。
色々言ったものの、現実的にサーバーサイドで最初に挙げる言語はC#よりもGoかな、とは思います。チーム全員がC#に慣れていればそれでも良いですが、学習コストや手軽さを考えると、多くの用途ではC#よりもGoで十分だと思います。(もちろん、近年のC#ではdotnet run App.csによる単一ファイル実行やMinimal API+NativeAOTでGoレベルにシンプルな構成を実現できます!という意見はわかりますが、それって別に最初からGoでいいじゃん、という...)
一方、エンタープライズな領域に関しては、長らくその領域で活躍してきたC#の方が強力でしょう。言語自体の表現力もそうですし、エコシステムに関してもASP.NET CoreやEFCoreなど、標準で多くの機能が充実しています。とりあえずGeneric Hostに載っけておくだけで大抵のものがいい感じになるのも嬉しいところ。また、大規模なGoコードを管理するのは結構しんどく、その辺に関しても型やDIのパワーが強いC#の方が優秀な印象です。
とはいえ今のC#は結構気軽に書けるようになっているので、Go使いの方々にも是非触ってみて欲しいところです。書き味の軽さに関しては結構近いんじゃないかな〜と思っています。
Rust
私はC#erであると同時にRustaceanでもあるので、Rustは最高の言語だと信じて疑いません。
Rustが素晴らしい点は挙げていくとキリがない(手続き型と関数型の良いところを両取りした最高級の言語設計、所有権ベースのメモリ管理、よく整理されたツールチェーン、etc.)ので、ここでは省略します。言語処理系の実装をかじっている身としても、Rustの設計は信じられないくらい良くできています。
とはいえ、Rustは万能な言語ではないという点は強調しておきたいです。その複雑さ故の学習コストの高さは否定できませんし、また所有権の厳格さが初期段階での開発効率の足を引っ張ることはよくあります。システムプログラミングにおいて所有権は極めて強力ですが、アプリケーションレイヤーでは雑にGCに任せてさっさと書き進めて行きたいことの方が多いでしょう。また、非同期周りに関してはtokioで一悶着あったこともあって混乱を招いており、あまり良いとは言えません。そもそも所有権とasync/awaitの相性が悪いという問題もあり、至る所Arc<T>/Mutex<T>だらけのコードを書く羽目になります。
そのため、私としてはむしろC#/Rustの二刀流を薦めたいです。最近ではcsbindgenというrust-bindgenを用いてCやRustのFFIコードを生成するライブラリもあるため、両方使えると役に立つ場面はかなりあります。Rustはいいぞ。
Swift
C#同様あまり話題に挙げられることがないSwiftですが、言語機能的にはbetter C#とでも言うべき素晴らしい設計になっています。nil安全な機構やRustのenumのようなタグ付きユニオンなどのモダンな言語機能をもれなくピックアップしているほか、例外周りに関してはJavaの検査例外を昇華させたモダンな仕上がりになっており、非常に良い感じです。また、async/await周りもコンパイラと密に統合する仕組みによってかなり洗練されています。(もちろんPromiseが暗黙的なのは、非同期ランタイムのカスタマイズの余地が少ないこととのトレードオフではありますが)
一方、Xcode以外にまともに使えるIDEが存在しない、というわりと致命的な問題があります。一応VSCode拡張も動いてはくれるんですが、Xcodeと比べると静的解析の信頼性は一段劣ります。特にSwiftUIに関してはXcode以外の選択肢は実質的に存在しません。このあたりは仕方ないといえば仕方ないですが、そもそもXcodeの使い勝手がかなり悪いという...
また、Appleの言語として使われていたこともクロスプラットフォームへの対応がかなり後手に回っています。一応最近はLinuxやWindowsでも動くようになっていますが、macOSやiOSと比較するとサポートの程度はかなり微妙です。
さらに、言語仕様自体の変更が激しく、度々破壊的変更でコミュニティを混乱させている印象があります。C#やGoが慎重すぎる感はあるのですが、それにしても安定感がなさすぎる。一応最近は安定してきている感じがしますが、メジャーバージョンが変わるたびにコードが動かなくなる、みたいなことがあるのが辛いところです。進化を恐れないところは評価すべきですが...
というわけで言語自体は非常に良いのですが、現実的に採用できる言語かというとかなり微妙さが残ります。個人的にはSwiftでサーバーサイドを書くならC#を薦めたいところです...
Java
JavaもC#同様にレガシーなイメージが先行しがちですが、現代のJavaは結構進化しています。C#もそうですが、なんと今のJavaのHello, Worldにpublic static void main()は出てきません。
void main() {
IO.println("Hello, World!");
}
とはいえ、実行時の型情報の少なさや値型を定義できないなど、C#を書いた後だと辛い部分はかなりあります。あと今どき文字列比較を==で出来ないのはどうなんでしょうか...
今から書くならKotlinを選んだ方がいいような気がしていますが、私がKotlinをほとんど書いたことがないのでJavaを選ぶべき場面があるのかどうかは分かりません...有識者の意見をください...
Kotlin
ぶっちゃけほとんど書いたことがないので分かりません...
パッと見Javaより洗練されていて、DSL用途にも強そうでなんかいい感じに見えるんですが、サーバーサイドに関しては微妙そうに見えるのと、どうしてもJVMであることの制約を背負っているな気がしていて...
なお↑は完全に印象だけで喋っているので、誰か実際のところどうなのか教えてください。
MoonBit
せっかくなので、最近話題のMoonBitも上げておきます。まだ1.0には到達していませんが、個人的にはかなりお気に入りの言語です。
ご存知ない方も多いと思うので軽く説明しておくと、WebAssembly/JSをメインターゲットとした言語で、文法はRustにGCを付け、さらに関数型に寄せたような雰囲気です。
struct Point {
x: Int
y: Int
} derive(Show, Eq)
pub fn Point::new(x: Int = 0, y: Int = 0) -> Point {
{x, y}
}
fn Point::get_x(self: Self) -> Int {
self.x
}
test "test Point" {
let p = Point::new(x=1)
assert_eq(Point::{x: 1, b: 0}, p)
}
何よりツールチェーンやLSP周りの完成度がとにかく高く、サクサク書いていけます。また非同期周りは未だに安定していませんが、言語自体は十分枯れてきています。
ただし、MoonBitはどちらかというとエッジコンピューティングやフロントエンドをメインターゲットとした言語であり、バックエンド用途で使うにはまだ弱いかな...という印象です。一応既にネイティブやLLVM向けにコンパイルすることも可能ではありますが、非同期ランタイム周りや文字列の扱いがJSとの相互運用にかなり寄せて作られており、CLIツールやバックエンドの開発という用途においてはやや不利になります。C#やGoなどの言語と比較しても、用途としてはあまり重なっていないかな、という感じですね。どちらかというとbetter TypeScriptとしての立ち位置が大きいんじゃないでしょうか。
まとめ
結局のところ、プログラミング言語は道具に過ぎません。手に馴染むものを使うのが一番であり、他言語のユーザーに利用を強要できるものでもないでしょう。あくまでこの記事はC#の良さを語るだけの記事であり、C#の利用を推奨するようなものでもありません。いい言語だとは思いますが、流行らないものは仕方ない...
とはいえC#は依然として人気な言語ですし、モダンな言語に肩を並べられるだけの機能やエコシステムを備えている、第一線で活躍出来るポテンシャルを持つ言語です。この記事を通じてC#の良さが伝われば、そして、Goの型の弱さやRustの厳格さに疲れた時の新しい選択肢としてC#を加えていただければ幸いです。
Discussion
「Benchmark Game」によると大体C++/Rustの次くらいになるみたいです。
Benchmark Gameでは「AOTとJIT」の差も出ていて、全体の7~8割くらいはAOT実行の方が速いのでそういう印象になってしまうのも仕方ないのかもしれません。
またAWSもNativeAOT推奨っぽい記事出してますし、file-based app (dotnet run app.cs)ではデフォルトでNativeAOT実行なので、世の中の流れ的にもAOT=速い、みたいな印象はよりぬぐいにくい感じになっちゃってるなあと感じます。
個人的にはハイブリッド実行方式の Ready To Run(R2R)はもっとアピールされていいと思うんですが…。
文脈的に "Web" フロントエンドのことだと思いますが、UI=フロントエンドとして考えると、むしろC#はフロントエンド用の言語ではないかと思います。
Delphiの開発者を招き入れて設計され、当初の主要用途がデスクトップアプリだったという誕生の経緯から、のちのasync/await・MVVM・Rx等の概念等。
string型にCultureの概念があるのもフロントエンドでの表示を想定しているようにも思えます。項目の主旨とはことなる細かいことなのですが、C#をよく知らない人も対象にするのであるなら誤解を生む表現かな、と思いました。
普段お書きになってる高パフォーマンスなコードを書こうとするとかなりコストが高いのは事実だと思いますが、
PythonやJavaScriptみたいなバージョン・ランタイム選択・管理の煩雑さがないこと、
「とりあえず動けばいい」ならfile based appをはじめとするスクリプトライクな書き方を考えると、
むしろ初心者向けの学習コストが低い言語ではないかと思います。
ただ初心者には標準APIと言語機能の区別がつかないのも事実で、そうなると巨大すぎる.NET APIがかなりの学習コストになるのもまた事実かなとも思います。
C#14だとExtensionsが入り、
ConditionalWeakTable<T1,T2>使うと拡張フィールド/自動実装プロパティも再現できるようになりましたが、同様の言語機能があるSwiftや、traitの機能があるRustと比較してどう思われますか?Riderは非商用利用なら無料で、商用利用は有料なので実質的にVS2022/2026, Rider, C# DevKitは同じライセンス体系かとおもいます。
OSSかつ完全無料なら「DotRush」はいかがでしょうか?
VSCode/Cursor等の派生エディタ/NeoVim/Zedで使え、基本機能はそろっているように思えます。
※まだ個人では試せていません
言及がない機能としてRoslyn Analyzerはどう思われますか?
コンパイラに統合されている静的解析機能(Linter)としてはRustも近いものがあるという事なのですが使ったことがないので個人的に分かりません。
また、Source Generatorと併用すると実質的に言語拡張が一部できると思いますが、他言語と比較してこのあたりどうなのでしょうか?
コメントありがとうございます!
実際ほとんどのケースではAOTの方が有利なので、まあそれはそう...
C#は割とゲームサーバーのような長時間駆動するものにも使われていて、その辺りはJITの得意分野でもあるんですが...
そうですね、GUIという面に関してはC#は結構強い言語だと思います。イベント・非同期周りの書き方を生み出してくれた功績は大きい...
フロントエンドという言葉自体はWebの文脈で定着している感はありますが、誤解を招く可能性はありますね、あとでちょっと直してみます。
近年のC#はスクリプト的な用途にも注力していて、public static void Mainのようなおまじないもなく書けるので、いい感じになってきてはいます...!ただどうしてもOOPという時点で学習コストが上がるのと(top-level statementsの書き方から入ると余計に混乱しそう...)、バージョンごとに書き方が増えていて記事の情報が古くなっていたりするのがあったりして、ちょっと大変かなーとは思います。この辺はGoが本当に良くできていて、やっぱり初心者に優しいのはGoかな、と。
また、PythonならColabのような環境があって手軽に試せるという利点があって、C#はちょっとその辺りが不足気味な気がします。有志のWebサンドボックスはいくつかありますが、どれもそこまで使いやすくはないため...
そういえばRiderは最近無料になってましたね...!あとで追記します。一応VSCode+ReShaperという選択肢もアリではあるんですが、私は挙動があんまり好きではないので...
DotRush知らなかった...!ちょっと見てみます...!
むしろ私としてはExtensionsは好きではなくて(拡張メソッドの構文に機能面の不足があることは理解していますが)、これを入れるくらいなら最初からRustのようにtraitに一本化する方がスマートかな、と思っています。これはSwiftも同様で、どこにメソッドを置くべきか迷うケースが多いんですよね。Rust、Nim、MoonBitなどの近年の言語は構造体と実装を分ける方針で、今となってはこっちが主流かな、と。
詰め込むか迷って結局入れなかったんですが、Roslyn Analyzerも最高の機能です!Linter的な使い方もそうですし、Code Fixの提案なども出せるのが嬉しいですね。Goは言語機能が少ない分、linterが発展しているように思えます。Rustはコンパイラ自体のlinterが丁寧な反面、この辺の拡張が出来ないことはないけど結構大変だったような...?(あまりやらないでわからず)
言語拡張という意味では、マクロの自由度が高いRustなどの方が強い印象があります。Source Generatorはコードの置き換えができないので、意外とやれることが少ないんですよね。最近はインターセプターも出てきたので、一応やれないことはないですが...
また、DSL的な用途ではKotlinが強いみたいな話を聞いたことがあるんですが、例によってKotlinはよくわからないのでなんとも...
Kotlinは、Android開発のイメージが強いかもしれませんが、基本的にJavaで使えるものはKotlinでも使えるので(例外はあり)、Javaで書いていたものをそのままKotlinに持ってくることができるので、Javaの人が軽い気持ちでKotlinに移ることもできるぐらいですね。
Kotlinの環境を整えてれば、特殊な設定も何もいらずにJavaとKotlinをまぜこぜにもできます!Kotlin -> Java, Java -> Kotlinといった呼び出しもできてしまいます!
入門者が、Javaから始めるかKotlinから始めるかの2択だとしたら、普通にKotlinで良いかなと思います。モダンなほうがモチベーションも上がると思いますし。
Java,KotlinがJVMであることの制約というか仕組みはC#も似たような課題はありそうな?C#をあまり知らないので、わからんですが…。
なるほど、今ならとりあえずKotlinを選んでおけば良さそうですね!
JVM側の制約として、ユーザー定義の値型(C#だとstructがある)やunsafeなポインタを利用出来ないあたりがパフォーマンス的に辛そうかな...と思っているんですが、どうなんでしょう...?
Kotlinには
@JvmInline value classを使うと、プロパティ1つ限定の制約はあるけど、見た目はクラスなのに内部ではクラスとして扱わずに値として扱うようになったりする。Javaのほうも似たような機能が将来出るのかもう出たのかってところですね(Javaの最新機能は微妙に追いきれてないので曖昧ですが)。JVMの制約を越える系の技術もいくつかあって、低レベル操作のsun.misc.Unsafe、JVMのヒープ外メモリを扱うDirectByteBuffer、同じくヒープ外メモリやネイティブ連携まで含めて扱えるFFM API(Foreign Function & Memory API)があったりします。
かなり低レベルまで触れる感じで良いですね...!
Java/Kotlinのその辺りの進化を全く追えていないので、そっちも機会があったら触ってみたい!
Javaの
mainはスクリプトがどのパッケージにも属さないことが前提になるので、テスト用ならともかく実用性は薄いと思われますstaticが不要になるだけでも大きいですが、クラス自体は必須その点C#はトップレベルが実用性を担保しますが、こちらも内部ではクラスなのでオブジェクトとしての制約があり、痒いところに手は届かない印象
個人的にはクラスとしてエントリポイントを定義したいです
C#は非常に優秀な言語だとは思うのですが、Windowsの資産が大きすぎて、Windows以外で使う意義をあまり見出せない点が惜しいです
Javaに比べると未だにWindows以外での安定性に信頼が薄い印象です
コンパイルはできても実行ファイルが動かないことがありますし
そうなんですね!最近久々にJava書いて変わっているのにびっくりしましたが、実際使うとなると微妙そうかも...?
C#のTop-level statementsは結構好きですが、あれも内部表現がクラス故の使いづらさは若干ありますね...
Macで開発していて困った経験が特にないので、今となってはそんなことはないと思うのですが、どうですかね...?少なくとも、.NET Coreの黎明期に比べれば現在は遥かに安定していると思います。
GUI系がWindowsに寄りすぎている感はありますが、今ではAvalonia UIやMAUIなどもあるので一応クロスプラットフォーム開発もできるはずです。(個人的にはXAMLが好きじゃないのでElectronかTauriを使ってますが...)
Linuxだとビルド時にオプションが必要だったり
dockerだと何故かapacheを入れないと動かなかったりするので、他環境で動かす時には気を使いますね
Javaなどの元がマルチプラットフォームな言語はその点の対応力が強み
特にgccやPythonは本当にどこでも動くので困ったらとりあえずPythonから使う、ということも多いです
c#についての話しと言うことですが
Blazorには触れておられないのが
少し残念な感じです
処理速度に関してはc#を選んだ時点で
プログラムの書き安さに振ってる思うので
あまり気にしてないで書いてます
ただ、今は7から8割はAIに任せてるのが現実ですね
それは Lambda なのでコールドスタートパフォーマンスが重要な分野だからですねー。
公式ドキュメントがイミフすぎるのをどうにかしてほしい。
いくら調べてもなんもわからんのですよ。
なんというか、超初心者向けのHelloWorldと、わかってる人がわかってる人向けに書いた高度なやつはよく見かけるんだけど、その中間が全然ないってイメージです。
OOPを捨て去った訳ではなく、OOPの実現方法が変わったというのが正しいんじゃないですかね。継承は所詮OOPの一要素でしかないので、継承が使われなくなった=OOPを捨てたというのは違うと思います。継承を捨てたなら、まだ理解出来ますが。