2026年5月1日金曜日

古い.NETで新しいC#の機能を使う(Polyfill)

 C#は日々進化していて、年々便利な機能が追加されて行っています。しかし、一部のC#の新機能は.NETランタイムのサポートが必要なものもあり、特にライブラリの開発だったりすると古めのターゲットフレームワークを指定しなければならないこともしばしばあるため、その「便利な新機能」が使えないことがあります。

一部の機能は公式よりライブラリが提供されることがあり、それを追加することで対応できます。例えば、C#7で導入されたタプルはSystem.ValueTuple、C#7.2で導入されたSpan<T>はSystem.Memoryを入れることで比較的古いフレームワークにも対応させることができます。しかし、C#8.0(.NET Core 3.0)で導入されたIndex/RangeやNullableのアノテーション属性などは公式ライブラリが無く、サポートされません。

PolySharp

そのような隙間を埋める非公式ライブラリ、通称PolyfillがC#(.NET)にもあります。Polyfillって私も最近知った用語なのですが、もともとはJavaScriptで発達した概念みたいですね。

いろいろなライブラリがあるようですが、有名どころはこの2つでしょうか。

いずれもコードジェネレーターとして動作するので、これらを導入したところで出力アセンブリに変化はありません。

今回はPolySharpの使い方を備忘録的に残していきたいと思います。

導入

Nugetからインストールするだけです。

Nullableアノテーション属性(.NET Core 3.0~)

Nullableアノテーション属性とは、参照型のNull許容性を静的に示すための属性です。コンパイラがこの情報をもとに異常なNull状態があるかどうかを判別するためのものです。

代表的な使用例としてはTryParseでしょうか。例えばIPAddress.TryParseメソッドはパースに成功した際はtrueを返し、かつ第2引数のout IPAddress? addressは非nullとなります。しかし、falseを返した時はnullになりますので、全体としてはnullの可能性があるということで'?'が付いています。これを「常にnullの可能性がある」扱いしてはかなり不都合が大きいので、属性を付けて静的解析の助けにするわけです。

public static bool TryParse(string input, [NotNullWhen(true)] out string? result)
{
    if(string.IsNullOrEmpty(input)) {
        result = null;
        return false;
    } else {
        result = input;
        return true;
    }
}

PolySharpさえ導入すれば、何も気にせずに古いバージョンでも動くようになります。ライブラリの公開メソッドにも害なく使えます(コンパイラが見るだけなので)。

Index / Range(.NET Core 3.0~)

上のNullableアノテーション属性はPolySharpを入れるだけで使えるようになっていましたが、Index / Rangeの導入には一癖あります。

まず、Index / Rangeを使うためにはValueTupleが必要です。.NET Framework 4.6.2以前では最初に述べた通りパッケージの導入が必要です。

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

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFrameworks>net462;net470;net10.0</TargetFrameworks>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
    <LangVersion>14</LangVersion>
    </PropertyGroup>

  <ItemGroup Condition="'$(TargetFramework)' == 'net462'">
    <PackageReference Include="System.ValueTuple" Version="4.5.0" />
  </ItemGroup>
  
    <ItemGroup>
      <PackageReference Include="PolySharp" Version="1.15.0">
        <PrivateAssets>all</PrivateAssets>
        <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      </PackageReference>
    </ItemGroup>

</Project>

さらに、Rangeを動かすためにはRuntimeHelpers.GetSubArray()メソッドも必要になります。ソースコードはここにありますが、これをそのままちゃんと動かそうとすると面倒なので、簡略化したコードにしておきましょう。

#if NETFRAMEWORK || !NETCOREAPP3_0_OR_GREATER
namespace System.Runtime.CompilerServices
{
    internal static class RuntimeHelpers
    {
        public static T[] GetSubArray<T>(T[] array, Range range)
        {
            var (offset, length) = range.GetOffsetAndLength(array.Length);
            var result = new T[length];
            Array.Copy(array, offset, result, 0, length);
            return result;
        }
    }
}
#endif

これでちゃんとIndex / Rangeが動くようになりました。

int[] array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
var range = array[5..];

Console.WriteLine(string.Join(", ", range));
5, 6, 7, 8, 9

Init-Onlyプロパティ(.NET 5~)

C#9.0/.NET5で導入されたInit-Onlyプロパティは、実体としてはただのsetアクセッサで、コンパイラが初期化時以外に触れないようにチェックしているに過ぎません。そのsetアクセッサにはIsExternalInitクラスという属性のようなものが付けられており、これによってinitプロパティであることを示しています。

余談ですが、属性のようなものであって属性ではないのは、上のNullableアノテーション属性とは異なって、「解釈できない場合は触らないで」というタイプのものだからです(解釈できないまま触られたらsetアクセッサとなってしまい、initよりも緩い制約になってしまいます)。ですので、modreq+IsExternalInitという特殊なマーキングを行っていて、このマーキングが解釈できない古いコンパイラでは勝手に触らないでもらうようにしているようです。ですが、役割としてはただのマーキングなので、クラスを定義するだけで古いバージョンのフレームワークで使えるようになります。

internal class Program
{
    static void Main(string[] args)
    {
        var test = new TestClass() { TestProperty = 1 };
    }
}

public class TestClass
{
    public int TestProperty
    {
        get; init;
    }
}

Record型(.NET 5~)

上記のInit-Onlyプロパティが使えるようになったことを受けて使えるようになります。 

Required修飾子(.NET 7~)

プロパティやフィールドにrequired修飾子を付けることによって、初期化時の代入を強制できます。null非許容参照型のプロパティをコンストラクタで初期化しなくて良くなるので便利ですね。

これも実体はRequiredMemberAttributeなので、PolySharpでその属性が実装されています。

internal class Program
{
    static void Main(string[] args)
    {
        var test1 = new TestClass() { Name = "Test1" };
    }
}

public class TestClass
{
    public required string Name { get; set; }
}

まとめ

少しIndex / Rangeに癖がありましたが、主要な機能は使えることがわかりました。

もともとPolyfillを入れなくても使える機能や、公式にパッケージが提供されているものもあり、今回紹介した機能がPolyfillでカバーできるのならば割と充分なのではないでしょうか。 

0 件のコメント:

コメントを投稿