xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

.NET 6 以前の環境下で C# 11 の required キーワードを利用する

C# 11 / .NET 7 で required キーワードが利用できるようになりました。詳細は公式ドキュメント等に譲りますが、簡単に説明するとプロパティやフィールドのオブジェクト初期化子で値を代入することを強制する機能です。

// こういうクラスがあるとして...
public class Person
{
    public required string Name { get; init; }
    public int? Age { get; init; }
}

// Name プロパティを初期化していないのでエラー
var p = new Person();

// これは OK
// Age プロパティの初期化は強制されていない
var p = new Person(){ Name = "xin9le" };

これまでプロパティの初期化漏れを防ぎたい場合はコンストラクタ引数を経由しなければなりませんでした。ですが、コンストラクタを毎度作るなんて手間だなぁと思うのが人情 (?) ってもんです。ということで C# 11 からオブジェクト初期化子でも必須性を表現し、コンパイルエラーとすることが可能になりました。便利過ぎる。

.NET 6 以前でも利用したい要望がある

この required キーワードは通常は .NET 7 でないと利用できません。と言うと次のような声が聞こえてきそうです。僕には聞こえてきました。

えー!.NET 7 じゃないとダメなの?LTS な .NET 6 のままがいいんだけど required だけでも使いたいわー。
ウチの会社 .NET Framework 2.0 やねん...。C# のバージョンだけは最新だけど!

なります。ということでやっていきましょう。

reqruied キーワードの展開のされ方

C# コンパイラが requried キーワードをどのように解釈しているのかを確認していきます。例によって (?) ILSpy を使ってみましょう。すると、以下のように展開されることがわかります。

using System;
using System.Runtime.CompilerServices;

[RequiredMember]
public sealed class Person
{
    [RequiredMember]
    public string Name { get; init; }

    public int? Age { get; init; }

    [Obsolete("Constructors of types with required members are not supported in this version of your compiler.", true)]
    [CompilerFeatureRequired("RequiredMembers")]
    public Person()
    {
    }
}

このうち [RequiredMember] 属性と [CompilerFeatureRequired] 属性が .NET 7 からのみ追加された型で、.NET 6 以前だとこれが不足していることで利用できなくなっています。つまり、これらの属性を独自に定義してしまえばコンパイラを騙す () ことができそうです。

.NET 6 + C# 11 で required を利用する

Step.1 : Visual Studio のバージョンを確認

まず C# 11 が利用可能なコンパイラを搭載している Visual Studio にしましょう。17.4.0 以降であればよいでしょう。

Step.2 : TargetFramework / LangVersion を確認

以下のように .csproj を設定します。今回は TargetFramework = net6.0 としていますが、それよりも古くても大丈夫です。LangVersion11.0 としましょう。

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net6.0</TargetFramework>
        <LangVersion>11.0</LangVersion>
        <Nullable>enable</Nullable>
    </PropertyGroup>

</Project>

Step.3 : RequiredMemberAttribute を追加

以下のように [RequiredMember] 属性を実装します。と言っても特段難しいことはなく、.NET Source Browser を眺めてサクッと持ってきます。

#if !NET7_0_OR_GREATER
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace System.Runtime.CompilerServices;

/// <summary>
/// Specifies that a type has required members or that a member is required.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
internal sealed class RequiredMemberAttribute : Attribute
{ }
#endif

.NET 7 未満の環境でのみ追加したいので #if !NET7_0_OR_GREATER で括っています。また利用したいアセンブリ内にのみ閉じればよい型なのでアクセシビリティを internal にしています。

Step.4 : CompilerFeatureRequiredAttribute を追加

ふたつ目の属性を追加しましょう。こちらも [RequiredMember] 属性と同様の考え方で実装します。.NET Source Browser で検索すれば簡単ですね。

#if !NET7_0_OR_GREATER
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace System.Runtime.CompilerServices;

/// <summary>
/// Indicates that compiler support for a particular feature is required for the location where this attribute is applied.
/// </summary>
[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)]
internal sealed class CompilerFeatureRequiredAttribute : Attribute
{
    public CompilerFeatureRequiredAttribute(string featureName)
    {
        FeatureName = featureName;
    }

    /// <summary>
    /// The name of the compiler feature.
    /// </summary>
    public string FeatureName { get; }

    /// <summary>
    /// If true, the compiler can choose to allow access to the location where this attribute is applied if it does not understand <see cref="FeatureName"/>.
    /// </summary>
    public bool IsOptional { get; init; }

    /// <summary>
    /// The <see cref="FeatureName"/> used for the ref structs C# feature.
    /// </summary>
    public const string RefStructs = nameof(RefStructs);

    /// <summary>
    /// The <see cref="FeatureName"/> used for the required members C# feature.
    /// </summary>
    public const string RequiredMembers = nameof(RequiredMembers);
}
#endif

Build & Run

お疲れ様でした。ここまで準備が整えばビルドが通るようになったはずです。属性をふたつ追加するだけという簡単なお仕事なので、.NET 6 以前のフレームワークをもうしばらく利用する方は是非やってみてください。手元でやってみた範囲では .NET Framework 2.0 でも動作しました

また、特定のフレームワークから追加される型に C# コンパイラが依存することはこれまでもありました。たとえば init キーワードモジュール初期化子も同様なので、過去記事を参考にしてみてくださいませ。