序論
.NET 6の開発進展を伴い、C# 10の最終機能もようやく決定され、デイリービルドに実装されました。今回の更新は主にタイプシステムの改善ために出来ました。じゃあ早速見に行こう。
Structレコード
まずはstructレコードです。C# 10以前はレコードをstructに適用できませんでしたが、C# 10以降は可能になりました。
例えば、ある点の定義は以下のような書き方は可能です:
record struct Point(int X, int Y);
そうすれば、ToString
やGetHashCode
やEquals
は自動的に実装され、とても便利です。
レコード型でToString
はsealed
可能
レコードの型でToString
をオーバーライドするときにsealed
修飾子を追加できます。ToString
をsealed
にすると、コンパイラで派生レコード型に対してToString
メソッドを生成できなくなります。
record class Foo
{
public sealed override string ToString()
{
// ...
}
}
Structでパラメータなしコンストラクター
C# 10では、Structのコンストラクターはパラメータなしでも構いません。
struct Foo
{
public int X;
public Foo() { X = 1; }
}
が、new Foo()
とdefault(Foo)
も同じではなくなりました。上記の例には、new Foo().X
は1
であり、default(Foo).X
は0
です。
匿名オブジェクトのwith
with
を使用して、新なオブジェクトを作成できます。
var x = new { A = 1, B = 2 };
var y = x with { A = 3 }; // y.A は 3 です
グローバルusing
global using
を使用すると、プロジェクト全体の使用が可能になり、各ファイルを書き込む必要がなくなります。 たとえば、Import.csを作成して次のように書き込むことができます:
global using System;
global using i32 = System.Int32;
そして、プロジェクト全体でusing System
は必要がなくなり、i32
を使用できるようになります。
ファイルスコープの名前空間
以前は、名前空間を書き込むために括弧のレイヤーを配置する必要がありました。C# 10ではファイルに名前空間が1つしかない場合は、それを直接先頭に書き込むだけは可能です。
例えば:
namespace MyNamespace;
定数の補間文字列
C# 10では、すべてのプレースホルダー自体が定数文字列の場合、文字列補間を使用してconst
文字列を初期化できます。文字列補間では、アプリケーションで使用される定数文字列を構築するときに、より読みやすい定数文字列を作成できます。
const string x = "hello";
const string y = $"{x}, world!";
ラムダの改善
この改善は非常に大きいと言えますので、複数のポイントで紹介します。
1. 属性のサポート
f = [Foo] (x) => x; // lambdaに配置
f = [return: Foo] (x) => x; // lambdaの戻り値に配置
f = ([Foo] x) => x; // lambdaのパラメーターに配置
2. 戻り値の型は指定可能
f = int () => 4; // intで戻ります
3. ref、in、outのサポート
f = ref int (ref int x) => ref x; // パラメータへの参照を戻ります
4. ファーストクラス関数
関数は暗黙的にデリゲートに変換できるため、関数はファーストクラスになります。
void Foo() { Console.WriteLine("hello"); }
var x = Foo;
x(); // hello
5. ナチュラルデリゲート
C# 10では、lambdaはデリゲート型を自動的に作成するようになったので、型を書き出す必要はもうありません。
var f = () => 1; // Func<int>
var g = string (int x, string y) => $"{y}{x}"; // Func<int, string, string>
var h = "test".GetHashCode; // Func<int>
CallerArgumentExpression
属性CallerArgumentExpression
がついに実装されました。この属性を使用すると、コンパイラは呼び出しパラメータの式文字列を自動的に入力します。
void Foo(int value, [CallerArgumentExpression("value")] string? expression = null)
{
Console.WriteLine(expression + " = " + value);
}
Foo(4 + 5)
を呼び出すと、4 + 5 = 9
が出力されます。
同じ分解内の代入と宣言
この変更により、以前のバージョンのC#からの制限がなくなります。以前は、分解ですべての値を既存の変数に代入したり、新しく宣言された変数を初期化したりすることができました。
int y = 0;
(var x, y, var z) = (1, 2, 3);
インターフェイスは抽象的な静的メソッドのサポート
この機能は、.NET 6でプレビュー機能としてリリースされます。つまり、デフォルトでは有効になっていません。
<LangVersion>preview</LangVersion>
と<EnablePreviewFeatures>true</EnablePreviewFeatures>
を設定して、nugetパッケージSystem.Runtime.Experimental
を追加すると、この機能は有効になります。
そして、インターフェイスは抽象静的メソッドを定義でき、.NET型システムは仮想静的メソッドを遣う機能を備えています。
たとえば、+
演算子がありゼロを持つインターフェイスIMonoid<T>
を定義するとします。
interface IMonoid<T> where T : IMonoid<T>
{
abstract static T Zero { get; }
abstract static T operator+(T l, T r);
}
MyInt
として実装:
public class MyInt : IMonoid<MyInt>
{
public MyInt(int val) { Value = val; }
public static MyInt Zero { get; } = new MyInt(0);
public static MyInt operator+(MyInt l, MyInt r) => new MyInt(l.Value + r.Value);
public int Value { get; }
}
次に、IMoniod<T>
を合計するメソッドの実装:
public static class IMonoidExtensions
{
public static T Sum<T>(this IEnumerable<T> t) where T : IMonoid<T>
{
var result = T.Zero;
foreach (var i in t) result += i;
return result;
}
}
使用:
List<MyInt> list = new() { new(1), new(2), new(3) };
Console.WriteLine(list.Sum().Value); // 6
System.Runtime.Experimental
には.NETの基本型の改善が含まれています。すべての基本型は、数値型のINumber<T>
の実装など、対応するインターフェイスで実装されます。
インターフェイスの静的抽象メソッドのサポートは、将来C#に追加されるシェイプ機能を補完します。その時点で、C#はインターフェイスとシェイプを使用してHaskellのclass
とRustのtrait
ような機能をサポートし、型システムを大きく改善しますのでご期待を。
ジェネリック属性
属性はジェネリックをサポートしました。が、この機能は.NET 6でプレビュー機能としてリリースされますので、<LangVersion>preview</LangVersion>
は必要です。
class TestAttribute<T> : Attribute
{
public T Data { get; }
public TestAttribute(T data) { Data = data; }
}
使用:
[Test<int>(3)]
[Test<float>(4.5f)]
[Test<string>("hello")]
メソッドでAsyncMethodBuilder属性の許可
C# 10では、特定のタスクのような型を返すすべてのメソッドに対してメソッドビルダー型を指定するだけでなく、1つのメソッドに対して別の非同期メソッドビルダーを指定することもできます。 これにより、特定のメソッドでカスタムビルダーのベネフィットが得られる高度なパフォーマンスチューニングシナリオが可能になります。
lineディレクティブの改善
以前は、#line
はファイル内の行を指定するためにのみ使用できましたが、行、列、および範囲を指定できるようになりました。これは、コンパイラーやコードジェネレーターを作成する人にとって非常に便利です。
# line (startLine, startChar) - (endLine, endChar) charOffset "fileName"
// 例:#line (1, 1) - (2, 2) 3 "test.cs"
ネストされた属性パターンマッチングの改善
以前:
if (a is { X: { Y: { Z: 4 } } }) { ... }
C# 10:
if (a is { X.Y.Z: 4 }) { ... }
改善された文字列補間
以前は、C#の文字列補間はstring.Formatであり、値型パラメーター用に直接ボックス化されていました。これは、パフォーマンスに影響を与えるだけでなく、有用性も限られていました。C# 10では、文字列補間が改善されました:
var x = 1;
Console.WriteLine($"hello, {x}");
上記のコードは次のようにコンパイルされます:
int x = 1;
DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(7, 1);
defaultInterpolatedStringHandler.AppendLiteral("hello, ");
defaultInterpolatedStringHandler.AppendFormatted(x);
Console.WriteLine(defaultInterpolatedStringHandler.ToStringAndClear());
上記のDefaultInterpolatedStringHandler
は、InterpolatedStringHandler
属性を使用して、それを独自の補間ハンドラーに置き換えて、補間の実行方法を決定することもできます。そのため、パフォーマンスも機能も大幅に強化されています。