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# のガイド のコードを利用させていただきました。

C# のガイド | Microsoft Docs