2025年を振り返る
- 2025-12-30
今年のC#やりこみ振り返り……!
新規:
大きめアップデート
作りかけ
毎年、今年のやりこみ度は大丈夫かな?という気になりますが、とりあえずZLinqが大型案件(?)だったので大丈夫でしょう。注目度(Star数は現在4800行きました)も高く、実装の安定度も高く、奇抜性も高くということで、いいものになっているのではないかと思います。Star数でいうと、UniTaskは10000超えしました!GitHub/Cysharp全体では45000超え、neueccやMessagePack-CSharp名義のものも含めれば、トータルで60000を超えるということで、C#関連でいったら世界屈指と言えるのではないでしょうか。
そんなわけで悪くない結果だと思うのですが、一方で今年はブログをほとんど書いていません!年々書かなくなっていますが、過去一で書いていません!理由としては体調不良で、ZLinqを終えてちょっとした辺りグッと体調を崩してしまって色々と停滞してしまいました。OSS関連も意識的に手を付けないようにしていたので、Issue/PR系も過去一番に手を付けられていません……!言い訳がましいことを言うと、作りかけを完了できなかったのもこの辺が悪く――。といった感じでは不満の残る結果でもあります。一応、復調傾向にはあるので、来年はじっくり取り込みたいとは思っています。
これは私が発表したわけではありませんが
というセッションでShadowverse: Worlds Beyondのアーキテクチャに関する発表があったのですが、かなりCysharp/MagicOnionについて言及してもらっています。触れられているとおり、初期から開発に関わらせていただいています。採用ライブラリも「MagicOnion、R3、ZLinq、MessagePack for C#、UniTask、YetAnotherHttpHandler」とアグレッシブに新しいものも詰んでもらいました。リリースを迎えられるのは、やはりとても嬉しいですね……!
なんとか of the Year
Game of the Year総舐めですが、やはり私もClair Obscur: Expedition 33を挙げたいと思います。ああ、こういうの好きだったよなあという感覚と、ただの懐古主義じゃない完全に現代的に調和した内容。こうあって欲しいという思いが見事実現化されているようです。めちゃくちゃフランスを感じるところは、西洋風のエスニックで、馴染みあるようで全然なくてめちゃくちゃ新鮮でもあって、まぁ、とにかく良い、文句無し……!全てが気合入っているというよりは、見せ場には力をしっかり入れつつ、手を抜くところはしっかり抜くというバランス感覚は開発的には大事ですねえ。ゲームバランスが序盤-中盤がかなり緊張感ある面白さだったのに反して、後半はインフレバトルに突入するのは、一粒で二度美味しい、というには、やはり大味になりすぎた気がするので若干残念ですが、まぁ味変ということで、それはそれで良いでしょふ。
なんか買ったものとしては、軽量化計画ということで財布をZenlet Cache 3.0に変えたのですが、ポケットがすっきりして実際めっちゃ良き。数年前から小銭入れのない財布を使ってきて、まぁまぁやれてたので、去年はthe RIDGEというマネークリップを使っていたのですが、更に突き詰めたいと思って色々探していて見つけたのがCache 3.0でした。
マイナンバーカードもiPhoneに入るようになったし(とはいえマイナンバーカードは持ち歩いていますが)、楽天銀行もスマホATMに対応したしということで、カードも厳選して持ち歩く枚数をかなり減らせるので、最小限の最小限で十分かな、と。最小限のカード(クレジットカードとマイナンバーカードとその他一枚)と最小限の現金(1万円札1枚と1000円札1枚)で問題なし。Phoneにペタっと付く&キックスタンドになるのも便利なので、良い塩梅でした。
iPhoneはiPhone Airに変更しました。ここ数年ずっとPro Maxだったので、こちらも一気に軽量化……!基本的に欲しいのは画面サイズだったので、Pro以上Max以下の画面サイズは個人的にはジャストです。まぁ不人気すぎて後継機器は出なそうなので、Foldが来たら一気に重量化しちゃいますが……。バッテリー持ちがかなり悪いので、マグセーフ型のバッテリーとしてMATECH(マテック)® Qi2 モバイルバッテリー マグセーフを合わせています。これはマグセーフに加えてスタンドが付いてるところがポイントで、外で、というよりかは家の中で、休日とかに1日中持たないので、スタンド代わりに使いながら充電することで1日持たせる、みたいな使い方をしています。というわけで、スタンド付きが重要です。財布もスタンド付きが重要だったということで、今年はスタンドブームが来てます……!
バッテリーといえばAnker Power Bank (25000mAh, Built-In & 巻取り式USB-Cケーブル)が今年買って、一番便利だったもの、かも。これも完全に家用なのですが、ワイヤレスで移動可能な軽量充電ステーションとして使ってます。コンセントの近くまで行かないと充電できないのが何かと不便だったのですが、これがあればどこでも充電ステーション化できるので、ちょっとした小物の充電がめっちゃ楽になりました。25000mAhあれば、私の使い方的には一週間は持つので、この容量が個人的にはベスト(あまり大きくするとデカいし重いしになるので)。ケーブルが2本ビルトインで生えてるのもいい塩梅です。
というわけで巻取り式ケーブル内蔵ブームが来たのでAnker Nano Charger (35W, 巻取り式 USB-Cケーブル)とAnker Nano Charging Station (7-in-1, 100W, 巻取り式 USB-Cケーブル)も調達して、便利度が上がりました。
来年
使ってないわけではないですが、AIブームから乗り遅れている&Vibe Codingには懐疑派として、CompilerBrainというC# Coding Agentの計画があるので、来年初頭には見せれるものに仕上げたいと思っています。あと今年溜まったOSS Issueもなんとかやりきりたい……!
それとUnityのCoerCLR対応がついに来年くる、っぽいので、めっちゃ楽しみにしています。今まで以上にC#やり込みパワーが火を吹くときが来ましたか……!言語を極めることで、表現できることの幅というのは広がるものなのです、ということを実証していきますよ……!
今年はOSSとお金に関する話も多くトピックスに上がって来ました。C#でも、やはりホットなトピックスで、有名ライブラリの有償化や、提言など色々が色々です。私としては引き続きOSSベースでやっていきますし、有償化は全く考えていません。とはいえメンテナーに強く負荷がかかることもあり、無償の奉仕を求めるのが当然ではありません。持続性のあるOSSエコシステムを求めるなら、なおのこと、お金に関しては強く考える必要があります。そんなわけで改めて、特に直接の還元ができているわけではないですが、sponsors/neueccへの寄付はとてもありがたく思っています。
というわけで引き続きC#を発展させていきますので、来年も頑張りましょう……!
ToonEncoder - C#とLLMのためのJSON互換フォーマットエンコーダー
- 2025-12-23
Token-Oriented Object Notation(TOON)というJSON互換のフォーマットのシリアライザー(エンコードのみ)を作りました。TOONは、適切に活用することで、LLMとの対話時に、トークンを大きく節約できる可能性を秘めています。コンパクトなライブラリーではありますが、内部的には全てUTF8ベースで処理していて、IBufferWriter<byte>対応やSource Generatorによるシリアライザー生成など、現代的なライブラリとしての基本機能は十分備えています。
もちろん、競合と比べてもパフォーマンスやメモリ効率は圧倒的に良いです。

この辺はとにかく私がシリアライザーの設計に慣れすぎていて(MessagePack-CSharp, MemoryPack, Utf8Json, etc...)実績もノウハウもありまくりなので……!ジャンルがジャンルなのでAIでとりあえず動くものにしましたっぽいライブラリも多い感じですが、全然勝負になりません。なんせこちらは温かみのある手作りコードですから……!ハイパーハンドメイドクラフトコーディング。現状のAI生成のコードレベルは、トップレベルからは、ほど遠いと実際思ってます。動くものはできるし、それは凄いんですけど、ね。
さて、まずTOONについて軽く説明すると、以下のJSONのデータが
{
"context": {
"task": "Our favorite hikes together",
"location": "Boulder",
"season": "spring_2025"
},
"friends": ["ana", "luis", "sam"],
"hikes": [
{
"id": 1,
"name": "Blue Lake Trail",
"distanceKm": 7.5,
"elevationGain": 320,
"companion": "ana",
"wasSunny": true
},
{
"id": 2,
"name": "Ridge Overlook",
"distanceKm": 9.2,
"elevationGain": 540,
"companion": "luis",
"wasSunny": false
},
{
"id": 3,
"name": "Wildflower Loop",
"distanceKm": 5.1,
"elevationGain": 180,
"companion": "sam",
"wasSunny": true
}
]
}
TOONで表現すると以下のように小さくなります。
context:
task: Our favorite hikes together
location: Boulder
season: spring_2025
friends[3]: ana,luis,sam
hikes[3]{id,name,distanceKm,elevationGain,companion,wasSunny}:
1,Blue Lake Trail,7.5,320,ana,true
2,Ridge Overlook,9.2,540,luis,false
3,Wildflower Loop,5.1,180,sam,true
JSONというよりかは、YAMLとCSVのハイブリッドのようなもので、特に、テーブルとして(CSVとして)表現できる、プリミティブ要素のみを含むオブジェクトの配列が、CSV的に出力されるのでデータが大きく縮みます。この縮み幅がLLMにおけるトークンの節約に繋がるということでちょっとだけ脚光を浴びました。ならよくわからんフォーマットじゃなくてCSVでいいじゃん、というと、CSVだけだとテーブルのみで付随情報がつけられなくて実用には厳しいので、こちらのほうが使い勝手は良い印象です。また、JSONと相互互換のある仕様にしていることで、JSONからのDrop-in replacementが可能というのもセールスポイントにはなっています。
個人的な所感としてはTOONはヒューマンリーダブルではないです。TOONは効率性に寄せているため、配列の表現方法が3種類あります。ToonEncoderではTabularArray、InlineArray、NonUniformArrayと呼んでいますが、3種類あると正直読みづらいよね。また、TabularArrayとNonUniformArrayがオブジェクトのネストと合わさると、インデントがわけわからなくなります。LLMは、よくわからん形式とはいえ、ヒューマンリーダブルなら、なんとなくちゃんと読み取ってくれている雰囲気がありますが、そうした破綻した状態で解釈を正しく持ってくれるかどうかには不安があります。
というわけで、JSONを全て置き換えるのではなく、ピンポイントにCSV的なテーブル(TabularArrya)か、フラットなオブジェクトにTabularArrayを末尾に足したぐらいのものに適用するのが、トークン効率的にもLLMの理解力的にも人間のリーダビリティ的にもちょうど良いのではないかと思っています。実際ToonEncoderではそうした運用で最高なパフォーマンスが出るように調整してありますし、Microsoft.Extensions.AIとの組み合わせで、一部の型のみToon化する、といった連携ができるようになっています。
Microsoft.Extensions.AIと一緒に使う
NuGet/ToonEncoderからダウンロードしてもらうとコアライブラリ―とSource Generatorが同梱でついてきます。なお最小ターゲットプラットフォームは .NET 10 です。
基本的にはEncodeでJsonElement、またはT valueを変換できます。
using Cysharp.AI;
var users = new User[]
{
new (1, "Alice", "admin"),
new (2, "Bob", "user"),
};
// simply encode
string toon = ToonEncoder.Encode(users);
// [2]{Id,Name,Role}:
// 1,Alice,admin
// 2,Bob,user
Console.WriteLine(toon);
public record User(int Id, string Name, string Role);
今回はプリミティブ要素のみのオブジェクト配列のため、表形式レイアウト(TabularArray)としてシリアライズされています。
具体的な利用法としてはMicrosoft.Extensions.AIのFunction Callingに適用する場合は、対応する型のコンバーターを設定した JsonSerializerOptions を用意し、オプションに渡してあげると良いでしょう。また、Source Generatorを使うと、効率的なJsonConverterを生成してくれます。使用方法は対象の型に[GenerateToonTabularArrayConverter]するだけです!
public IEnumerable<AIFunction> GetAIFunctions()
{
var jsonSerializerOptions = new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
Converters =
{
// setup generated converter
new Cysharp.AI.Converters.CodeDiagnosticTabularArrayConverter(),
}
};
jsonSerializerOptions.MakeReadOnly(true); // need MakeReadOnly(true) or setup converter to TypeInfoResolver
var factoryOptions = new AIFunctionFactoryOptions
{
SerializerOptions = jsonSerializerOptions
};
yield return AIFunctionFactory.Create(GetDiagnostics, factoryOptions);
}
[Description("Get error diagnostics of the target project.")]
public CodeDiagnostic[] GetDiagnostics(string projectName)
{
// ...
}
// Trigger of Source Generator
[GenerateToonTabularArrayConverter]
public class CodeDiagnostic
{
public string Code { get; set; }
public string Description { get; set; }
public string FilePath { get; set; }
public int LocationStart { get; set; }
public int LocationLength { get; set; }
}
この例の場合、CodeDiagnostic[]の件数が多いと、JsonとToonでトークン消費量にかなりの差が出てToonの優位度が高まります。ただし、Toonには得手不得手があるので、特性を見てToonを適用するか(Converterを追加するか)そのままにする(Json)かを選んでいくといいと思っています。
フラットな階層のオブジェクト(プリミティブ, プリミティブ要素の配列, プリミティブ要素のみで構成されたオブジェクトの配列)の生成の場合は、別の属性[GenerateToonSimpleObjectConverter]によりTabularArray + 追加のメタデータといったシナリオに対応できます。
var item = new Item
{
Status = "active",
Users = [new(1, "Alice", "Admin"), new(2, "Bob", "User")]
};
var toon = Cysharp.AI.Converters.ItemSimpleObjectConverter.Encode(item);
// Status: active
// Users[2]{Id,Name,Role}:
// 1,Alice,Admin
// 2,Bob,User
Console.WriteLine(toon);
[GenerateToonSimpleObjectConverter]
public record Item
{
public required string Status { get; init; }
public required User[] Users { get; init; }
}
Json to Toon
ToonEncoder.Encodeは JsonElement から string, byte[] への変換、 IBufferWriter<byte>, ToonWriterへの書き込みをサポートします。
namespace Cysharp.AI;
public static class ToonEncoder
{
public static string Encode(JsonElement element);
public static void Encode<TBufferWriter>(ref TBufferWriter bufferWriter, JsonElement element)
where TBufferWriter : IBufferWriter<byte>;
public static void Encode<TBufferWriter>(ref ToonWriter<TBufferWriter> toonWriter, JsonElement element)
where TBufferWriter : IBufferWriter<byte>;
public static byte[] EncodeToUtf8Bytes(JsonElement element);
public static async ValueTask EncodeAsync(Stream utf8Stream, JsonElement element, CancellationToken cancellationToken = default);
}
IBufferWriter<byte>のオーバーロードを用いるとUTF8で直接データを書き込むため、string変換を介すよりもパフォーマンスが高くなります。
EncodeではJsonElementがarrayの際に、TabularArrayかInlineArrayかNonUniformArrayかどうかを全件チェックしてから書き込みしますが、JsonElementがarrayかつ、全ての要素の出現順序が等しく、全てがプリミティブ(Array, Objectではない)であることを保証できる場合は EncodeAsTabularArray メソッドを用いると検査を省くため、より高いパフォーマンスで変換できます。
namespace Cysharp.AI;
public static class ToonEncoder
{
public static string EncodeAsTabularArray(JsonElement array);
public static void EncodeAsTabularArray<TBufferWriter>(ref TBufferWriter bufferWriter, JsonElement array)
where TBufferWriter : IBufferWriter<byte>;
public static byte[] EncodeAsTabularArrayToUtf8Bytes(JsonElement array);
public static async ValueTask EncodeAsTabularArrayAsync(Stream utf8Stream, JsonElement array, CancellationToken cancellationToken = default);
public static void EncodeAsTabularArray<TBufferWriter>(ref ToonWriter<TBufferWriter> toonWriter, JsonElement array)
where TBufferWriter : IBufferWriter<byte>;
}
というのが基本的な変換の仕様になっています。
まとめ
この記事は、C# Advent Calendar 2025に特にエントリーしていない記事ですが、時期的にはだいたいそんな感じです。
このToonEncoderは、Cysharp/CompilerBrainという全然まだできてないC# Coding Agentのパーツとして用意しました。結構データ大量にドカドカするので節約したいなあ、と思い……。そんなわけで来年初頭はCompilerBrainやっていきます、多分……!
ところで改めて正直なところTOON自体は別に全然いいフォーマットとは思えません。というかどちらかといえば相当厳しい……。が、まぁマーケティング的にJSON互換でDrop-in replacementというのが響いたのはありそうだし、実際CSVだと厳しいっちゃあ厳しいので、とりあえず仕様があるという点で妥協として悪くないといえば悪くない選択かもしれません。
複雑なデータをシリアライズする気はない、ということが[GenerateToonTabularArrayConverter]と[GenerateToonSimpleObjectConverter]に現れています。これはAnalyzerも兼ねていて非対応なネストしたプロパティとか持たせようとするとコンパイルエラーにするという、ようはToonのサブセットみたいなものを疑似的に作り出しているんですね。もちろんJsonElement経由のメソッドを呼べば、ちゃんとネストしたプロパティとかはシリアライズできます。一応用意されている公式のテストスイートには(意図的にサポートしていない機能を除いて)全件合格しています。
またライブラリ名の通り、Encodeしかサポートしていません。Decodeはできません。LLMに送信するためのものなのでだから、デコードは別にいらないでしょう。
といった感じで色々と手を抜いたコンパクトさもあるのですが、それなりに実用的にはなっているので、興味ある方は是非是非試してみてください!