rksoftware

Visual Studio とか C# とかが好きです

C# 2.0 以降の新機能まとめ (C# 10.0 ~ )

 C# 2.0 以降の新機能の名前と公式ガイドページへのリンクをまとめました。(C# 10.0 ~ ) (途中)

 注1)機能の名前はできるだけ公式ガイドから言葉を拾うようにしましたが、完全ではありません。
 注2)リンク先ページはできるだけ日本語ページを拾うようにしましたが、見つけられずに英語のページもあります。
 注3)良さそうなページを探しましたが、もっと良いページがあるかもしれません。

■ C# 10.0 での新機能

・レコード構造体 ( Record structs )
  https://docs.microsoft.com/ja-jp/dotnet/csharp/fundamentals/types/records#how-records-differ-from-classes-and-structs
 以前はクラスでしか作れなかったレコードが構造体でも作れるようになった。ただし標準ではプロパティの値が変更可能。readonly にすることで不変に。 == での比較も可能になる。

{ // レコード構造体 (型の定義は末尾)
    SampleRecordStruct sampleRecordStruct = new(1);
    sampleRecordStruct.Value1 = 2;  // レコード構造体は超順では変更可能
}

{ // レコードクラス (以前からの機能) (型の定義は末尾)
    SampleClass sampleClass = new(1);
    // sampleClass.Value1 = 2; これはエラー。レコードクラスは不変
}

{ // 不変のレコード構造体 (型の定義は末尾)
    SampleReadolyStruct sampleReadonlyStruct = new(1);
    // sampleReadonlyStruct.Value1 = 2; これはエラー
}

{ // レコード構造体の ToString() や == の実装の確認
    SampleRecordStruct sampleRecordStruct = new(1);
    sampleRecordStruct.Value1 = 2;

    var struct4 = sampleRecordStruct with { Value1 = 2 };

    // ToString の結果は「SampleRecordStruct { Value1 = 2 }」フィールドの値も表示するいい感じの実装
    Console.WriteLine(sampleRecordStruct);
    // == で比較ができる。フィールドの値が同じなら True になる
    Console.WriteLine(sampleRecordStruct == struct4); // True
    // != もできる
    Console.WriteLine(sampleRecordStruct != struct4); // False
}

{ // 通常の構造体では ToString() はいまいち。== もできない
    // 位置指定レコードは通常の構造体では使えない
    SampleStruct sampleStruct = new (){ Value1 = 1 };
    sampleStruct.Value1 = 2;

    // with 式は通常の構造体でも使える
    var struct4 = sampleStruct with { Value1 = 2 };

    // ToString の結果は「SampleStruct」構造体の名前だけの実装
    Console.WriteLine(sampleStruct);
    // == はできない文法エラー
    // Console.WriteLine(sampleStruct == struct4);
}

// 以降、型の定義

// レコード構造体
record struct SampleRecordStruct(int Value1);
// レコードクラス (以前からの機能)
record class SampleClass(int Value1);
// 不変のレコード構造体
readonly record struct SampleReadolyStruct(int Value1);
// 構造体
struct SampleStruct { public int Value1; };

・パラメーターなしの構造体コンストラクター ( Parameterless struct constructors)
 https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/builtin-types/struct#parameterless-constructors-and-field-initializers
 構造体でパラメータ (引数) のないコンストラクタが作れるようになった。ただしフィールドの初期化はされるようになっている必要がある。また、インスタンス フィールドまたはプロパティを宣言で初期化することもできる。

struct Sample
{
    public int Value1 = default;  // フィールドを初期化しているのがポイント

    // C# 10.0 未満ではエラー 「CS8773  Feature 'parameterless struct constructors' is not available in C# 9.0. Please use language version 10.0 or greater.」
    public Sample() { }

    // パラメータのあるコンストラクタは以前から OK
    public Sample(int value1) { Value1 = value1; }
}

// これはエラー。フィールドは初期化される必要がある
struct Sample2
{
    public int Value1;  // フィールドを初期化していないのがポイント

    // エラー 「CS0171  Field 'Sample2.Value1' must be fully assigned before control is returned to the caller」
    // public Sample2() { }
}

// これは OK。
struct Sample3
{
    public int Value1;

    // フィールドを初期化しているので OK
    public Sample3() { Value1 = default; }
}

・グローバルな using ディレクティブ (Global using directive)
  https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/using-directive#global-modifier
 どこかの1ファイルで using するだけですべてのファイルで using したことにできる機能。
 デフォルトで System System.Collections.Generic System.IO System.Linq System.Net.Http System.Threading System.Threading.Tasks System.Net.Http.Json などが暗黙に using されたことになっている。
 暗黙の global using を無効にするには .csproj ファイルで ImplicitUsings プロパティを false に設定すると回避できる (暗黙的な global using を無効化できる)。
  また .csproj の設定で暗黙的な global using がされる namespace を足したり除外したりでる。

global using System.Text;  // グローバルな using ディレクティブを宣言

internal class Class1
{
    void Method()
        => Console.WriteLine(new StringBuilder());
}
// using System.Text; していなくとも System.Text.StringBuilder が使えている
internal class Class2
{
    void Method()
        => Console.WriteLine(new StringBuilder());
}
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>true</ImplicitUsings>
  </PropertyGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <Using Remove="System" />     <!-- System 名前空間を暗黙から除外 -->
    <Using Include="System.Text" /> <!-- System.Text 名前空間を暗黙に追加 -->
  </ItemGroup>
</Project>

・ファイル スコープの名前空間 (File scoped namespaces)
  https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/namespace
 ファイルの先頭で名前空間を書くだけでファイル全体に名前空間を指定できる。( { } 不要)
 この機能での名前空間はファイル内で一回しか書けない。該当ファイル内では通常の ( { } のある) 名前空間との併用も不可。

namespace Test1;

class A
{
}

・拡張プロパティのパターン (Extended property patterns)
  https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/operators/patterns#property-pattern
 パターンマッチングの機能強化。プロパティの中のオブジェクトのプロパティをシンプルな記述でパターンマッチングできる。

SampleA a = new();

// プロパティの中のプロパティをシンプルに書ける
if (a is SampleA { PropertyA.PropertyB: 1 })
{
}

// 構造体定義
struct SampleA
{
    public SampleB PropertyA { get; set; }
}

struct SampleB
{
    public int PropertyB { get; set; }

}

・明確な代入分析の改善 (Improved definite assignment analysis)
  https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/attributes/nullable-analysis ( ※いいページが見つけられませんでした )
 変数に値がセットされているかの分析が機能するパターンが増えた。

#nullable enable

C c = new C();
if (c?.M(out object obj3) == true)
{
    obj3.ToString(); // OK
}

if (c?.M(out object obj4) ?? false)
{
    obj4.ToString(); // OK
}
if (c != null ? c.M(out object obj) : false)
{
    obj.ToString(); // OK
}

public class C
{
    public bool M(out object obj)
    {
        obj = new object();
        return true;
    }
}

・汎用属性 (Generic attributes)
  https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/generics/generics-and-attributes
 ( プレビュー機能 )
 属性クラスでジェネリクスが使える。

[Sample<int>]
void Method() {; }

class SampleAttribute<T> : System.Attribute { }

・強化された #line ディレクティブ (Enhanced #line directives)
  https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/preprocessor-directives#error-and-warning-information
 #line ディレクティブで Razor などで正しいソースコードのマッピングを取れなかったものが取れるようになる。

・呼び出し元の引数の式 (Caller argument expression)
  https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/attributes/caller-information#argument-expressions
 引数の部分で書かれた式を呼び出し先のメソッドで文字列として引数として受け取れる。

var array = new[] { 1 };

Debug.Assert(array != null);        // 「array != null」と出力される
Debug.Assert(array.Length == 1);    // 「array.Length == 1」と出力される

public static class Debug
{
    public static void Assert(bool condition, [System.Runtime.CompilerServices.CallerArgumentExpression("condition")] string message = null)
    {
        System.Console.WriteLine(message);
    }
}

・ラムダの機能強化 ラムダ属性 (Lambda improvements)
  https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/operators/lambda-expressions#attributes
 ラムダで属性が使える、また return 値の型も書ける。メソッドの引数などでラムダを渡したいときの扱いの改善。

MapAction([HttpPost("/")] ([FromBody] string todo) => todo);    // ラムダで属性が使える
MapAction(string (todo) => todo);   // 直接各ラムダで return の型ががける

//MapAction([HttpPost("/")] todo => todo);    // これはエラー。ラムダに属性をつける場合、引数リストは ( ) が必要
MapAction([HttpPost("/")] (todo) => todo);  // これは ( ) があるので OK

// 関数やクラスの定義

// Function
void MapAction(Func<string, string> action) {; }

// 属性
class HttpPostAttribute : Attribute { public HttpPostAttribute(string root) {; } }
class FromBodyAttribute : Attribute { public FromBodyAttribute() {; } }

・定数の補間文字列 (Constant interpolated strings)
  https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/tokens/interpolated#structure-of-an-interpolated-string
 const string の定義で値に文字列補間が使えるようになった。

public class C
{
    const string S1 = $"Hello world";
    const string S2 = $"Hello{" "}World";
    const string S3 = $"{S1} Kevin, welcome to the team!";
}

・補間された文字列の改善 (ImprovedInterpolatedStrings)
  https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/attributes/general#interpolatedstringhandler-and-interpolatedstringhandlerarguments-attributes
 ロギングフレームワークなどを想定した文字列補完の効率化。
 そのための属性が追加された。 System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute

A a = new();

a.Method($"{"Test"}");
a.Method($"a{1}b{2}c");
a.Method($"{DateTime.Now}");
/*
コンソールには次の出力
s
AppendLitera a
AppendFormatted System.Int32
AppendLitera b
AppendFormatted System.Int32
AppendLitera c
p
AppendFormatted System.DateTime
p
*/

class A
{
    public void Method(string s) { Console.WriteLine("s"); }
    public void Method(SampleHandler p) { Console.WriteLine("p"); }
}

[System.Runtime.CompilerServices.InterpolatedStringHandler]
public ref struct SampleHandler
{
    public SampleHandler(int literalLength, int formattedCount, out bool handlerIsValid) => handlerIsValid = true;
    public void AppendLiteral(string s) => Console.WriteLine($"AppendLitera {s}");
    public void AppendFormatted<T>(T t) => Console.WriteLine($"AppendFormatted {t.GetType().FullName}");
}

・AsyncMethodBuilder のオーバーライド (AsyncMethodBuilder override)
  https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/attributes/general#asyncmethodbuilder-attribute
 非同期メソッド (async メソッド) をコンパイルしたときに使われる AsyncMethodBuilder のカスタマイズができる。

■ C# 11.0 での新機能

・ファイル スコープ型
 https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/file  型を修飾子 file で定義することで、同一ファイル内だけで使える型を作れます。

file class Class1 { }
file struct Struct1 { }

// 同一ファイル内では使える
class Class2
{
    void Method()
    {
        new Class1();
        new Struct1();
    }
}
// 別ファイルでは使えない
class Class3
{
    void Method()
    {
        new Class1();   // エラー CS0246 型または名前空間の名前 'Class1' が見つかりませんでした(using ディレクティブまたはアセンブリ参照が指定されていることを確認してください)
        new Struct1();  // エラー CS0246 型または名前空間の名前 'Struct1' が見つかりませんでした(using ディレクティブまたはアセンブリ参照が指定されていることを確認してください)
    }
}

・ジェネリック型数値演算のサポート - インターフェイスの static virtual メンバー
 https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/interface#static-abstract-and-virtual-members
 インターフェイスに static virtual メソッド、static abstract メソッドを定義できます。性質は virtualabscract を思い浮かべれば良いのですが、static virtual メソッドを呼ぶことはできないので注意が必要です。

IInterface.Method2(0);  // エラー CS8926 静的な仮想または抽象インターフェイス メンバーには、型パラメーターでのみアクセスできます。

CClass.Method2(0); // OK
CClass.Method3(0);

interface IInterface
{
    static int Method1(int arg) => default;
    static virtual int Method2(int arg) => default;
    static abstract int Method3(int arg);
}

class CClass : IInterface
{
    public static int Method2(int arg) => default;  // これがあると呼び出せる
    public static int Method3(int arg) => default;
}

・ジェネリック型数値演算のサポート - ユーザー定義の checked 演算子
 https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/operators/arithmetic-operators#user-defined-checked-operators
 演算子のオーバーロードをする際に、checked 用の演算子を定義できます。

Console.WriteLine(new Record(100) + new Record(200));               // Record { Value = 1 }
checked { Console.WriteLine(new Record(100) + new Record(200)); }   // Record { Value = 2 }

record struct Record(int Value)
{
    public static Record operator +(Record arg1, Record arg2) => new(1);
    public static Record operator checked +(Record arg1, Record arg2) { checked { return new(2); } }
}

・ジェネリック型数値演算のサポート - 緩和されたシフト演算子
 https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/operators/bitwise-and-shift-operators#operator-overloadability
 シフト演算子のオーバーロードをする際に、右オペランド (第 2 引数) に int でない型を使えます。独自の方でも OK です。

Console.WriteLine(new Sample() >> 1.0 / 2.0);
Console.WriteLine(new Sample() >> new Dummy());

record struct Dummy { }

record struct Sample
{
    public static Sample operator >>(Sample arg1, int arg2) => new Sample();
    public static Sample operator >>(Sample arg1, double arg2) => new Sample();
    public static Sample operator >>(Sample arg1, Dummy arg2) => new Sample();
}

・ジェネリック型数値演算のサポート - 符号なし右シフト演算子
 https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/operators/bitwise-and-shift-operators#unsigned-right-shift-operator-
 新しい右シフト演算子 >>> で符号なしの右シフトができます。

Console.WriteLine(Convert.ToString(-1, toBase: 2).PadLeft(32, '0'));
Console.WriteLine(Convert.ToString(-1 << 1, toBase: 2).PadLeft(32, '0'));
Console.WriteLine(Convert.ToString(-1 >> 1, toBase: 2).PadLeft(32, '0'));
Console.WriteLine(Convert.ToString(-1 >>> 1, toBase: 2).PadLeft(32, '0'));
// 11111111111111111111111111111111
// 11111111111111111111111111111110
// 11111111111111111111111111111111
// 01111111111111111111111111111111

・auto-default 構造体
 https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/builtin-types/struct#struct-initialization-and-default-values
 構造体のコンストラクタで初期化されないフィールドおよびプロパティが自動で default で初期化されるようになりました。

Console.WriteLine(null == new Saitama().Omiya);     // True
Console.WriteLine(null == new Saitama().Kawagoe);   // True

struct Saitama
{
    public string Omiya;
    public string Kawagoe { get; set; }
    public Saitama() { }
}

・string 定数での Span<char> のパターン マッチ
 https://learn.microsoft.com/ja-jp/dotnet/csharp/fundamentals/functional/pattern-matching#compare-discrete-values
 Span<char> の値の対するパターンマッチで文字列とマッチできます。

Span<char> saitama = "saitama".ToCharArray();
ReadOnlySpan<char> tokyo = "tokyo".ToCharArray();

Console.WriteLine($"{saitama} is {((saitama is "saitama")? "最高":"もうすこしがんまりましょう")} です。");
Console.WriteLine($"{tokyo} is {((tokyo is "saitama") ? "最高" : "もうすこしがんまりましょう")} です。");
// saitama is 最高 です。
// tokyo is もうすこしがんまりましょう です。

・拡張 nameof スコープ
 https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/operators/nameof
 メソッドに付ける属性の宣言時に、そのメソッドのパラメータ名で nameof できます。

[return: System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(arg))]
string? Method(string? arg) => arg;

・数値 IntPtr
 https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/builtin-types/integral-numeric-types#characteristics-of-the-integral-types
 nintSystem.IntPtr の、 nuintSystem.UIntPtr のそれぞれエイリアスになりました。

Console.WriteLine(((nint)0).GetType());     // System.IntPtr
Console.WriteLine(((nuint)0).GetType());    // System.UIntPtr

・UTF-8 の文字列リテラル
 https://learn.microsoft.com/ja-jp/dotnet/csharp/whats-new/csharp-11#utf-8-string-literals
 文字列の後ろ ( 閉じの " の後ろ ) に u8 を付けると UTF-8 エンコードの System.ReadOnlySpan<T> が得られます。

foreach (var c in "埼玉".ToArray()) Console.Write(c); Console.WriteLine();
foreach (var c in System.Text.Encoding.Unicode.GetBytes("埼玉")) Console.Write(c); Console.WriteLine();
foreach (var c in System.Text.Encoding.UTF8.GetBytes("埼玉")) Console.Write(c); Console.WriteLine();
foreach (var c in "埼玉"u8) Console.Write(c); Console.WriteLine();
// 埼玉
// 25287137115
// 229159188231142137
// 229159188231142137

・必須メンバー
 https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/required
 クラスや構造体のメンバーを required キーワードで修飾すると必須にできます。必須とは初期化が必須ということで、null で初期化することは OK です。

new Class1 { P1 = null, P2 = null };
new Class1 { P1 = null };  // エラー

class Class1
{
    internal string? P1 { get; init; }
    internal required string? P2 { get; init; }
}

・ref フィールド
 https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/builtin-types/ref-struct
 ref 構造体のフィールドを ref にできます。

ref struct Saitama
{
    public ref string Omiya;
    public Saitama(ref string omiya) => Omiya = ref omiya;
}

・scoped ref
 https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/statements/declarations#scoped-ref
 ref の値の範囲を現在のスコープに限定します。

static ref Saitama Method1(scoped ref Saitama saitama)
{
    return ref saitama; // エラー CS9075  現在のメソッドに範囲指定されているため、参照渡し 'saitama' でパラメーターを返すことはできません
}

static Saitama Method2(scoped Saitama saitama)
{
    return saitama;  // エラー CS8352 参照される変数が宣言のスコープ外に公開される可能性があるため、このコンテキストで変数 'scoped Saitama' を使用することはできません
}

static Saitama Method3(ref string saiko)
{
    scoped Saitama saitama = new(ref saiko);
    return saitama; // エラー CS8352  参照される変数が宣言のスコープ外に公開される可能性があるため、このコンテキストで変数 'saitama' を使用することはできません
}

ref struct Saitama
{
    public ref string Omiya;
    public Saitama(ref string omiya) => Omiya = ref omiya;
}

・未加工の文字リテラル
 https://learn.microsoft.com/ja-jp/dotnet/csharp/programming-guide/strings/#raw-string-literals
 """ で囲うと改行やエスケープが必要な文字列をそのまま書けます。""" を文字列に含みたい場合は """" で囲みます。"""" を含みたい場合は """"" ......。機能を表す言葉が 未加工の文字リテラル で機能名が 生文字列リテラル のようです。

var ramen = """
    "手打ち風" ラーメン
    \550-
    """;
Console.WriteLine(ramen);
// "手作り風" ラーメン
// \550-
var ramen = """"
    """手打ち風""" ラーメン
    \550-
    """";
Console.WriteLine(ramen);
// """手作り風""" ラーメン
// \550-

・改善された、メソッド グループからデリゲートへの変換
 ※公式 Learn の記事は C# 11 の新機能の記事しか見つけることができませんでした。
 この機能は C# 新機能の記述を読んでも良くわかりませんでした。ジェネリックのメソッドからそうでないデリゲート型への暗黙の変換が行われます? その際にキャッシュされて複数回同じ変換をしてもキャッシュが使われる......? と書いてあるような気がします。

// こういうこと、なのでしょうか?
DInt dint1 = Method<int?>;
DInt dint2 = Method;
DString dstring1 = Method<string?>;
DString dstring2 = Method;

Console.WriteLine(object.Equals(dint1, dint2));         // True
Console.WriteLine(object.Equals(dint1, dstring1));      // False
Console.WriteLine(object.Equals(dstring1, dstring2));   // True

T? Method<T>(T? t) => default;

delegate int? DInt(int? i);
delegate string? DString(string? i);

・"警告ウェーブ 7"
 https://rksoftware.hatenablog.com/entry/2022/12/04/220000
 型名がすべて英小文字だと警告になります。

class saitama { }   // 警告 CS8981    型名 'saitama' には、小文字の ASCII 文字のみが含まれています。このような名前は、プログラミング言語用に予約されている可能性があります。

class saitama1 { }  // 数字が含まれているので OK
class saitamaA { }  // 大文字が含まれているので OK
class saitama_ { }  // 記号が含まれているので OK
class 埼玉 { }      // 非 ASCII 文字が含まれているので OK

・汎用属性
 ※公式 Learn の記事は C# 11 の新機能の記事しか見つけることができませんでした。
 日本語よりも英語の機能の名前の方が分かりやすいと思います。「 Generic attributes 」です。名前のまま。属性のクラスを Generic にできます。

var typeString = typeof(SampleStringClass).GetCustomAttributes(false)[0].GetType();
var typeInt = typeof(SampleIntClass).GetCustomAttributes(false)[0].GetType();
Console.WriteLine(typeString);  // SampleAttribute`1[System.String]
Console.WriteLine(typeInt);     // SampleAttribute`1[System.Int32]

class SampleAttribute<T> : Attribute { }

[Sample<string>] class SampleStringClass { }
[Sample<int>] class SampleIntClass { }

・文字列補間式の改行
 https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/tokens/interpolated
 文字列補間式の中で改行ができます。

var prefectures = new[] { "埼玉", "東京" };

foreach (var prefecture in prefectures)
    Console.WriteLine(
        $"{prefecture}{
            prefecture switch
            {
                "埼玉" => "最高",
                _ => "もっとがんばりましょう"
            }
        } です。"
    );

// 埼玉 は 最高 です。
// 東京 は もっとがんばりましょう です。

・リスト パターン
 https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/operators/patterns#list-patterns
 リスト ( や配列など ) の要素でパターンマッチできます。使える型は IList<T>IReadOnlyList<T> 。細かくは int 型のインデクサーと int Count プロパティがある型で使える。

IList<int> data = new[] { 1, 2, 3 };

var result = data switch
{
    [1, 2, 3] => 1,
    _ => 2
};
Console.WriteLine(result);  // 1
// IList<T> 、 IReadOnlyList<T> で使える
IList<int> dataA = new[] { 1, 2, 3 };
IReadOnlyList<int> dataB = new[] { 1, 2, 3 };
Console.WriteLine(dataA is [1, 2, 3]);
Console.WriteLine(dataB is [1, 2, 3]);
// int 型のインデクサーと int Count プロパティがある型で使える
Sample data = new();
Console.WriteLine(data is [1, 2, 3]);

class Sample
{
    public int this[int index]{ get => 0; }
    public int Count => 0;
}

■ 出典/参考文献

 上記まとめの作成に、次のサイトを大いに参考にさせていただきました。
 文言やサンプルコードは C# のガイド のコードを利用させていただきました。

C# のガイド | Microsoft Docs