(この記事の読了時間:約15分)
C# 10 が .NET 6 および Visual Studio 2022 の一部としてリリースされたことを発表いたします。この記事では、コードをより美しく、表現力豊かで、高速にする C# 10 の新機能の多くをご紹介します。
詳細については、Visual Studio 2022 の発表と .NET 6 の発表(インストール方法を含む)をご覧ください。
グローバル usings と暗黙的 usings
using ディレクティブは名前空間の利用を簡素化します。C# 10 には、新しいグローバル using ディレクティブと暗黙的 usings が含まれており、各ファイルの先頭で指定する必要がある usings の数を減らします。
グローバル using ディレクティブ
キーワード global が using ディレクティブの前に置かれた場合、その using はプロジェクト全体に適用されます。
global using System;
グローバル using ディレクティブでは、using の機能をすべて使用できます。たとえば、静的インポートを追加して、その型のメンバーと入れ子になった型をプロジェクト全体で利用可能にします。using ディレクティブでエイリアスを使用すると、そのエイリアスもプロジェクト全体に影響します。
global using static System.Console;
global using Env = System.Environment;
グローバル using は、Program.cs や globalusings.cs など、任意の .cs ファイルに配置できます。グローバル usings のスコープは現在のコンパイル(通常は現在のプロジェクト)です。
詳細については、「グローバル using ディレクティブ」を参照してください。
暗黙的 usings
暗黙的 usings 機能は、構築中のプロジェクトの種類に応じて、一般的なグローバル using ディレクティブを自動的に追加します。暗黙的 usings を有効にするには、.csproj ファイルで ImplicitUsings プロパティを設定します。
<PropertyGroup>
<!-- Other properties like OutputType and TargetFramework -->
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
新しい .NET 6 テンプレートでは暗黙的 usings が有効になっています。このブログ記事で .NET 6 テンプレートの変更について詳しくお読みください。
特定のグローバル using ディレクティブのセットは、ビルドするアプリケーションの種類によって異なります。たとえば、コンソールアプリケーションやクラスライブラリの暗黙的 usings は、ASP.NET アプリケーションの暗黙的 usings とは異なります。詳細については、この暗黙的 usings の記事を参照してください。
using 機能の組み合わせ
ファイル先頭の従来の using ディレクティブ、グローバル using ディレクティブ、暗黙的 using は、うまく連携します。暗黙的 usings を使用すると、プロジェクトファイルで、ビルドするプロジェクトの種類に適した .NET 名前空間を含めることができます。グローバル using ディレクティブを使用すると、他の名前空間をプロジェクト全体で利用可能にできます。コードファイル先頭の using ディレクティブを使用すると、プロジェクト内の少数のファイルだけで使用する名前空間を含めることができます。
定義方法に関係なく、余分な using ディレクティブは名前解決のあいまいさを増す可能性があります。このような場合は、エイリアスを追加するか、インポートする名前空間の数を減らすことを検討してください。たとえば、グローバル using ディレクティブをファイルのサブセットの先頭にある明示的な using ディレクティブに置き換えることができます。
暗黙的 usings に含まれる名前空間を削除する必要がある場合は、プロジェクトファイルで指定できます。
<ItemGroup>
<Using Remove="System.Threading.Tasks" />
</ItemGroup>
また、グローバル using ディレクティブと同様に、名前空間を追加することもできます。Using 項目をプロジェクトファイルに追加します。例:
<ItemGroup>
<Using Include="System.IO.Pipes" />
</ItemGroup>
ファイルスコープの名前空間
多くのファイルは、単一の名前空間のコードを含んでいます。C# 10 以降では、名前空間をステートメントとして、セミコロンと中括弧なしで含めることができます。
namespace MyCompany.MyNamespace;
class MyClass // Note: no indentation
{ ... }
コードが単純化され、入れ子のレベルが削除されます。ファイルスコープの名前空間宣言は1つだけ許可され、型の宣言より前に記述する必要があります。
ファイルスコープの名前空間の詳細については、「namespace キーワードの記事」を参照してください。
ラムダ式とメソッドグループの改善
ラムダの構文と型にいくつかの改善を加えました。これらは広く役立つと考えており、その動機のひとつは ASP.NET Minimal API をよりシンプルにすることです。
ラムダの自然な型
ラムダ式は、時々「自然な」型を持つようになりました。これは、コンパイラがラムダ式の型を推論できることが多いことを意味します。
これまでは、ラムダ式をデリゲート型または式ツリー型に変換する必要がありました。ほとんどの場合、BCL のオーバーロードされた Func<...> や Action<...> デリゲート型のいずれかを使用します。
Func<string, int> parse = (string s) => int.Parse(s);
しかし、C# 10 以降では、ラムダにそのような「ターゲット型」がない場合、コンパイラは型を計算しようとします。
var parse = (string s) => int.Parse(s);
お気に入りのエディターで var parse にマウスを合わせると、型が依然として Func<string, int> であることが確認できます。一般的に、コンパイラは利用可能な適切な Func または Action デリゲートがあればそれを使用します。そうでない場合(ref パラメーターや多数のパラメーターがある場合など)は、デリゲート型を合成します。
すべてのラムダ式が自然な型を持つわけではありません。型情報が不十分なものもあります。たとえば、パラメーターの型を省略すると、コンパイラはどのデリゲート型を使用すべきか判断できません。
var parse = s => int.Parse(s); // ERROR: Not enough type info in the lambda
ラムダの自然な型は、object や Delegate のような弱い型に割り当て可能になることを意味します。
object parse = (string s) => int.Parse(s); // Func<string, int>
Delegate parse = (string s) => int.Parse(s); // Func<string, int>
式ツリーに関しては、「ターゲット型」と「自然な型」を組み合わせます。ターゲット型が LambdaExpression または非ジェネリックの Expression(すべての式ツリーの基本型)で、ラムダが自然なデリゲート型 D を持つ場合、代わりに Expression<D> を生成します。
LambdaExpression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
Expression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
メソッドグループの自然な型
メソッドグループ(パラメーターリストのないメソッド名)も、時々自然な型を持つようになりました。以前から、メソッドグループを互換性のあるデリゲート型に変換することはできました。
Func<int> read = Console.Read;
Action<string> write = Console.Write;
現在では、メソッドグループに1つのオーバーロードしかない場合、自然な型を持ちます。
var read = Console.Read; // Just one overload; Func<int> inferred
var write = Console.Write; // ERROR: Multiple overloads, can't choose
ラムダの戻り値の型
前の例では、ラムダ式の戻り値の型は明らかであり、推論されました。常にそうであるとは限りません。
var choose = (bool b) => b ? 1 : "two"; // ERROR: Can't infer return type
C# 10 では、メソッドやローカル関数と同様に、ラムダ式に明示的な戻り値の型を指定できます。戻り値の型はパラメーターの前に置きます。明示的な戻り値の型を指定する場合、コンパイラや他の開発者が混乱しないように、パラメーターは括弧で囲む必要があります。
var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>
ラムダの属性
C# 10 以降では、メソッドやローカル関数と同様に、ラムダ式に属性を付けることができます。属性がある場合、ラムダのパラメーターリストは括弧で囲む必要があります。
Func<string, int> parse = [Example(1)] (s) => int.Parse(s);
var choose = [Example(2)][Example(3)] object (bool b) => b ? 1 : "two";
ローカル関数と同様に、属性が AttributeTargets.Method で有効な場合、ラムダに属性を適用できます。
ラムダはメソッドやローカル関数とは呼び出し方が異なるため、ラムダの呼び出し時に属性は影響しません。しかし、ラムダの属性はコード分析に役立ち、リフレクションで発見できます。
構造体の改善
C# 10 では、構造体とクラスの間でより良いパリティを提供する機能を導入します。これらの新機能には、パラメーターなしのコンストラクター、フィールド初期化子、レコード構造体、with 式が含まれます。
01 パラメーターなしの構造体コンストラクターとフィールド初期化子
C# 10 より前では、すべての構造体には暗黙のパブリックなパラメーターなしのコンストラクターがあり、構造体のフィールドをデフォルト値に設定していました。構造体でパラメーターなしのコンストラクターを作成することはエラーでした。
C# 10 以降では、独自のパラメーターなしの構造体コンストラクターを含めることができます。提供しない場合は、すべてのフィールドをデフォルト値に設定する暗黙のパラメーターなしのコンストラクターが提供されます。構造体で作成するパラメーターなしのコンストラクターは、パブリックであり、部分的なものであってはなりません。
public struct Address
{
public Address()
{
City = "<unknown>";
}
public string City { get; init; }
}
上記のようにパラメーターなしのコンストラクターでフィールドを初期化することも、フィールドまたはプロパティ初期化子で初期化することもできます。
public struct Address
{
public string City { get; init; } = "<unknown>";
}
デフォルト作成または配列割り当ての一部として作成された構造体は、明示的なパラメーターなしのコンストラクターを無視し、常に構造体メンバーをデフォルト値に設定します。構造体のパラメーターなしのコンストラクターの詳細については、「構造体型」を参照してください。
02 レコード構造体
C# 10 以降、record struct を使用してレコードを定義できるようになりました。これらは C# 9 で導入されたレコードクラスに似ています。
public record struct Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
引き続き record を使用してレコードクラスを定義することも、record class を使用して明確に指定することもできます。
構造体は既に値の等価性を持っています(比較は値で行われます)。レコード構造体は IEquatable<T> サポートと == 演算子を追加します。レコード構造体は、リフレクションのパフォーマンス問題を避けるために IEquatable<T> のカスタム実装を提供し、ToString() オーバーライドなどのレコード機能を含みます。
レコード構造体は位置指定可能であり、プライマリコンストラクターは暗黙的にパブリックメンバーを宣言します。
public record struct Person(string FirstName, string LastName);
プライマリコンストラクターのパラメーターは、レコード構造体のパブリックな自動実装プロパティになります。レコードクラスとは異なり、暗黙的に作成されるプロパティは読み取り/書き込み可能です。これにより、タプルを名前付き型に変換しやすくなります。戻り値の型を (string FirstName, string LastName) のようなタプルから Person のような名前付き型に変更すると、コードが整理され、メンバー名の一貫性が保証されます。位置指定レコード構造体を宣言することは簡単で、可変セマンティクスを維持します。
プライマリコンストラクターのパラメーターと同じ名前のプロパティやフィールドを宣言すると、自動プロパティは合成されず、ユーザーのものが使用されます。
不変のレコード構造体を作成するには、(任意の構造体に追加できるように)構造体に readonly を追加するか、個々のプロパティに readonly を適用します。オブジェクト初期化子は、読み取り専用プロパティを設定できる構築フェーズの一部です。これは、不変レコード構造体を使用する 1 つの方法です。
var person = new Person { FirstName = "Mads", LastName = "Torgersen"};
public readonly record struct Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
レコード構造体の詳細については、この記事をご覧ください。
03 レコードクラスにおける ToString() のシール修飾子
レコードクラスも改善されました。C# 10 以降、ToString() メソッドに seal 修飾子を含めることができ、コンパイラが派生レコードに対して ToString 実装を合成するのを防ぎます。
ToString() の詳細については、この記事の「レコード」を参照してください。
04 構造体と匿名型の with 式
C# 10 は、レコード構造体を含むすべての構造体、および匿名型の with 式をサポートします。
var person2 = person with { LastName = "Kristensen" };
これは、新しい値を持つ新しいインスタンスを返します。任意の数の値を更新できます。設定しなかった値は、初期インスタンスと同じ値を保持します。
with の詳細については、この記事をご覧ください。
補間文字列の改善
C# に補間文字列を追加したとき、パフォーマンスと表現力の面で、この構文でさらに多くのことができると感じていました。
01 補間文字列ハンドラー
現在、コンパイラは補間文字列を string.Format の呼び出しに変換します。これにより多くの割り当てが発生します(パラメーターのボックス化、パラメーター配列の割り当て、そしてもちろん結果の文字列自体)。さらに、実際の補間の意味について余裕がありません。
C# 10 では、API が補間文字列のパラメーター式の処理を「引き継ぐ」ことを可能にするライブラリパターンを追加しました。例として、StringBuilder.Append を考えてみましょう。
var sb = new StringBuilder();
sb.Append($"Hello {args[0]}, how are you?");
これまでは、新しく割り当てられ計算された文字列を使って Append(string? value) オーバーロードを呼び出し、それを StringBuilder に 1 つのチャンクとして追加していました。しかし、Append には新しいオーバーロード Append(ref StringBuilder.AppendInterpolatedStringHandler handler) があり、補間文字列を引数として使用する場合、文字列オーバーロードよりも優先されます。通常、SomethingInterpolatedStringHandler 形式のパラメーター型を見た場合、API 作成者は舞台裏で、補間文字列を目的に適切に処理するための作業を行っています。Append の例では、文字列 "Hello"、args[0]、"、how are you?" が個別に StringBuilder に追加され、より効率的で同じ結果が得られます。
文字列の構築作業を特定の条件下でのみ行いたい場合があります。例として Debug.Assert があります。
Debug.Assert(condition, $"{SomethingExpensiveHappensHere()}");
ほとんどの場合、条件は真であり、2 番目の引数は使用されません。しかし、呼び出しのたびにすべての引数が評価されるため、不必要に実行が遅くなります。Debug.Assert には現在、カスタム補間文字列ビルダーを持つオーバーロードがあり、条件が偽の場合にのみ 2 番目の引数が評価されるようにします。
最後に、特定の呼び出しで文字列補間の動作を実際に変更する例を示します。String.Create() を使用すると、補間文字列パラメーター自体の穴の中の式の書式設定に使用する IFormatProvider を指定できます。
String.Create(CultureInfo.InvariantCulture, $"The result is {result}");
補間文字列ハンドラーの詳細については、この記事とカスタムハンドラー作成のチュートリアルをご覧ください。
02 定数補間文字列
補間文字列のすべての穴が定数文字列である場合、結果の文字列も定数になります。これにより、属性など、より多くの場所で文字列補間構文を使用できるようになります。
[Obsolete($"Call {nameof(Discard)} instead")]
穴は定数文字列で埋める必要があることに注意してください。数値や日付値などの他の型は、カルチャに依存し、コンパイル時に計算できないため使用できません。
その他の改善
C# 10 には、言語全体にわたる多くの小さな改善が含まれています。そのいくつかは、C# を期待通りに動作させるだけのものです。
分解での宣言と変数の混合
C# 10 より前では、分解ではすべての変数が新しいか、すべての変数があらかじめ宣言されている必要がありました。C# 10 では、混合できます。
int x2;
int y2;
(x2, y2) = (0, 1); // Works in C# 9
(var x, var y) = (0, 1); // Works in C# 9
(x2, var y3) = (0, 1); // Works in C# 10 onwards
分解に関する記事で詳細をご覧ください。
明確な代入の改善
まだ明確に代入されていない値を使用すると、C# はエラーを生成します。C# 10 ではコードをよりよく理解し、誤ったエラーが減ります。これらの改善により、null 参照に関する誤ったエラーや警告も減少します。
C# 10 の新機能に関する記事で、C# の明確な代入について詳しくご覧ください。
拡張プロパティパターン
C# 10 では、拡張プロパティパターンが追加され、パターン内の入れ子になったプロパティ値にアクセスしやすくなりました。たとえば、上記の Person レコードにアドレスを追加した場合、次の 2 つの方法でパターンマッチングが可能です。
object obj = new Person
{
FirstName = "Kathleen",
LastName = "Dollard",
Address = new Address { City = "Seattle" }
};
if (obj is Person { Address: { City: "Seattle" } })
Console.WriteLine("Seattle");
if (obj is Person { Address.City: "Seattle" }) // Extended property pattern
Console.WriteLine("Seattle");
拡張プロパティパターンはコードを簡素化し、特に複数のプロパティをマッチングする場合に読みやすくします。
パターンマッチングの記事で、拡張プロパティパターンの詳細をご覧ください。
呼び出し元式属性
CallerArgumentExpressionAttribute は、メソッド呼び出しのコンテキストに関する情報を提供します。他の CompilerServices 属性と同様に、この属性はオプションのパラメーターに適用されます。この場合は文字列です。
void CheckExpression(bool condition,
[CallerArgumentExpression("condition")] string? message = null )
{
Console.WriteLine($"Condition: {message}");
}
CallerArgumentExpression に渡されるパラメーター名は、別のパラメーターの名前です。そのパラメーターに引数として渡された式が文字列に含まれます。例:
var a = 6;
var b = true;
CheckExpression(true);
CheckExpression(b);
CheckExpression(a > 5);
// Output:
// Condition: true
// Condition: b
// Condition: a > 5
ArgumentNullException.ThrowIfNull() は、この属性の良い使用例です。デフォルトで提供される値によって、パラメーター名を渡す必要がありません。
void MyMethod(object value)
{
ArgumentNullException.ThrowIfNull(value);
}
ご意見やご提案があれば、下のコメント欄でお聞かせください。ありがとうございます。