はじめに
2024 年 11 月に .NET 9 とともに C# 13 が正式リリースされる予定です。今回の C# 年度アップデートは、主に ref struct を中心に多くの改善が行われ、生産性をさらに向上させる便利な機能もたくさん搭載されています。
この記事では、C# 13 に搭載する予定の機能を紹介していきます。
注意:この時点で C# 13 はまだ正式リリースされていませんので、以下の内容は変更される可能性があります。
イテレータと非同期メソッドに ref と ref struct
C# でプログラミングをするとき、結構 ref 変数と Span などの ref struct 型を使ったりしませんか?しかしイテレータと非同期メソッドの中にこれらを使えず、ローカル関数などを使ってイテレータと非同期メソッドで直接 ref 変数 ref struct 型の使用を回避しないといけないというとっても不便なところがありました。
この欠点が C# 13 で改善され、イテレータと非同期メソッドにも ref と ref struct が使えるようになりました!
イテレータに ref と ref struct を使う例:
IEnumerable<float> GetFloatNumberFromIntArray(int[] array)
{
for (int i = 0; i < array.Length; i++)
{
Span<int> span = array.AsSpan();
// 何かの処理を行う...
ref float v = ref Unsafe.As<int, float>(ref array[i]);
yield return v;
}
}
非同期メソッドに ref struct を使う例:
async Task ProcessDataAsync(int[] array)
{
Span<int> span = array.AsSpan();
// 何かの処理を行う...
ref int element = ref span[42];
element++;
await Task.Yield();
}
機能を示すために不適切かつ意味不明な「何かの処理」を使いましたが、とにかく ref と ref struct が使用できるようになりました!
ただ、一つ注意すべき点として、ref 変数と ref struct 型の変数は yield と await の境界に超えると使えなくなります。例えば以下の例はコンパイルエラーになります。
async Task ProcessDataAsync(int[] array)
{
Span<int> span = array.AsSpan();
// 何かの処理を行う...
ref int element = ref span[42];
element++;
await Task.Yield();
element++; // エラー:element へのアクセスが await の境界に超えている
}
ここまで話しましたが、そもそも ref と ref structってなにという疑問を抱える方はいらっしゃると思いますので、少し説明していきます。
C# では、ref を使って変数の参照を取得できます。これにより、参照を通じて元の変数を変更できます。以下に例を示します:
void Swap(ref int a, ref int b) // ref で参照を表す
{
int temp = a;
a = b;
b = temp; // ここまで a と b が交換された
}
int x = 1;
int y = 2;
Swap(ref x, ref y); // x と y の参照を取り、Swap を呼び出して x と y を交換する
一方、ref struct はスタック上にのみ存在できる値型を定義するためのものです。これは、ガベージコレクションのオーバーヘッドを避けるために使用されます。ただし、ref struct はスタック上にしか存在できないため、C# 13 以前にはイテレータや非同期メソッドなどのところに使用できません。
ちなみに、ref struct に ref が付いている理由は、ref struct のインスタンスがスタック上でのみ存在でき、そのライフタイムが従うルールは ref 変数と同じであるためです。
allows ref struct ジェネリック制約
従来では、ref struct はジェネリック型引数として使えないので、コードの再利用性を考慮してジェネリックを導入したものの、結局ref struct が使用できなくなり、Span や ReadOnlySpan のために同じ処理を再度書かなければならないという問題がありました。
これは結構ややこしいと思いませんでしょうか?
C# 13 では、ジェネリック型にも ref struct が使えようになりました:
using System;
using System.Numerics;
Process([1, 2, 3, 4], Sum); // 10
Process([1, 2, 3, 4], Multiply); // 24
T Process<T>(ReadOnlySpan<T> span, Func<ReadOnlySpan<T>, T> method)
{
return method(span);
}
T Sum<T>(ReadOnlySpan<T> span) where T : INumberBase<T>
{
T result = T.Zero;
foreach (T value in span)
{
result += value;
}
return result;
}
T Multiply<T>(ReadOnlySpan<T> span) where T : INumberBase<T>
{
T result = T.One;
foreach (T value in span)
{
result *= value;
}
return result;
}
そもそもなぜ ReadOnlySpan<T> ような ref struct 型が Func の型引数として使えるんでしょうか?これを調査するために .NET のソースコードを見ると、Func 型のジェネリック引数が以下のように定義されています:
public delegate TResult Func<in T, out TResult>(T arg)
where T : allows ref struct
where TResult : allows ref struct;
なるほど、ジェネリック引数に allow ref struct という制約を追加したら、その引数に ref struct 型を渡すことができるようになります。
そうだとしたらこれは確かに便利な機能ですね。
ref struct にもインターフェースを実装できる
C# 13 では、ref struct にインターフェースの実装が可能になりました。
この機能と allows ref struct と組み合わせてうまく利用すれば、ジェネリック型を介して参照も渡せますね:
using System;
using System.Numerics;
int a = 10;
// Ref<int> で a の参照を保存する
Ref<int> aRef = new Ref<int>(ref a);
// Ref<int> を渡す
Increase<Ref<int>, int>(aRef);
Console.WriteLine(a); // 11
void Increase<T, U>(T data) where T : IRef<U>, allows ref struct where U : INumberBase<U>
{
ref U value = ref data.GetRef();
value++;
}
interface IRef<T>
{
ref T GetRef();
}
// Ref<T> という ref struct にインターフェースを実装する
ref struct Ref<T> : IRef<T>
{
private ref T _value;
public Ref(ref T value)
{
_value = ref value;
}
public ref T GetRef()
{
return ref _value;
}
}
これで ref struct に関するコードを書くのがかなり楽になりましたね。ref struct で実装された列挙型にも IEnumerator などのインターフェースを実装できるようになりましたね。
コレクション型や Span にも params
従来では、params は配列型にしか使えないのですが、C# 13 から他のコレクション型や Span にも使えるようになりました。
params とは、メソッドが呼び出される際に任意の数の引数を直接指定できるようにする機能です。
例えば、
Test(1, 2, 3, 4, 5, 6);
void Test(params int[] values) { }
上記のように、任意の数の int 引数を直接指定できます。
C# 13 から、配列型以外に、他のコレクション型、Span、ReadOnlySpan 型やコレクションに関するインターフェースにも params を付けるようになりました:
Test(1, 2, 3, 4, 5, 6);
void Test(params ReadOnlySpan<int> values) { }
// あるいは
Test(1, 2, 3, 4, 5, 6);
void Test(params List<int> values) { }
// インターフェースもOK
Test(1, 2, 3, 4, 5, 6);
void Test(params IEnumerable<int> values) { }
これも便利です!
field キーワード
C# のプロパティを実装するとき、以下のようにいちいちフィールドを定義しないといけないときはよくありましたね...
partial class ViewModel : INotifyPropertyChanged
{
// フィールドを定義する
private int _myProperty;
public int MyProperty
{
get => _myProperty;
set
{
if (_myProperty != value)
{
_myProperty = value;
OnPropertyChanged();
}
}
}
}
そこで、C# 13 から field キーワードが助けになります!
partial class ViewModel : INotifyPropertyChanged
{
public int MyProperty
{
// field を使うだけで十分
get => field;
set
{
if (field != value)
{
field = value;
OnPropertyChanged();
}
}
}
}
自分でフィールドを定義する必要がなくなり、field キーワードを使えばフィールドが自動生成されます。
これもとても便利ですね!
部分プロパティ
C# を書くときよくあるある問題その一つ:プロパティに partial 修飾子を付けられません。
いきなり partial を説明すると意外とわからないかもしれませんので、既存の partial メソッドやpartial クラスについても説明していきます。
C# では、クラスやメソッドに partial を付くことで、宣言と実装を別々で行うことができます。また、クラスの各部分を分散することもできます。一番の用途としては、ソースジェネレーターなどの自動生成ツールに、何を生成するのかを指定するときに使います。
例えば:
partial class ViewModel
{
// ここはメソッドを宣言するだけ、実装の部分はツールで自動生成される
partial void OnPropertyChanged(string propertyName);
}
そして自動生成ツールが以下のようなコードを生成します:
partial class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
partial void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new(propertyName));
}
}
開発者はただ OnPropertyChanged を宣言するだけで十分、その実装がすべて自動生成されますので、開発者の手間が省けます。
C# 13 から、プロパティも partial を対応します:
partial class ViewModel
{
// 部分プロパティを宣言する
public partial int MyProperty { get; set; }
}
partial class ViewModel
{
// 部分プロパティの実装
public partial int MyProperty
{
get
{
// ...
}
set
{
// ...
}
}
}
これでプロパティもツールによって自動生成できるようになりましたね。
ロックオブジェクト
lock という機能はご存知の通り、モニターを通じてスレッド同期に使用する機能です。
object lockObject = new object();
lock (lockObject)
{
// 排他的なスコープ
}
しかしこの機能は、オーバーヘッドが大きく、パフォーマンスに影響を与えます。
これを解決するために C# 13 はロックオブジェクトが実装されました。この機能を使用するには、System.Threading.Lock でロックされるオブジェクトを入れ替えれば十分です:
using System.Threading;
Lock lockObject = new Lock();
lock (lockObject)
{
// 排他的なスコープ
}
これで簡単にパフォーマンスを向上することができますね。
初期化子での末尾からのインデックス
インデックス演算子 ^ は、集合の末尾に対する相対位置を示すために使用できます。C# 13 から初期化子でも機能します:
var x = new Numbers
{
Values =
{
[1] = 111,
[^1] = 999 // ^1 は末尾から一番目の要素
}
// x.Values[1] は 111
// x.Values[9] は 999、なぜなら Values[9] は最後の要素だからです
};
class Numbers
{
public int[] Values { get; set; } = new int[10];
}
エスケープ文字
Unicode 文字列に \u001b と \x1b の代わりに \e が使えようになりました。\u001b、\x1b、そして \e はすべてエスケープ文字を表します。これらは通常、制御文字を表現するために使用されます。
-
\u001bは Unicode エスケープシーケンスを表し、\uの後に続く 4 桁の 16 進数は Unicode コードポイントを表します -
\x1bは 16 進エスケープシーケンスを表し、\x の後に続く 2 桁の 16 進数は ASCII コードを表します -
\eはエスケープ文字そのものを表します
なぜ \e の使用がおすすめというと、16 進数における解釈の混乱を避けるためです。
例えば、\x1b に 3 がついてるの場合は、\x1b3 になってしまい、\x1b と 3 の間に明確な区切りがないため、\x1b と 3 を別々に解釈するべきか、それとも一緒に解釈するべきかが不明確になります。
代わりに \e を使えば、解釈の混乱を避けられます。
そのほか
上記の機能以外、メソッドグループでの自然型とメソッドオーバーロードにおける優先順位に関してもいくつの改善がありますが、この記事で省略します。さらに詳しく知りたい場合はドキュメントまで参照してください。
おわりに
C# が年々進化し続けていることが改めて実感されますね。今回 C# 13 のアップデートは、筆者にとってかなり実用かつ便利な機能がいっぱい実装されて、大変うれしいと思います。
.NET 9 と C# 13 の正式リリースを楽しみしています~
皆様はいかがでしょうか?