はじめに
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 の正式リリースを楽しみしています~
皆様はいかがでしょうか?