個人的なメモ

Tomohiro Suzuki @hiro128_777 のブログです。Microsoft MVP for Developer Technologies 2017- 本ブログと所属組織の公式見解は関係ございません。

C# 10 の新機能 - ファイルスコープの名前空間宣言

C# 10 の新機能の情報の目次は以下をご覧ください。
hiro128.hatenablog.jp 
 

ファイルスコープの名前空間宣言

1つのファイルにつき1つだけ使用でき、従来の中括弧({})を含めた3行の構文ではなく、中括弧を省いた1行の構文で名前空間宣言が記述できます。
なお、従来の3行の構文は名前空間を入れ子にすることができますが、新しい1行の構文は入れ子にすることができません。
ファイル内で定義されたすべての型の前に宣言する必要があります。

名前空間宣言は、最上位レベルのステートメントと互換性がありませんのでご注意ください。最上位レベルのステートメントは、最上位レベルの名前空間内に存在します。
 
「最上位レベルのステートメント」は C# 9 で追加された機能です。詳細は以下をご参考ください。
最上位レベルのステートメント - Main メソッドを使用しないプログラム | Microsoft Docs
 

導入の意図

通常使われる名前空間を1つだけ含むファイルの場合に、中括弧やインデントをなくすことができます。
一言で言えば「ソースコードを簡潔にするための機能」です。
 

新しい中括弧を省いた1行の構文
namespace MyNameSpace; // セミコロンが必要

class A // インデントを最小にできる
{
}

 

従来の中括弧を含めた3行の構文
namespace MyNameSpace
{
    class A // 無駄にインデントが多くなって読みにくい
    {
        
    }
}

公式情報:
File-scoped namespace declaration
File Scoped Namespaces
 
 

C# 10 の新機能 - global using

C# 10 の新機能の情報の目次は以下をご覧ください。
hiro128.hatenablog.jp 
 

global using

任意のソース ファイルに global using ディレクティブを追加するか、プロジェクトファイル(*.csproj)に Using Itemを追加することですべてのソースファイルで使用したい名前空間を、各ソースファイルで宣言されているかのように指定することができます。
なお、static 修飾子やエイリアスと一緒に使うことができます。
 

導入の意図

共通の using 宣言を1箇所に集約できるので多くの using 行が不要になり、ソースコードからロジック以外の要素を排除できます。
一言で言えば「ソースコードを簡潔にするための機能」です。
 

任意のソースファイルに global using ディレクティブを追加
global using System.Linq; 
global using static System.Console; // static 併用
global using E = System.Environment; // エイリアス併用

 

プロジェクトファイル(*.csproj)に Using Itemを追加
<ItemGroup>
  <Using Include="System.Linq" />
  <Using Include="System.Console" Static="True" /> 
  <Using Include="System.Environment" Alias="E" />
</ItemGroup>

 
公式情報:
Global usings
global 修飾子
 
 

C# 10 の新機能 - 拡張プロパティ パターン

C# 10 の新機能の情報の目次は以下をご覧ください。
hiro128.hatenablog.jp 
 

拡張プロパティ パターン

ネストされたプロパティまたはプロパティ パターン内のフィールドを参照できるようになりました。
以下のような C# 9.0 のコードは、C# 10.0 では「.(ドット)」を使って簡潔に直感的に理解できるように記述できます。

C# 9.0

Person person01 = new();
Console.WriteLine(person01 is Person { Name: Name { FirstName: "太郎" } });

public record struct Name(string FirstName, string LastName);
public record struct Person(Name Name, DateOnly Birthday);

 
C# 10.0

Person person02 = new();
Console.WriteLine(person02 is Person { Name.FirstName: "太郎" });

public record struct Name(string FirstName, string LastName);
public record struct Person(Name Name, DateOnly Birthday);

導入の意図

ネストして冗長な中括弧が増えて読みにくくなるのを防ぎ、簡潔で直感的に理解できるようなコードが記述できます。
こちらも「ソースコードを簡潔にするための機能」です。

公式情報:
Extended property patterns
プロパティ パターン
 

C# 10 の新機能 - ラムダ式の自然型

C# 10 の新機能の情報の目次は以下をご覧ください。
hiro128.hatenablog.jp 
 

ラムダ式の自然型

ラムダ式に対して Func<...>Action<...> などのデリゲート型を強制的に宣言するのではなく、コンパイラがパラメーターと式の型からデリゲート型を推論し、Func または Action のデリゲートの割り当て、または、デリゲート型の合成が行われる機能です。

var f = (string s) => int.Parse(s);  // Func<string, int>

上記の場合、コンパイラにより、parseFunc<string, int> と推論することができます。上記のように当てはまる Func または Action が存在する場合、コンパイラには Func または Action のデリゲートが使われます。 それ以外の場合デリゲート型が合成されます。 たとえば、ラムダ式のパラメーターが ref の場合や、パラメータの数が多い場合などはコンパイラによって型が合成されます。

なお、ラムダ式が自然型の場合、System.Object、System.Delegate などの曖昧な型に割り当てることができます。
(後述しますが、この、ラムダの自然型は、System.Object、System.Delegate に割り当て可能というところがポイントになります。)

object f = (string s) => int.Parse(s);   // Func<string, int>
Delegate f = (string s) => int.Parse(s); // Func<string, int>

 

導入の意図

明示的なデリゲート型なしでラムダとメソッドグループを使用できるようになるため、本来明示的なデリゲート型を定義して割り当てる必要があるメソッドのパラメータに対して明示的なキャストなしでパラメーターと式の型を合わせた任意のラムダを割り当て可能になります。
こちらも「ソースコードを簡潔にするための機能」です。

わかりやすい例として、この機能は ASP.NET の Minimal API (最小限のコードで表現する REST API)のシナリオで便利です。
.NET 6 では以下のオーバーロードが MapGet 拡張メソッドに追加されています。(Delegate handler パラメータに注目してください。)

public static Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapGet (this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string pattern, Delegate handler);

MapGet(IEndpointRouteBuilder, String, Delegate)

上記のように、MapGet メソッドで Microsoft.AspNetCore.Http.RequestDelegate ではなく、Delegate を利用できるようになりました。
さらに、ラムダ式が自然型の場合、System.Delegate に割り当て可能になったため、下記のコードのように、明示的なデリゲート型へのキャストなしでラムダを記述できるようになり、余計な括弧や型が不必要になって直感的に読みやすいコードになりました。

using System;
using System.Linq;
using Microsoft.AspNetCore.Builder;

var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello Minimal API !"); // ラムダの自然型によりラムダを直接割り当てられる
app.MapGet("/", (Func<string>)(() => "Hello Minimal API !")); // ラムダの自然型を使用しない場合、明示的な Func<string> へのキャストが必要
await app.RunAsync();

 
公式情報:
Natural types for lambdas
ラムダ式の自然型
 

Minimal API

Minimal API については以下を参照ください。
hiro128.hatenablog.jp
 

C# 10 の新機能 - レコード構造体

C# 10 の新機能の情報の目次は以下をご覧ください。
hiro128.hatenablog.jp 
 

レコード構造体

C# 9 でレコードクラスが追加されましたが、C# 10 ではレコード構造体が追加されました。
C# 10 ではレコード構造体が追加されたことで、クラスより速度が出る構造体でレコード型を利用可能になりました。
 

導入の意図

レコード型(レコード構造体、レコードクラス)は、簡潔なコードで便利にデータを格納するための型です。

通常のクラスや構造体でデータモデルを作成しようとすると、データとしての等価性をサポートするための Equals のオーバーライドや operator == のオーバーロード、データ表示用の書式設定のための ToString のオーバーライドやデータを不変にするための構文など面倒な手間が生じます。

レコード型を利用すると、不変なプロパティを持つ型を簡潔な構文で作成でき、データ指向の型に役立つ動作が組み込みでサポートされているので、データモデルを簡潔に定義できます。
 

値の等価性

レコード構造体(レコードクラスも含む)は等価性に特徴があります。

  • record class 型の場合、型が同じで同じ値が格納されていれば、2 つのオブジェクトは等しい。
  • record struct 型の場合、型が同じで同じ値が格納されていれば、2 つのオブジェクトは等しい。

比較として、レコード型ではないクラスや構造体の等価性は以下の通りです。

  • class 型の場合、メモリ内の同じオブジェクトを参照していれば、2 つのオブジェクトは等しい。
  • struct 型の場合、型が同じで同じ値が格納されていれば、2 つのオブジェクトは等しい。

 

コード
// レコード構造体
PersonStruct person01 = new("太郎", "山田", new DateOnly(1990, 4, 15));
PersonStruct person02 = new("太郎", "山田", new DateOnly(1990, 4, 15));

Console.WriteLine("レコード構造体");
Console.WriteLine(person01 == person02);


// レコードクラス
PersonClass person03 = new("太郎", "山田", new DateOnly(1990, 4, 15));
PersonClass person04 = new("太郎", "山田", new DateOnly(1990, 4, 15));

Console.WriteLine("レコードクラス");
Console.WriteLine(person03 == person04);

record struct PersonStruct(string FirstName, string LastName, DateOnly Birthday);
record class PersonClass(string FirstName, string LastName, DateOnly Birthday);

 

実行結果
レコード構造体
True
-----------
レコードクラス
True

 

表示用の組み込みの書式設定

レコード型では既定で ToString メソッドで、パブリック プロパティとフィールドの名前と値が表示されます。
これは、データを取り扱う型としてはとても便利です。

<record type name> { <property name> = <value>, <property name> = <value>, ...}

 

コード
PersonStruct person01 = new("太郎", "山田", new DateOnly(1990, 4, 15));
Console.WriteLine(person01);

record struct PersonStruct(string FirstName, string LastName, DateOnly Birthday);

 

実行結果
PersonStruct { FirstName = 太郎, LastName = 山田, Birthday = 4/15/1990 }

 

with 式

with 式を使うと、元のインスタンスの値はそのままに、指定したプロパティとフィールドが変更された、コピーを作成できます。
膨大なプロパティを持つレコード型の一部のプロパティだけを変更したコピーを簡潔な構文で作成できるので非常に便利です。

補足ですが、C#10 以降ではレコード型ではない、struct 型も with 式 をサポートしています。
 

コード
PersonStruct person01 = new("太郎", "山田", new DateOnly(1990, 4, 15));
PersonStruct person02 = new("太郎", "山田", new DateOnly(1990, 4, 15));

person02 = person01 with { FirstName = "花子" };
Console.WriteLine(person02);
Console.WriteLine(person01 == person02);

record struct PersonStruct(string FirstName, string LastName, DateOnly Birthday);

 

実行結果
PersonStruct { FirstName = 花子, LastName = 山田, Birthday = 4/15/1990 }
False

 
なお、レコード構造体はC# 9 のクラスベースのレコードと似ていますが、重要な違いがあります。

  • レコード構造体のプロパティは、デフォルトで変更可能(get / set)です。
  • レコードクラスのプロパティは、デフォルトでは不変(get / init)です

初期化後に init キーワードが設定されているプロパティを再割り当てしようとすると、コンパイルエラーが発生します。
※readonly キーワードを追加することでレコード構造体を不変にできます。

レコード構造体はレコード クラスに取って代わるものではなく、レコード クラスからレコード構造体への移行を推奨するものではありませんので、クラスと構造体のどちらのレコードを使用するかは、利用シナリオに合わせ選択する必要があります。
 
公式情報:
Record structs
レコード (C# リファレンス)