この記事は、「Advent Calendar 2022」に参加しています。
Qiita の Advent Calender の位置づけは、「エンジニアに関する情報に特化している」や「年に1度のお祭りのような企画」のようになっていて、C# は「C#に関係あるみんなに伝えたいことを、気軽に書いてください!」ということみたいです。
この記事は、C# 11 の新しい機能を調べて、勉強をして、アウトプットして共有しようという単純なものです。
.NET Blog に開発者 Mads Torgersen さんの「Welcome to C# 11」という記事があったので、その内容を下敷きにして「Microsoft Learn - C# 11 の新機能」を確認しながら、勉強した内容を添えたりしたコンテンツになっています。
前書きと後書き (In closing …) の内容は Advent Calendar 内では省略しています。(見たい方はブログを参照してください)基本的に、テキストは「Welcome to C# 11」の内容ですが、付け加えたり色々変わっています。
UTF-8 string literals (UTF-8 の文字列リテラル)
C# の string 型のテキストは、デフォルトで UTF-16 にハードコーディングされていますが、インターネット上のテキストは UTF-8 ですよね。変換の手間とパフォーマンスのオーバーヘッドを最小限にするため、文字列リテラルに u8 を付与するだけで、すぐに UTF-8 テキストを使えます:
var u8 = "This is a UTF-8 string!"u8;
Console.WriteLine(u8);
出力結果:
error CS1503: Argument 1: cannot convert from 'System.ReadOnlySpan<byte>' to 'bool'
UTF-8 文字列リテラルは、ReadOnlySpan<byte>
型の byte のチャンクを返却します。(var の型)UTF-8 エンコーディングが重要なシナリオでは、特定の目的のための (decicated) UTF-8 文字列型よりも便利になるはずです。
「Microsoft Docs - UTF-8 string literals」の記事を読んでおこう。
UTF-8 の文字列リテラルはコンパイル時定数ではありません。これらはランタイム定数です。 したがって、省略可能なパラメーターの既定値として使用することはできません。
Raw string literals (未加工の文字列リテラル)
文字列リテラルに入れられるものの多くは、ある種の「コード」です。プログラムのテキストだけではなくて Json, XML, HTML, 正規表現, SQL クエリーなどなど、です。このようなテキストで利用される特殊文字が、C# の文字列リテラルとして意味を持ってしまうと、(本来期待する)手助けにはなっていませんよね! 注目する例を挙げると、{
と }
で補間される文字列に含まれる \
と "
です。これらの文字をすべてエスケープしなければならないのは、本当にガッカリするし、バグる原因として残っていました。
エスケープ文字を使わない文字列リテラルの形式は、どうでしょうか。それが、未加工の文字列リテラル (Raw string literals) です。(エスケープ文字などを含まない)テキストは、すべてテキスト本来の文字で構成されます。
未加工の文字列リテラル (Raw string literals) は、3つのダブルクォーテーション """
を使います。
// 未加工の文字列リテラル
var raw1 = """This\is\all "content"!""";
Console.WriteLine(raw1);
出力結果:
This\is\all "content"!
もしも、コンテンツの部分に3つ以上の "
が必要な場合は、外側に "
を増やします。(下の例は4つの "
で囲っています)最初と最後の "
の数が一致するようにしましょう:
var raw2 = """""I can do ", "", """ or even """" double quotes!""""";
出力結果:
I can do ", "", """ or even """" double quotes!
未加工の文字列リテラル (Raw string literals) を利用することで、リテラルの中身を読んだり、ペーストしたりと、しやすくなりました。
複数行の未加工の文字列リテラル (Raw string literals) は、最初のホワイトスペースを切り捨てることができます。終了引用符の位置によって出力に含まれるホワイトスペースの開始位置が決まります。
ホワイトスペースとは、空白や改行として表現されるが、最終的に表示・出力させた際には反映されないような文字のこと
var raw3 = """
<element attr="content">
<body>
This line is indented by 4 spaces.
</body>
</element>
""";
出力結果:
<element attr="content">
<body>
This line is indented by 4 spaces.
</body>
</element>
未加工の文字列リテラル (Raw string literals) は、このほかにも多くの機能があります。例えば、補間 (interpolation) です。詳しくは、「ドキュメント」を参照してください。(補間の例を勉強したので、下におまけで書きました)
補間
int X = 2;
int Y = 3;
var pointMessage = $$"""The point {{{X}}, {{Y}}} is {{Math.Sqrt(X * X + Y * Y)}} from the origin.""";
出力結果:
The point {2, 3} is 3.605551275463989 from the origin.
開始と終了の $
の数で、C# 6.0 の補間文字列を作る {}
の数を指定しています。ただ、この記事での説明内容をわかりづらくする部分があるので略されたのだと思います。
静的メンバーの抽象化 (Abstracting over static members)
演算子など、本質的に静的な操作はどのように抽象化すればよいでしょう。これまでの答えは "貧弱 (poorly)" です。C# 11 では C# 10 でプレビュー版だった静的仮想メンバー (static virtual member) をインターフェースでサポートするようにしました。これを使えば、非常にシンプルな数学的なインターフェースを定義できるようになります。
public interface IMonoid<TSelf> where TSelf : IMonoid<TSelf>
{
public static abstract TSelf operator +(TSelf a, TSelf b);
public static abstract TSelf Zero { get; }
}
このインターフェースは "自分自身 (itself)" の型パラメーターを取ることに注意してください。それは、静的なメンバーには this がないからという理由ですね。
ふたつの静的メンバーの実装を提供し、TSelf 型の引数に自分自身を渡すことで、誰にでもこのインターフェース IMonoid を実装することができるようになりました:
public struct MyInt : IMonoid<MyInt>
{
int value;
public MyInt(int i) => value = i;
public static MyInt operator +(MyInt a, MyInt b) => new MyInt(a.value + b.value);
public static MyInt Zero => new MyInt(0);
}
重要なのは、これらの抽象的な操作をどのように消費するか (consume) ということです。仮想メンバーを呼び出すインスタンスがない場合、どのように呼び出すのでしょうか。答えは、ジェネリクスです。以下のとおり:
T AddAll<T>(params T[] elements) where T : IMonoid<T>
{
T result = T.Zero;
foreach (var element in elements)
{
result += element;
}
return result;
}
型パラメーター T は、IMonoid<T>
インターフェースの制約を受けて、インターフェースの静的仮想メンバーの Zero と + を T 自身に対して使えます!
MyInt を使って、ジェネリクスメソッドを呼び出すことができるし、+ と Zero の実装は型引数付きで渡されます:
MyInt sum = AddAll<MyInt>(new MyInt(3), new MyInt(4), new MyInt(5));
.NET 7 には新しい名前空間 System.Numerics が追加され、数学のインターフェースがぎっしりと詰まっています。これは、上記の小さな IMonoid<T>
インターフェースの "grown-up" (成長版) バージョンのようなもので、演算子や他の静的メンバーのさまざまな組み合わせを表しています。
.NET のすべての数値型は、これらの新しいインターフェースを実装していて、自分でつくった型にも追加することができます。これで、本質的に同じコードのオーバーロードをいくつも持つ代わりに――具体的な型から抽象化されて、数値計算のアルゴリズムを書くことが簡単になりました。
静的仮想メンバーは、数学以外でも有用であることは注目に値します。例えば、型の階層のファクトリーメソッドを抽象化できます。「静的抽象インターフェース メソッド (static abstract interface methods)」と「ジェネリック型数値演算 (generic math)」については、ドキュメントを参照ください。
最後に、静的仮想メンバーを使用したインターフェースを自分で作成しなくても、.NET ライブラリーからこの恩恵を受けていることがあるのを忘れないこと。
静的抽象インターフェース メソッド (static abstract interface methods)
ついでに、ドキュメントの内容をちょっと調べることにします。 ふたつの数値の中間値を返すメソッドについて考える例です。
public static double MidPoint(double left, double right) =>
(left + right) / (2.0);
数値型 (int, short, long, float, decimal)、または、数値を表す任意の型にも同じロジックでできます。System.Numerics.INumber インターフェースを使用すると、次のジェネリクス メソッドを作れます。
public static T MidPoint<T>(T left, T right)
where T : INumber<T> => (left + right) / T.CreateChecked(2);
// note: 左右の足し算は両方が大きい値のときオーバーフローする恐れがあります
// これはデモンストレーション用のコードです
INumber<TSelf>
インターフェースの実装に、オペレーター +
と -
の定義を含める必要があります。表せる範囲に入らない値の場合、OverflowException がスローされます。
次に、オペレーター ++
の例を定義します。
public interface IGetNext<T> where T : IGetNext<T>
{
static abstract T operator ++(T other);
}
型引数 T
は IGetNext<T>
を実装するという制約により、演算子のシグネチャに、含んでいる型、または、その型引数が含まれると保証されます。次のコードは、文字 "A" の文字列を作成し、インクリメントするたびに "A" をもうひとつ追加する構造体を作成します。
public struct RepeatSequence : IGetNext<RepeatSequence>
{
private const char Ch = 'A';
public string Text = new string(Ch, 1);
public RepeatSequence() {}
public static RepeatSequence operator ++(RepeatSequence other)
=> other with { Text = other.Text + Ch };
public override string ToString() => Text;
}
++
はこの次の値を生成すると予想できるので、このインターフェースを使用するとわかりやすいコードになります。
var str = new RepeatSequence();
for (int i = 0; i < 10; i++)
Console.WriteLine(str++);
出力結果:
A
AA
AAA
AAAA
AAAAA
AAAAAA
AAAAAAA
AAAAAAAA
AAAAAAAAA
AAAAAAAAAA
この例は、静的抽象インターフェース メソッド (static abstract interface methods) の意図を示したものです。 演算子、定数値、他の静的操作に自然な構文を使用できることがポイント。
operator 関係だと checked も追加されていて、クラスには file で「ファイル内からだけアクセスできる型」みたいなミニマムな Extension などを作れる。
リストパターン (List patterns)
パターンマッチングは、C# で現在進行中のストーリーのひとつで、私たちは膨らませ続けています。パターンマッチングは、C# 7 で導入されてから、この言語で最も重要、かつ、強力な制御構造のひとつに成長しました。
C# 11 では、パターンマッチングにリストパターンが追加されました。リストパターンを使うことで、リストのような入力の個別要素、または、それらの範囲にパターンを再帰的に適用できます。リストパターンを使用して書いた例は次のとおり:
T AddAll<T>(params T[] elements) where T : IMonoid<T> => elements switch
{
[] => T.Zero,
[var first, ..var rest] => first + AddAll<T>(rest),
};
ふたつのケースを持つ switch 式です。ひとつは空のリスト [] に対して Zero を返却するケースで、Zero はインターフェースで定義されています。もうひとつは、最初の要素を var first として取得して、残りの要素をぜんぶ var rest で取得する(ふたつにスライスする)ケースです。
リストパターンについて、詳しくは「ドキュメント」を参照します。
シーケンスの参照
C# 11 のリストパターンの特徴のひとつは、シーケンスの要素を参照できることです。
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers is [1, 2, 3]); // True
Console.WriteLine(numbers is [1, 2, 4]); // False
Console.WriteLine(numbers is [1, 2, 3, 4]); // False
Console.WriteLine(numbers is [0 or 1, <= 2, >= 3]); // True
リストから任意の要素を取り出して利用するときは、破棄パターン _
を使うか、リストパターンで紹介した var パターンで要素を取り出します。
List<int> numbers = new() { 1, 2, 3 };
if (numbers is [var first, _, _])
{
Console.WriteLine($"The first element of a three-item list is {first}.");
}
// Output:
// The first element of a three-item list is 1.
先頭または末尾のデータを参照したいときは、リストパターン内でスライスパターン ..
を利用します。スライスパターンの動作は、次の例で特徴を確認します。
Console.WriteLine(new[] { 1, 2, 3, 4, 5 } is [> 0, > 0, ..]); // True
Console.WriteLine(new[] { 1, 1 } is [_, _, ..]); // True
Console.WriteLine(new[] { 0, 1, 2, 3, 4 } is [> 0, > 0, ..]); // False
Console.WriteLine(new[] { 1 } is [1, 2, ..]); // False
Console.WriteLine(new[] { 1, 2, 3, 4 } is [.., > 0, > 0]); // True
Console.WriteLine(new[] { 2, 4 } is [.., > 0, 2, 4]); // False
Console.WriteLine(new[] { 2, 4 } is [.., 2, 4]); // True
Console.WriteLine(new[] { 1, 2, 3, 4 } is [>= 0, .., 2 or 4]); // True
Console.WriteLine(new[] { 1, 0, 0, 1 } is [1, 0, .., 0, 1]); // True
Console.WriteLine(new[] { 1, 0, 1 } is [1, 0, .., 0, 1]); // False
スライスパターン ..
は0個以上の要素を参照できる、ということですね。
必須メンバー (Required members)
C# のいくつかのリリースで取り組まれてきたテーマのひとつに、オブジェクトの作成と初期化の改善があります。C# 11 では、この改良を必須メンバーで継続して取り組んでいます。
オブジェクトイニシャライザー(オブジェクト初期化子)を使用する型を作成するとき、以前は一部のプロパティを初期化することを必須であると指定することができませんでした。これからは、プロパティやフィールドに required を示すことができるようになります。これによって、その型のオブジェクトが作成されるとき、オブジェクトイニシャライザーによって初期化されなければいけない、ということを意味します。
public class Person
{
public required string FirstName { get; init; } // 初期化必須!
public string? MiddleName { get; init; } // 初期化できるけど必須じゃない
public required string LastName { get; init; } // 初期化必須!
}
FirstName と LastName を初期化せずに Person を作成すると、エラーになります:
var person = new Person { FirstName = "Ada" }; // Error: no LastName!
必須メンバーについて、詳しくは「ドキュメント」を参照します。
おまけ
C# 11 は .NET 7 環境でないと使えません。.NET 7 のライフサイクル(リリースの種類)は STS です。.NET 6 よりも、ちょい短いみたいです。
でも、気になる機能があって使いたい。でも、.NET 環境を変更できない……といったときは、(Web 開発の用語ですが)ポリフィル (Polyfill) という考えもあるみたいです。