.NET 6の現状を把握し、具体的な移行方法を学ぶ連載。今回は、C# 10の新機能について。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
.NET 6の現状を把握し、具体的な移行方法を学ぶ本連載「.NET 6移行入門」。第2回は、C# 10の新機能の中から注目の機能をピックアップして紹介します。
C# 10の新機能の特長は、「簡潔なコードを書くための機能」が多く追加されていることです。新しい機能を利用することで冗長な構文のコードを排除でき、簡潔で直感的なコードを記述できます。
C# 9で「レコードクラス」が追加されましたが、C# 10では「レコード構造体」が追加されました。メンバーの数が少ない軽量オブジェクトの場合にクラスよりも速度が出る構造体のレコードを利用できます。
レコード型(レコード構造体、レコードクラス)は、簡潔なコードで便利にデータを格納するための型です。通常のクラスや構造体でデータモデルを作成しようとすると、データとしての等価性をサポートする「Equals」のオーバーライドや「operator ==」のオーバーロード、表示用の書式設定の「ToString」のオーバーライドやデータを不変にする構文などを記述する面倒な手間が生じます。
レコード型を利用すると、不変なプロパティを持つ型を簡潔な構文で作成できます。また、上記のようなデータ指向の型に役立つ動作が組み込みでサポートされているので、データモデルを簡潔に定義できます。
レコード構造体(レコードクラスも含む)は等価性に特徴があります。
比較として、レコード型ではないクラスや構造体の等価性は下記の通りです。
// レコード構造体 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式」を使うと、元のインスタンスの値はそのままに、指定したプロパティとフィールドが変更されたコピーを作成できます。膨大なプロパティを持つレコード型の一部のプロパティだけを変更したコピーを簡潔な構文で作成できるので非常に便利です。
補足ですが、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のクラスベースのレコードと似ていますが、下記の違いがあります。
※初期化後にinitキーワードが設定されているプロパティを再割り当てしようとすると、コンパイルエラーが発生します。
※readonlyキーワードを追加することで、レコード構造体を不変にできます。
レコード構造体はレコードクラスに取って代わるものではなく、レコードクラスからレコード構造体への移行を推奨するものではありません。そのため、クラスと構造体のどちらのレコードを使用するかは、メンバーの数やインスタンスが作成される回数などを考慮し、利用シナリオに合わせて選択する必要があります。
任意のソースファイルに「global using」ディレクティブを追加するか、プロジェクトファイル(*.csproj)に「Using Item」を追加することで、全てのソースファイルで使用したい名前空間を、各ソースファイルで宣言されているかのように指定することができます。
なお、static修飾子やエイリアスと一緒に使うことができます。
共通のusing宣言を1カ所に集約できるので、多くのusing行が不要になり、ソースコードからロジック以外の要素を排除できます。ひと言で言えば「ソースコードを簡潔にする機能」です。
global using System.Linq; global using static System.Console; // static 併用 global using E = System.Environment; // エイリアス併用
<ItemGroup> <Using Include="System.Linq" /> <Using Include="System.Console" Static="True" /> <Using Include="System.Environment" Alias="E" /> </ItemGroup>
コンパイラによって、プロジェクトの種類ごとにあらかじめ決められた、よく使われる名前空間のセットが自動的に追加されます。
例えば、コンソールアプリケーションの場合、下記の名前空間が自動的に追加されます。
using System; using System.IO; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks;
.NET 5以前のプロジェクトを.NET 6以降に再ターゲットするときに、暗黙的なglobal usingディレクティブは追加されないので、注意してください(場合によっては、コンパイルできなくなります)。
また、.NET 6以降の「ImplicitUsings」プロパティが有効になっているプロジェクトでこの機能を無効にすると、例えば、暗黙的に宣言されていたSystem名前空間が宣言されなくなってしまい、アプリがコンパイルされなくなります。プロジェクト単位での有効/無効の変更時はご注意ください。
手動でImplicitUsingsプロパティをtrueまたはenableに設定することで、プロジェクト単位でこの機能を有効にすることができます。.NET 6 以降をターゲットとするC#プロジェクトのテンプレートでは、ImplicitUsingsは既定でenableに設定されています。
<PropertyGroup> <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup>
明示的に指定することなくその種類のアプリで一般的によく使われる名前空間が自動的に追加されるので、多くのusing行が不要になり、ソースコードからロジック以外の要素を排除できます。こちらも「ソースコードを簡潔にする機能」です。
従来の中かっこ({})を含めた3行の構文ではなく、中かっこを省いた1行の構文で名前空間宣言を記述できます。1つのファイルにつき1つだけ使用できます。
なお、従来の3行の構文は名前空間を入れ子にできますが、新しい1行の構文は入れ子にできません。
ファイルスコープの名前空間宣言はファイル内で定義された全ての型の前に宣言する必要があります。名前空間宣言は、最上位レベルのステートメントと互換性がないのでご注意ください。最上位レベルのステートメントは、最上位レベルの名前空間内に存在します。
「最上位レベルのステートメント」はC# 9で追加された機能です。詳細は下記リンクを参考にしてください。
通常使われる名前空間を1つだけ含むファイルの場合に、中かっこやインデントをなくすことができます。これも「ソースコードを簡潔にする機能」です。
namespace MyNameSpace; // セミコロンが必要 class A // インデントを最小にできる { }
namespace MyNameSpace { class A // 無駄にインデントが多くなって読みにくい { } }
ラムダ式に対して「Func<……>」「Action<……>」などのデリゲート型を強制的に宣言するのではなく、コンパイラがパラメーターと式の型からデリゲート型を推論し、FuncまたはActionのデリゲートの割り当て、または、デリゲート型の合成が行われる機能です。
var f = (string s) => int.Parse(s); // Func<string, int>
上記の場合、コンパイラにより、fを「Func<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メソッドで「Microsoft.AspNetCore.Http.RequestDelegate」はなく、「System.Delegate」を利用できるようになりました。さらにラムダ式の自然型によって、System.Delegateに割り当て可能になったので、下記のコードのように明示的なデリゲート型へのキャストなしでラムダ式を記述できるようになり、冗長なかっこや型の明示が不必要になって直感的に読みやすいコードになりました。
using System; using System.Linq; using Microsoft.AspNetCore.Builder; var app = WebApplication.Create(args); app.MapGet("/hello1", () => "Hello Minimal API !"); // ラムダ式の自然型によってラムダを直接割り当てられる app.MapGet("/hello2", (Func<string>)(() => "Hello Minimal API !")); // ラムダ式の自然型を使用しない場合、明示的な Func<string> へのキャストが必要 await app.RunAsync();
Minimal APIについては下記リンクを参照してください。
プレースホルダーに使用する値が定数である場合は、constで定義された値にも文字列補間が使えるようになり、コード記述の制限が1つ改善されました。
さらに、下記コード1のように補間された文字列の全ての部分が文字列リテラルであることを認識できる場合、コンパイラは、コード2のように単一の文字列リテラルとして記述されていた場合と同じように最適化してILに出力します。つまり、以下の2つのコードはどちらの場合も、コード2のように記述されたものとしてILに出力されます。
const string Greeting = "Hello"; const string GreetingName = $"{Greeting} .NET 6"; // constで定義する値にも文字列補間が使用できる string result = $"{GreetingName}!";
string result = "Hello, .NET 6!";
この機能はコードの記述の制限を1つなくし、コンパイラの最適化による速度を向上させる機能です。速度向上について開発者は特に意識することなくコンパイラで最適化が働き、パフォーマンスがアップします。
ネストされたプロパティまたはプロパティパターン内のフィールドを参照できるようになりました。下記のようなC# 9.0のコードは、C# 10.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);
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);
ネストして冗長な中かっこが増えて読みにくくなるのを防ぎ、簡潔で直感的に理解できるようなコードが記述できます。こちらも「ソースコードを簡潔にする機能」です。
新機能の紹介は以上です。最初に述べた通り、簡潔で直感的なコードを記述するための改善が多く提供されています。移行のプロジェクトではなかなか新しい構文を取り入れるのは難しいでしょうが、将来のための知見という意味でもその快適さを一度体験してみてはいかがでしょうか。
Copyright © ITmedia, Inc. All Rights Reserved.