参照関連の新機能
C# 11では、refフィールドとscoped refの利用が可能になりました。refフィールドは、構造体に置くことのできる参照型のフィールドで、Span<T>という構造体を実装するための機能の一つです。また、scoped refは、参照の安全な利用のための機能の一つです。単体で見ると用途が分かりにくい機能ですが、これらを理解するためにC# 7.0以降の参照関連の機能強化を絡めながら紹介していきます。
[NOTE]参照型
参照型は、値そのものではなく値の場所を保持する型です。これに対し値型は、値そのものを保持する型です。例えば、クラスは参照型で構造体やプリミティブ型は値型です。関数の引数にrefキーワードやoutキーワードを付与することによって、値ではなく参照を渡すこともできます(参照引数)。参照引数によって、大きな構造体を効率よく渡したり、関数から引数経由で値を受け取ることができます。
Span<T>構造体について
まず、Span<T>構造体について触れておきます。Span<T>構造体は、その名の通りメモリ上の連続した領域を指し、配列やマネージドメモリなどの一部あるいは全部を表現するために利用されます。このような用途の構造体あるいはクラスはほかにもありますが、Span<T>は効率よく高速な処理が要求される場面で利用されることを想定しています。そのため、やや低レベルな処理においてその真価を発揮します。C#は高水準なオブジェクト指向言語ですが、パフォーマンスが重視される場面にも適用できるように、このような機能が用意されています。
C# 11のSpan<T>構造体の実装は、大まかに以下のようになっています(本質的ではないのでアクセス修飾子などは省いています)。シンプルにいうと、T型の参照と、長さをフィールドとして持つ構造体です。
readonly ref struct Span<T> (1) { readonly ref T _reference; (2) readonly int _length; }
ここには、C# 7.2以降で実装された機能が幾つか見られます。(1)の「readonly ref struct」は変更不可のref構造体の定義、(2)の「readonly ref」は再代入不可のrefフィールドの宣言です。それぞれ、以降で見ていきます。
ref構造体(ref struct)[7.2]
C# 7.2では、配置場所がスタックに限定される、ref構造体を使えるようになりました。
構造体は値型であるので、通常はスタック上に配置されます。しかしながら、クラスのメンバになったりすると、クラスのインスタンスの一部として通常はヒープへ配置されてしまいます。上記のSpan<T>構造体は、その性質上、スタックへの配置が要求されることから、これでは具合が悪いということでref構造体が追加されました。ref構造体は、いかなるときにもスタックに配置されることが保証された構造体です。そのため、ヒープへ配置されないようにするための、以下に挙げるようなさまざまな制限を持っています。
- Box化できない
- 配列にできない
- クラスのメンバになれない
- 型引数になることができない
ref構造体は、ref構造体自身(C# 11ではrefフィールドに拡大)をフィールドに持つことができます。構造体の定義にrefキーワードを付与してref構造体であることを宣言します。Span<T>構造体はref構造体なので、ref構造体ではSpan<T>構造体のフィールドを持つことができます。逆をいうと、Span<T>構造体のフィールドを持てるのはref構造体のみです。通常の構造体では持つことができません。
以下は、Span<T>構造体のフィールドspanを持つref構造体Sの使用例です。
ref struct S { // ref構造体 public Span<string> span; // ref構造体Spanのフィールド public S() { span = new string[10]; // 10個のstring型要素で初期化 } } var s = new S(); s.span[0] = "Hello"; // 配列と同じように使用できる Console.WriteLine($"{s.span[0]}"); // 実行結果:Hello
Span<T>構造体については、次回でパターンマッチングの新機能とともに改めて紹介します。
refフィールド(ref fields)[11.0]
C# 11では、ref構造体のみに限定されますが、参照型のフィールドを使えるようになりました。
C# 10までは、ref構造体に置ける参照はref構造体のみでした。C# 11では、これが拡張されてref構造体に限らない参照フィールドを置くことができるようになりました。refフィールドは、通常のフィールドの宣言にrefキーワードを付与して宣言します。上記のSpan<T>構造体の定義イメージでは、このrefフィールドによってT型の参照をフィールドとして配置しています。
以下は、refフィールドの使用例です。構造体Rのインスタンス生成でscopedキーワードをエラー回避のために付与しています。scopedキーワードについては後述します。
ref struct R { public ref int iref; // refフィールド public ref string? sref; } var ivalue = 100; var svalue = "Hello"; scoped var r = new R(); // scopedキーワードについては後述 r.iref = ref ivalue; r.sref = ref svalue;
refフィールドが導入されていないC# 10までのSpan<T>の実装は、参照としてのフィールドを内部で特別扱いすることで目的の機能を達成していました。C# 11では、refフィールドにより参照型フィールドを自然な形で構造体に置くことができるようになったというわけです。
参照戻しとrefローカル変数[7.0]
C# 7.0では、関数の戻り値とローカル変数を、参照型とすることができるようになりました。
ここで、C# 7.0で利用可能になった、参照戻しとrefローカル変数について紹介しておきます。これらは、関数の戻り値とローカル変数を、参照型とします。これにより、参照引数を参照のまま取り扱い、関数の実行結果として返すことができます。関数内部でも値のコピーが発生しにくくなるので、パフォーマンス的に有利となります。
以下は、参照引数をrefローカル変数で受け取り、値を書き換えて参照戻しとして返す関数の例です。呼び出し側の変数aが書き換わる点に注目です。
static ref int func(ref int x) { ref var y = ref x; // x, yともにaへの参照 y = 200; // yはaへの参照なのでaの値が書き換わる return ref y; // aへの参照が返る } var a = 100; Console.WriteLine($"a={a},ret={func(ref a)},a={a}"); // 実行結果:a=100,ret=200,a=200
このように、くどいほどにrefキーワードの付与が必要になります。呼び出し側の実引数にrefキーワードが必要なのは従来どおりですが、参照のままの受け渡しを明示的に記述することで、意識しない代入などによる不具合をあらかじめ防止するためと言えます。
[NOTE]エスケープ解析
例えば関数から参照戻しで結果を受け取る場合、その参照先は関数終了後も存在している必要があります。これを含めて、参照(ポインタ)を追跡してその有効性をコンパイル段階で調べることをエスケープ解析といいます。これにより、あるスコープにおいて参照先が有効であるかどうか(エスケープ=逃げ出されていないか)コンパイル段階で確認することができます。参照戻しやrefローカル変数、refフィールドなどを安全に使うには必須の機能です。
スコープされるref(scoped ref)[11.0]
C# 11では、参照の有効なスコープを明示するscoped refを使えるようになりました。
参照は、エスケープ解析によって、あるスコープで有効か調べられますが、このとき参照が有効なスコープを明示するのがscopedキーワードです。参照引数、refローカル変数ともに、scopedキーワードが付与されると、それはその関数外に値を漏らさない(エスケープしない)参照(scoped ref)であることを明示的に指定したことになります。
エスケープ解析では可能な限り参照を追跡しようとするので、上記のrefフィールドのサンプルのように、インスタンスが別のスコープにまで渡される可能性がある場合にはエラーとなってしまいます。このようなときに、インスタンスにscopedキーワードを付与することで、そのスコープの外には持ち出されないことを明示し、エラーを回避するのです。
以下は、scopedキーワードを付与された参照引数とrefローカル変数を関数が戻そうとしたときにエラーとなることを確かめる例です。
static ref int func1(scoped ref int x) { return ref x; // error CS9075: 現在のメソッドに範囲指定されているため、 // 参照渡し 'x' でパラメーターを返すことはできません } static ref int func2(ref int x) { scoped ref int y = ref x; return ref y; // error CS8157: 'y' は参照渡しで返せない値に初期化されたため、 // 参照渡しで返すことができません }
再代入不可のrefフィールド[11.0]
C# 11では、再代入のできないrefフィールドを指定できるようになりました。
refフィールドでは、再代入不可をreadonlyキーワードで指定することができます。これにより、上記のSpan<T>構造体の定義イメージのように、参照先の変更ができないrefフィールドを置くことができます。参照先の変更ができないので、エスケープ解析による追跡の精度が向上するというメリットがあります。なお、参照先の変更ができず、参照先の値の書き換えもできないrefフィールドを、readonly ref readonlyとして記述可能です(readonly ref+readonly)。
以下は、再代入不可のrefフィールドと値の書き換えもできないrefフィールドへの代入でエラーとなる例です。
ref struct S { public readonly ref int x; // 参照が変更不可 public readonly ref readonly int y; // 値も参照も変更不可 public S(ref int _x, ref int _y) { x = ref _x; y = ref _y; } } var a = 100; var b = 200; var c = 300; var s = new S(ref a, ref b); s.x = ref c; // error CS0191: 読み取り専用フィールドに割り当てることはできません s.y = c; // error CS8331: フィールド に割り当てることができません。