はじめに
昨年は .NET5を使いたい理由6選 という記事でご好評いただきました。ありがとうございました。
今年は .NET6がLTS(Long-term Support)にふさわしいい理由に触れます。
.NET5 では単独では動く機能が、組み合わせだと齟齬を起こしていたものがいくつかありました。それらは今どうなっているでしょうか。
「組み合わせて初めて完成した機能」に着目したほうが面白そうだと考えました。
.NET6 までまともに使えなかった組み合わせ
- アセンブリのトリミング × System.Text.Json
- Blazor コンポーネント × ジェネリック型制約(where)
System.Text.Json の機能拡張はLTSにふさわしい機能の代表
- アセンブリトリミング × System.Text.Json × ソースジェネレーター
アセンブリトリミング機能のぬぐい切れない問題を解消する道
https://docs.microsoft.com/ja-jp/dotnet/core/deploying/trimming/trim-self-contained
最初に .NET Core 3.0から段階的に導入・強化されたていったトリミング機能について触れるのは適切でしょう。
これは .NET 5 のBlazor WebAssembly などでダウンロードサイズ削減などに貢献しました。
しかし魅力的な機能でありながら、正しく扱うのはなかなか難しい機能でもありました。
(.NET Core 3.0のドキュメントはこちら https://docs.microsoft.com/ja-jp/dotnet/core/whats-new/dotnet-core-3-0#assembly-trimming )
実際の動作は「コンパイル後のIL(中間コード)を解析して、到達しないところをデプロイしない」という単純な内容ですが、この方法ではリフレクションで到達するコードを認識することができません。
具体的な問題点はここに列挙されています。
https://docs.microsoft.com/ja-jp/dotnet/core/deploying/trimming/incompatibilities
例えばXAMLでデータバインディングするWPF、内部でCOMを使うWindows Forms、そしてリフレクションを用いるシリアライザなどが代表的でしょう。またC#界隈では主流ではありませんが、外部のテキストに定義するタイプのDI(依存性注入)も動きません。
特にシリアライザの問題は深刻で、シリアライズに使うクラスが、IL上では非到達であるという理由で削除される可能性があるため、書き方に気を遣う必要が生じます。
要するにデプロイ後のサービスでJSONを正しく扱うことができるのかが不安視されました。
.NET 6 においてこの不安を完全に払しょくするオプションが追加されました。
System.Text.Json の機能拡張でソースジェネレーターを用いる
シリアライズ/デシリアライズの処理にソースジェネレーターを使うのは最もポピュラーな用例の1つと考えられていましたが、.NET 5 時代の標準的なライブラリ群にはソースジェネレーターを用いるものはありませんでした。
それで最初に System.Text.Json の拡張するのは妥当だったと言えます。現在のWeb APIの多くはJSONで通信しているためです。
https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-source-generation-modes?pivots=dotnet-6-0
メリットとしては
- 実行時のパフォーマンス向上
- アセンブリのトリミングでの安全性の確保(生成コードにより到達しない部分が無くなる。)
デメリットとしては
- コードの増加(起動時間が長くなる)
トリミング機能を十全に機能させるためには、ソースジェネレーターによる拡張が必要であり、標準でサポートしたライブラリ(この場合は System.Text.Json )が実際に登場したことで、初めて個々の機能を安全に使えるようになったと言えます。
今後、各シリアライザは System.Text.Json を手本に拡張していくことになるでしょう。
この部分だけを取り上げても .NET 6 はLTSに相応しいと言えます。
Blazor コンポーネント × ジェネリック型制約(where)
Blazor コンポーネントの .razor ファイルはコンパイル時にパーシャルクラスを出力します。
今までもジェネリック型自体には対応していましたがジェネリック型制約を付けることができませんでした。
.NET 6 からはジェネリック型制約を付けたコンポーネントを作ることができます。
ジェネリック型に特定のインターフェースや基底クラスを要求しなければ書けないコンポーネントも自在となります。
詳しくは以下を参照してください。
https://docs.microsoft.com/ja-jp/aspnet/core/blazor/components/?view=aspnetcore-6.0#generic-type-parameter-support
.NET6 でも使えない組み合わせ
全ての問題が解消したわけではありません。未解消の事案もあります。
Blazor × null許容参照型 × DI
@jsakamoto 氏の記事が本件に詳しいのでリンクします。
- プロジェクト全体としては null許容参照型のオプションを有効にしたい。
- .razor のクラスに依存性注入したいが、コードビハインド用のパーシャルクラスのnull非許容な参照型に代入したい。
- .razor から自動出力するC#のファイルは、null許容参照型のアナライズが適用されないため、警告が消えない。
本件の抜本的対応は .NET 7 のロードマップに含まれています。
https://github.com/dotnet/aspnetcore/issues/5680
現時点ではちょっとトリッキーですが null免除演算子で default!
を代入するのが妥当と思われます。
https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/operators/null-forgiving
これは、実用上問題ないと言えます。
まとめ
今回は特に、今までできなくて困っていたこと。せっかくの機能を活用できなかった点に着目しました。
.NET 5 では不十分だったところのいくつかは確かに是正され、安全性とパフォーマンスの両立を果たしています。
しかしながら、自動生成したコードは null許容参照型関連のオプションが適用されないというアンマッチな部分があります。それでも現時点で残っている問題は比較的軽微なもので、実害の無い回避方法が存在します。
そういえば、null許容参照型もそろそろ自プロジェクトに組み込んでゆきたいと考え、自社ブログにはこんな記事を書いたばかりです。
https://recruit.cct-inc.co.jp/tecblog/csharp/csharp-null/
.NET 6 は LTSに相応しいバージョンに仕上がっています。普及させていきたいですね。
そして、今後の一層の発展を期待したいと思います。