0
1

Unity プロジェクトで正しく #nullable を有効化する方法

Last updated at Posted at 2024-07-22

Nullable(ヌル許容値型/ヌル許容参照型)の有効化と言っても、全ての .cs ファイルに #nullable enable を入力して回るとか Unity コンソールに警告が出るようにしようって話ではないです。

👇 #nullable enable の入力無しで目指すもの

ヌル許容と非許容間のやり取りやヌルチェックに対して構文解析と診断が行われた結果

既知の方法では無理

Unity ではアセンブリのコンパイル時に読み込まれる csc.rsp という設定ファイルを使って処理内容を調整する機能があります。(rsp = response)

csc(C Sharp Compiler)に対するコマンドラインフラグはこのファイルに書き込むことでコンパイル時に適用されるようになります。

コンパイル時に適用、つまりヌル許容性のフラグ /nullable:enable を csc.rsp に書き加えても Unity のコンソールに大量の警告が表示されるだけであり、IDE(Visual Studio)上でコーディングしている場面では適用されない(警告の下線が表示されない)という事です。

.csproj の更新でしょ?

さて、ネットで検索すると .csproj 内の PropertyGroup<Nullable>enable</Nullable> を追加すればプロジェクト内の全ての .cs ファイルに対して Nullable が有効化される旨の記載がありますが、この方法は Unity + Visual Studio という開発環境では使えません

(Visual Studio のエディター上で Nullable 警告の下線を表示することが出来ません)

ただ、出来るという情報が多く見つかるので Visual Studio 最新版(VS 2022 = v17)より前のバージョンには「動作してしまうバグ」があった可能性? もしくは Unity + JetBrains Rider の話をしている?

SDK-Style と Legacy-Style

入力した内容が .csproj ファイルの仕様上間違っていないのに反映されない理由は、Unity の書き出す .csproj ファイルが legacy-style と呼ばれるフォーマットだからです。

Legacy-style では Nullable プロパティーは手書きで追加しても .csproj を後処理で書き換える系のエディター拡張を使っても Visual Studio の仕様として反映されません。過去の .csproj 資産すべてが破壊されかねないため今後も対応する予定は無いそうです。

Visual Studio Editor パッケージの変更履歴に SDK-Style に対応とありますが、実際には対応していません。おそらく VS Code に対応するために Sdk タグを使ったことを SDK-Style に対応したと記載しているだけだと思われます。

無理矢理どうにかする方法

Unity + Visual Studio で開発を行う場合、Nullable を有効化するには全ての .cs ファイル内で明示的に #nullable enable を指定する必要があります。が、面倒ですね!

面倒もモチロンですが #nullable enable は .cs ファイル単位で働くので、string.IsNullOrEmpty 等の外部アセンブリ由来の API が意図した動作をしないという問題もあります。

string.IsNullOrEmpty でヌルチェックした後もヌル警告が出てしまう)

エディター拡張を使わない方法

ではどうすれば良いのかというと、<Project Sdk=""> と SDK に空文字を渡してやればオッケーです。

<Project Sdk><Project Sdk="NotFound"> ではダメで空文字にする必要があります。
また、残念ながら .props .targets ファイルで Sdk="..." を指定しても反映されないので .csproj ファイルを更新する必要があります。

副作用として「プロジェクトが正しく構成されていません」的な警告が表示されますが、開発中であればまあ問題はないでしょう。

全てのファイルに #nullable enable を書き込むのが避けられるのはデカいです。コンパイルの結果として nullable 警告が欲しいのではなくコーディング中にそれを示して欲しいわけですから。

Unity 向けエディター拡張

警告を表示したくない場合「何もしない」SDK を使用するのが手っ取り早いです。以下の URL から諸々良きように設定する Unity 向けエディター拡張を導入できます。

Unity パッケージマネージャー用インストール URL(git)

https://github.com/sator-imaging/Csproj.Sdk.git?path=Unity

導入後は .csproj が SDK-Style として書き出されるようになるので、Directory.Build.props ファイルを通して Nullable を有効化します。

(ファイルが存在しない場合はエディター拡張が Nullable を有効化する為のファイルを自動で作成します)

エディター拡張の有無に関わらず既定で読み込まれるファイル群

これらのファイルが存在する場合、.csproj 内に Import タグが存在しなくても Visual Studio(MSBuild)によって自動的に読み込まれます。

  • プロジェクトまたは親フォルダー内
    • Directory.Build.props
    • Directory.Build.targets

ファイル拡張子 .props は .csproj ファイルの早い段階、.targets はファイルの末尾に取り込まれることを示すためだけのモノであり、ファイルフォーマット自体は .csproj と完全に同じもの。

エディター拡張にはビルド/デバッグ時のみ Unity の書き出すオリジナルの .csproj に戻す機能もあります。この機能によって作業中は SDK スタイル、ビルドマシンでは old スタイルを使うことが可能になり、なんか良く分からないエラーが起きることを防止できます。

(が、基本的にどっちでも変わらないと思います。.csproj に関する詳細は後述)

その他のエディター拡張の動作設定は Unity Editor > File > C# Project メニューから変更できます。

ProjectSettings フォルダーにエディター拡張の設定ファイルがあるので、それを Git 管理しないとビルド時に適切に反映されません。

--

もし既存の .csproj 向けエディター拡張と競合を起こすようなら、

UnityCsProjectConverter.CallbackOrder = 0;  // or any

を良い感じに設定すると解決できます。

適切な MSBuild SDK を作るのは結構難しい

過去ログ(失敗例)

残念なことに Legacy スタイルを SDK スタイルに変更した後は .csproj の事前処理済みファイルの行数が 8,663 -> 9,733 と増えて読み込みに時間がかかるようになりました。体感1~2秒ほど。実行中+実行結果は変化なし。

動作設定の Use Empty SDK Attribute を使うとめっちゃ早く読み込めますが、代償としてデバッグ実行が出来なくなって起動時に警告バナーが表示されるようになります。この場合の行数は 8,784 でした。

.cs ファイルが一つしかないアセンブリのプロジェクトファイルの行数は 8,559 です。基本的に事前処理を通すと 8,000 行程度の .csproj に化けます。

数千行から本当に必要なものだけを導き出した結果が Csproj.Sdk.Void て事ですね。

公式SDK?

MSBuild と .csproj はちょっと触ってみようか、で扱うにはあまりに魔境過ぎるので、是非とも公式な Legacy → SDK スタイル変換用の SDK を用意して欲しい所です。

数年前に対応の予定なしと閉じられた issue ですが、新しく開いた issue は即閉じられることなくアサインが行われたので、今後 Csproj.Sdk.Void に代わる何かしらの公式 SDK が来るかもです。ワンチャンあります。嬉しいですね!

デバッグモードを有効化する

Nullable を正しく有効化する、と言いながら SDK-Style だと Visual Studio の「Unity にアタッチ」機能が働きません。そしてこの機能は VSTU 側でハードコードされてる感があり対処のしようが無さそうです。

なので、エディター拡張には Unity がデバッグモードの間は .csproj ファイルを old-style に戻す機能があります。

が! Visual Studio の Tools for Unity 環境設定でプロジェクトの自動再読み込みをオフにしておかないと、VS を開いた状態で Unity をデバッグモードに変更した際、稀に再読み込みが無限ループすることがあります。

まあ Unity にアタッチ機能の存在は殆どの人が忘れてる(?)と思うのであまり影響はないでしょう。

UnityEngine.Object を Nullable に対応させる方法

せっかくプロジェクト全体で Nullable を有効化したとしても、UnityEngine.Object にはヌルなのにヌルじゃない問題があるのでそのメリットを享受することができない。

👇 どうするか。

つかいかた

?? 演算子

かなり理想的で .Nullable() が付くだけ。しかもパフォーマンスが TryGetComponent よりも良い。

// NullableUnityObject<T>? は Nulable<NullableUnityObject<T>> という構造体
var rb = go.GetComponent<Rigidbody>().Nullable() ?? go.AddComponent<Rigidbody>();
var rb = go.GetComponent<Rigidbody>().Nullable() ?? throw new NullReferenceException();

// C# 的に理想の形  ※ UNT0007 警告/エラーが出る
var rb = go.GetComponent<Rigidbody>() ?? go.AddComponent<Rigidbody>();
var rb = go.GetComponent<Rigidbody>() ?? throw new NullReferenceException();

// いつものやり方
if (!go.TryGetcomponent(out Rigidbody rb))
    rb = go.AddComponent<Rigidbody>();

if (!go.TryGetcomponent(out Rigidbody rb))
    throw new NullReferenceException();

Unity アナライザーの UNT0007UNT0008 をエラーに設定してコンパイルが通らないようにしている場合は特に有用。

.ThrowIfNull(message, innerException)

C# の仕様上どうしても戻り値を受けないとダメだしスニペットで良くね? 感がある。
Visual Studio の「ヌルチェックを簡略化できます」の提案が出なくなるのは利点か。

GameObject go = null;

go.ThrowIfNull();  // 戻り値が NonNull なので受けないと go が nullable のまま
go.name = "BAD";
~~

go = go.ThrowIfNull();  // go = go... すれば以後 Nullable 警告は表示されない
go.name = "OK";

// 現状
if (go == null)
    throw new NullReferenceException();
go.name = "Usual way";

NullReferenceException 以外の例外を投げたい場合は Nullable() を使う。

go = go.Nullable() ?? throw new Exception();

その他

Nullable() は ...OrDefault() 系 LINQ との相性が抜群。(こんなコード書くか?)

var rb = go.GetComponentsInChildren<Rigidbody>().FirstOrDefault()
    .ThrowIfNull("no rigidbody!!");

var rb = go.GetComponentsInChildren<Rigidbody>().FirstOrDefault()
    .Nullable() ?? go.AddComponent<Rigidbody>();

Nullable は地味にプロパティーとの喰い合わせが悪い。

// メソッドは行ける
go.GetComponentsInChildren<Rigidbody>().FirstOrDefault().Nullable()?.Value.SetDensity(0f);

// プロパティーはエラー(ヌルに対して文字列を割り当てようとしていることになる
go.GetComponentsInChildren<Rigidbody>().FirstOrDefault().Nullable()?.Value.name = "test";

// Nullable<NullableUnityObject<T>> を使ったヌルチェックは訳わかんないことになるので、、、
var nullable = go.GetComponentsInChildren<Rigidbody>().FirstOrDefault().Nullable();
if (nullable.HasValue)
    nullable.Value.Value.name = ".Value x2";  // ?.Value は NullableUnityObject<T>.Value を呼び
                                              // .Value は Nullable<T>.Value を呼ぶので2回必要

// ヌルチェックするなら普通に
var rb = go.GetComponentsInChildren<Rigidbody>().FirstOrDefault();
if (rb == null)
    return;
else
    nullable.name = "Not null";

nullable.HasValuenullable != null でも良い。null 許容値型 は int? v = null に対して v > 0 出来たり特別扱いされている。しかし ?.*** だと Nullable<T> が抱えた構造体のメンバーを呼ぶって処理はちょっと分かり辛いね?

??= 演算子

??= オペレーターは implicit operator NullableUnityObject<T>(T value) を定義すれば使えそうだが、変な使い方を防ぐために NullableUnityObject<T> を使い辛くするべきという点で不要だろう。

class Test : MonoBehaviour
{
    NullableUnityObject<Rigidbody> field;

    void Start()
    {
        // 構造体をフィールドにしても余計なボックス化が起きるだけでメリットはない。
        field ??= gameObject.AddComponent<Rigidbody>();

        // ローカル変数として抱えることもできるが意味がない
        var nullable = GetComponent<Rigidbody>().Nullable();
        nullable ??= gameObject.AddComponent<Rigidbody>();

        var rb = nullable.Value;
    }
}

TryGetComponent とのパフォーマンス比較

テストコード
public class NullableLooper : MonoBehaviour 
{
    void Update()
    {
        for (int i = 0; i < 1000; i++)
        {
            var tf = transform.Nullable() ?? new GameObject().transform;
        }
    }
}
public class TryGetLooper : MonoBehaviour 
{
    void Update()
    {
        for (int i = 0; i < 1000; i++)
        {
            if (!TryGetComponent<Transform>(out var tf))
            {
                tf = new GameObject().transform;
            }
        }
    }
}

  • Nullable: 0.120 ~ 0.180ms
  • Nullable (NoInlining | NoOptimization): 0.120 ~ 0.200ms
    • ※ 拡張メソッドにのみ MethodImpl 属性を付与
  • TryGetComponent: 0.180 ~ 0.230ms
  • ※ サンプルを TransformMeshFilter に変更
    • Nullable: 0.200 ~ 0.230ms
    • TryGetComponent: 0.180 ~ 0.230ms(変わらず)

※ 手作業での確認。全ての手法でドカンとスパイクが出るタイミングがあるがそれらは除外。GC Alloc は全ての手法で 0B。

毎フレーム 1,000 回、Nullable<T> とそれに包まれる NullableUnityObject<T> の二つの構造体を作っている Nullable() の方がパフォーマンスが良いのは面白い。そして確実に存在する Transform を、存在しない可能性がある MeshFilter に変えると遅くなる不思議。

とはいえ Update() 内で 1,000 回のループを回して 1ms 以下のスケール。そして初期化時にのみ実行されるなど頻度の低いヌルチェックなので、本当に構文上の満足感が得られるだけとも言える。その単なるソースコードの見た目・書き味の為にパフォーマンスを犠牲にしなくても良いのは僥倖か。

LINQ を良く使うのならスッキリしたコードに出来るので使う意味はあるし、コンポーネント用の Q のようなユーティリティーの場合、例外を投げる拡張メソッドのバリエーションが要らなくなる。

var animator = prefab.Q<Animator>().Nullable() ?? throw new MyAppExcepption();
var animator = prefab.Q<Animator>().ThrowIfNull();

結構良いかもね?

UI Toolkit「Q<T>」の MonoBehaviour 版

https://gist.github.com/sator-imaging/a4c369a12f6d964ebad415997c06c2b6

注意点

NullableUnityObject<T> は抱えてるオブジェクトが UnityEngine.Object 的にヌルじゃないことを前提にしているので、クラスのフィールドや辞書などのコレクションに抱えた場合はトラブルになる可能性がある。

理想は構造体を ref struct NullableUnityObject<T> にしてスタック外への持ち出しを禁止することだが、ref struct は C# の仕様上ジェネリックの型引数に指定できず。。。

ただ、構造体には [EditorBrowsable(EditorBrowsableState.Never)]Obsolete アトリビュートを付けているのでそもそもオートコンプリートに表示されず、直打ちした場合も鬱陶しい下線が表示されるので狙ってやらない限りは問題とならないだろう。

NullableUnityObjectSlim<T>

実装するメンバー関数を減らしても特にパフォーマンスに影響はなかった。変な使われ方をした際の悪影響を考えるとリスクの方が大きい。

public readonly struct NullableUnityObjectSlim<T>
{
    readonly T value;

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    internal NullableUnityObjectSlim(T value)
    {
        this.value = value;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static implicit operator T(NullableUnityObjectSlim<T> self) => self.value;
}

TODO

リスト
  • Nullable() メソッド名の別案
    • unityObj.Exists() ?? throw new ...(良さげだが動詞はブーリアン返しそう
    • unityObj.NullCheck() ?? throw new ...(ブーリアン返しそう/例外投げそう
    • unityObj.NotNull() ?? throw new ...(Nullable で良い
    • unityObj.Validate() ?? throw new ...(検証って何を?
    • unityObj.AsIs() ?? throw new ...(合ってはいる。厳密には AsIs()?.Value だけど
    • unityObj.As() ?? throw new ...(微妙に意味が分からない
    • unityObj.Is() ?? throw new ...(存在に対する問いかけ
    • unityObj.Be() ?? throw new ...
    • unityObj.\u004e() ?? throw new ...(== .N()
  • 拡張プロパティー(C# 12.0) https://ufcpp.net/blog/2023/3/extensions/

--

ヌル合体演算子のオーバーライド(当然 Unity 絡みのリクエストw)

https://github.com/dotnet/csharplang/discussions/2020

EqualityComparer<T>.Default を差し替えて処理を Nullable-aware に変えられたりしないだろうか。副作用が怖いが。

.csproj について

色々と書いてますが、基本的に .csproj はカスタマイズしない方が良いです。というのもやろうとして失敗したからです。

Unity は dotnetmsbuild コマンドを使ってアセンブリをコンパイルしておらず、.asmdef で設定された内容から .rsp を作って csc に渡してコンパイルしている(と思われる)ので、.csproj での変更はビルドには反映されません。

例えば上記の Nullable 拡張メソッドを暗黙的に読み込み、Unity プロジェクト内の .asmdef ファイルの編集ナシに使えるようにしたい場合は Directory.Build.props

<ProjectReference Include="UnityObject.NullableExtensions.csproj" />

を追加すれば Visual Studio では意図通り機能します。

が、Unity のアセンブリのビルドでは「○○が見つかりません」エラーが出てコケることになります。なので Unity のビルド向けに Assets/csc.rsp を作って

-r:Library\ScriptAssemblies\UnityObject.NullableExtensions.dll

を追加することで整合性をとる必要性があります。

Assets/csc.rsp の内容は全ての .csproj ファイルの生成に影響を与えるので、この例なら .rsp の追加だけで十分)

※ ビルドマシンの場合は Unity エディターがアセットインポート時に作成する Library フォルダーが存在しないのでエラーでコケます。

Unity の書き出す .csproj はあくまで「Visual Studio を動作させるために必要だから」作られています。

なので、.csproj / msbuild には意外と便利な機能があるんですが、IDE 向けの構文解析アナライザーや nullable の有効化等々、Unity のビルド結果に影響を及ぼさない範囲での利用に留めるのが良いでしょう。

おススメ

アナライザーが報告する警告をエラーに昇格させてビルドが通らないようにしたい、そういった用途の場合は別ですが、単に IDE 上で構文解析/診断を実行したいだけであれば Unity プロジェクトに .dll を追加する必要はありません。

以下のような内容を Directory.Build.props に追加すれば勝手に nuget.org からダウンロードしてくれるので、共有ストレージの .dll ファイルを管理する手間も無くなります。マクロと組み合わせればアナライザーの解析対象となるプロジェクトを絞り込むことも可能です。

<PropertyGroup>
    <!-- 多分必要 -->
    <RestoreProjectStyle>PackageReference</RestoreProjectStyle>
</PropertyGroup>

<!-- キャッシュフォルダーになければ nuget.org から勝手にダウンロード! -->
<ItemGroup>

    <!-- ErrorProne.NET -->
    <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2">
        <PrivateAssets>all</PrivateAssets>
        <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2">
        <PrivateAssets>all</PrivateAssets>
        <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>

    <!-- const より static readonly 派 -->
    <PackageReference Include="SatorImaging.StaticMemberAnalyzer" Version="1.4.0">
        <PrivateAssets>all</PrivateAssets>
        <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>

</ItemGroup>

--

Nuget.org からアナライザーを持ってくる場合はパッケージ名の下のタブで PackageReference を選んで内容をコピペします。独自の設定項目が無いと適切に動作しないこともあるのでリポジトリをよく確認しましょう。

Directory.Build.props を更新しても反映されないことがあるので、プロジェクト > NuGet パッケージの管理 メニューから適切に読み込まれているか確認する。もし読み込まれていないなら、

  • プロジェクトフォルダーの .csproj を全て削除して生成しなおす
  • 読み込まれていても解析が行われないなら 分析 > Code Analysis の実行 メニューを試す
  • PC を再起動する

その他おススメ 👇

.NET 5+ Analyzer

C# 公式の最新アナライザーです。.NET 8 のモノを読み込んでも Visual Studio 上で普通に動きます。前述の通り Unity のコンパイル時には反映されないので注意してください。

(反映させたい場合は Unity に対応したバージョンの Roslyn を使用している .dll をダウンロードして適切に設定します)

残念ながら使っている SDK が公式の .NET じゃない場合は CA1008 みたいな有用そうな解析が実施されません。何故。。。(表に記載のない隠しパラメーターっぽいのも意味をなさず)

<!-- .NET 5+ Analyzer -->
<PropertyGroup>
    <EnableNETAnalyzers>true</EnableNETAnalyzers>

    <!-- https://learn.microsoft.com/ja-jp/dotnet/core/project-sdk/msbuild-props#analysislevel -->
    <AnalysisLevel>latest</AnalysisLevel>  <!-- 5 とか 8 とか latest とか -->
    <AnalysisMode>Recommended</AnalysisMode>
</PropertyGroup>

<ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0">
        <PrivateAssets>all</PrivateAssets>
        <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
    </PackageReference>
</ItemGroup>

Banned API Analyzer

$(ProjectDir) の末尾にパス区切り文字が含まれているのでバックスラッシュは要らないです。加えると mac に持っていった時に動かなくなるかもです。

<!-- Banned API Analyzer -->
<ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
        <PrivateAssets>all</PrivateAssets>
        <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
    </PackageReference>

    <!-- https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.BannedApiAnalyzers/BannedApiAnalyzers.Help.md -->
    <AdditionalFiles Include="$(ProjectDir)BannedSymbols.txt" />
</ItemGroup>

--

その他有名どころのアナライザーは公式サイト参照のこと。

.csproj で使えるマクロ

プロジェクトファイルごとにカスタマイズする(MSBuild)

ココで言うプロジェクトファイルとは .csproj の事です。Unity ではアセンブリ(.asmdef)毎に .csproj ファイルが作られます。

Unity の .csproj を扱う場合は Unity 上のエディター拡張をゴリゴリにカスタマイズするよりも、msbuild のお作法を覚えたほうが効率が良いでしょう。ドキュメント化されていないだけで結構色んなことが出来ます。

例えば Unity のエディター拡張から書き出される .csproj に手を加えずに、特定のファイルでのみ有効になる処理を行いたい場合は以下のようにします。

例)Directory.Build.props(次項参照)

<Project>

    <!-- 条件があった時のみプロパティーを設定する -->
    <PropertyGroup Condition=" '$(MSBuildProjectName)' == 'CsprojFileNameWithoutExtension' ">
        <Something>true</Something>
    </PropertyGroup>

    <Import Project="SharedPropertiesForAllAssemblies.props" />

    <Import Condition=" '$(Something)' == 'true' " Project="NotAppliedToAllAssemblies.props" />
    <Import Condition=" '$(Something)' == 'true' " Project="OtherSpecificProperties.props" />

</Project>

バッチコマンドやシェルスクリプトに近いお作法で、プロパティーが未定義で空白文字だった場合のエラーを避けるために ' で囲います。" 前後のスペースは公式ファイルが良くやっているスタイル。

.csproj に対しては特定のアセンブリーにのみ適用したい、程度のカスタマイズしかすることは無いと思います。Condition だけ覚えておけば十分でしょう。

既定で定義されているプロパティーも色々ありますが、こちらも MSBuildProjectName だけ覚えておけば良いでしょう。

.csproj(MSBuild?)の仕様/挙動

Unity プロジェクトフォルダーまたはその親フォルダーに

  • Directory.Build.props
  • Directory.Build.targets

というファイルを置いておくと、MSBuild / Visual Studio が(ビルド時以外も)全ての .csproj に自動的に内容をインポートしてくれます。

この MSBuild の仕様は Unity プロジェクトでも有効です。

.props はプロジェクト読み込みの早い段階、.targets は読み込みの最終段階でインポートされます。

.csproj の拡張機能が使えるかに関してはプロジェクトファイルが sdk と old、どちらのスタイルになっているかに依存します。つまり Directory.Build.props の内容を以下のようにしても .csproj は SDK-style として処理されないという事です。

<Project Sdk="">

    <PropertyGroup>
        <Nullable>enable</Nullable>  <!-- 効かないよ! -->
    </PropertyGroup>

</Project>

注)Unity が最終的にどうやって .dll を書き出しているのかは、ふわっとしたドキュメントしか存在しないので不明です。ビルドに関わることを .props .targets に書き込んだ場合はビルド結果に反映されるかを確認する必要があります。

2つのスタイルの読み込み方の違い

SDK-Style では指定された SDK の SDK.props SDK.targets を暗黙的に読み込む、という動作をします。

<Project Sdk="MyApp.Shared">
    ...
</Project>

👇 この .csproj は暗黙的に以下のように変換されます。

<Project DefaultTargets="Build">
    <Import Project="<SdkPath>/Sdk.props" Sdk="MyApp.Shared" />

    ... 元の .csproj に含まれていた内容

    <Import Project="<SdkPath>/Sdk.targets" Sdk="MyApp.Shared" />
</Project>

前出のエディター拡張を導入すると変換結果を確認することが出来ます。

結局は old スタイルに変換されるんですが、SDK スタイルから変換された old-style でのみ Nullable 等の拡張機能が有効化されます。過去の資産が全て壊れることになるので、old-style の読み込み/保存の挙動は今後一切変更されること無いそうです。

NuGet パッケージ関連

NuGet パッケージのパック時のみ読み込まれるプロパティーが存在する。

  • Directory.Packages.props (NuGet.props)

.csproj で使えるプロパティー一覧

おわりに

#nullable 便利ですよね。XML ドキュメントを書いたり雑に GetValueOrNull とかいうメソッド名にしなくてもオッケーになります。

(使用頻度が低かったりリスクがあったりするパブリック API の場合は、GitHub 等の IDE 以外の環境でもその挙動が分かりやすくなる Get...OrNull Get...OrThrow って名づけは存外悪くはない)

が、真価を発揮するには自分の書くもの含め、既存の全ての API を nullable 対応しないと意味がないし呼び出し側も #nullable する必要があります。今回の対応は Unity + Visual Studio でモダンな C# を書く上で絶対に必要なわけです。

C# 高速化の一環で重複したヌルチェックを取り除いて効果があった(ソース失念)とのことなので、全ての API が Nullable に対応して根っこから末端まで適切にヌル許容性を伝播させ、「エディター上で」警告が出る環境を整えて初めて意味が出る。
Nullable を有効化して慣れてきたらこの辺りを履修して自分の API もヌル許容性に対応していきたいですね。

https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/attributes/nullable-analysis

--

ヌル許容性のほかには取得処理が失敗したときに例外を投げるのかヌルを返すのか問題があるけど、ヌル許容性が全てが整ったならローレベル API は失敗したら必ずヌルを返すように統一、利用者側に例外を投げるか別の処理をするのかを任せることが出来るようになって色々とスッキリする。素晴らしい。

でも失敗したらどうなるか分からない問題の根本的な原因は、Unity が使っている C# 公式由来のライブラリから XML ドキュメントが取り除かれてしまっている点にある気はする。

メソッドのポップアップで ArgumentException があれば例外を投げるってのは分かるわけだし、そもそも C# 公式 API の Get... 系メソッドは失敗した場合にヌルを返すのか例外を投げるのか、XML コメントに書いてないことの方が少ない。

Unity 開発だとそれらが全て消滅しているから C# 公式のリファレンスソースを読まなきゃいけないし、そももそどのタイミングのソースからビルドされているかも分からないっていう。XML コメントは多言語対応してるし(?)アセンブリのサイズを小さくしたいから消してるんだろうけど、今後は英語版だけでも残してほしいわ。

--

対象フレームワークに現存する全ての .NET 関連フレームワークを含んだ .nupkg を作るのに苦労しました。意味があるかは分かりませんが。ともあれ二度とビルドシステム周りは触りたくないですね。週末返して。

以上です。お疲れ様でした。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1