C# の ref まとめ
C#7.2 までの参照渡し関係のまとめです。
C# 7 系で参照渡しの扱いが強化されて種類も増えました。 上手につかうとサイズの大きい値型のコピーを避けられるのでまとめてみました。 動作をきちんと理解するために C# to C# の変換をしたコードや IL をのせています。
予備知識 - defensive copy, readonly struct
defensive copy - 防衛的なコピー
readonly
指定された値型は値が変化しないことを保証するために、コンパイラが値を防衛的にコピーしている場合がある。
defensive copy が発生するのは下記の場合に、後述する readonly struct
ではないふつうの struct
を使用しているとき
- readonly フィールドとして構造体を持っている場合
- readonly な参照渡しで構造体を返すとき
例
こんな構造体があった場合を考える。
public struct Point
{
public double X;
public double Y;
// フィールドを書き換えるメソッド
public void Set(double x, double y)
{
X = x;
Y = y;
}
}
readonly フィールドでない場合
たとえば、このようなクラスでは防御的コピーは発生しない(readonly でないので、構造体のフィールドを書き換えることに制限はない)
public static class MyClass
{
// readonly でないフィールド
private static Point s_origin = default;
public static void Sample()
{
// フィールド書き換え
s_origin.Set(1, 1);
// 実際に書き換わっている
Console.WriteLine($"X: {s_origin.X}, Y: {s_origin.Y}");
}
}
IL を見ると、こんな感じ
MyClass.Sample:
IL_0000: nop
; s_origin の「アドレス」をスタックに push
IL_0001: ldsflda MyClass.s_origin
; set メソッドの呼び出し
IL_0006: ldc.r8 00 00 00 00 00 00 F0 3F
IL_000F: ldc.r8 00 00 00 00 00 00 F0 3F
IL_0018: call Point.Set
IL_001D: nop
readonly フィールドの場合
public static class MyClass
{
// 原点の座標を何度も使うので readonly フィールドにもつ
private static readonly Point s_origin = default;
public static void Sample()
{
// 構造体のフィールドは readonly を受け継ぐので書き換えできない
// s_origin.X = 1;
// フィールドを書き換えるかもしれないメソッドは呼べるように見える
s_origin.Set(1, 1);
// 実際には書き換わっていない
Console.WriteLine($"X: {s_origin.X}, Y: {s_origin.Y}");
}
}
s_origin.Set()
メソッドを呼んでもフィールドが書き換わっていないが、 これは(フィールドを変更しているかもしれない)メソッド呼び出しを許容しつつ、readonly であることを保証するために、いったん s_origin
をコピーしてから、そのコピーに対してメソッドを呼ぶため。
「メソッドの中でなにも書き換えていない」ことは呼び出し側から知るすべがないので、実際にコピーが必要かどうかにかかわらず常にコピーが発生する。 readonly なフィールドや readonly な参照 (in 引数) を使用するときは注意が必要。
IL をみると、ローカル変数に値をコピーしてからメソッドを読んでいる
MyClass.Sample:
IL_0000: nop
; 値をローカル変数にコピー
IL_0001: ldsfld MyClass.s_origin
IL_0006: stloc.0
; ローカル変数のアドレスをスタックに push
IL_0007: ldloca.s 00
; ローカル変数にたいして Set() を呼ぶ
IL_0009: ldc.r8 00 00 00 00 00 00 F0 3F
IL_0012: ldc.r8 00 00 00 00 00 00 F0 3F
IL_001B: call Point.Set
IL_0020: nop
readonly struct
防衛的なコピーは、下記のようにすべてのフィールドを readonly にしていても発生する。
struct NoReadOnlyPoint
{
// X, Y は readonly
public readonly double X;
public readonly double Y;
// フィールドや this の書き換えを行うメソッドは持たないが、
// 呼び出し側からはフィールドの書き換えを行っていないことを知るすべがないため、
// readonly な NoReadOnlyPoint のインスタンスに対して Hoge() を呼ぶと、常に defensive copy が発生する
public void Hoge()
{
// ...
}
}
下記のように readonly struct
とすることによって、フィールドの書き換えが起こらないことを保証でき、 defensive copy を避けられる
readonly struct ReadOnlyPoint
{
// readonly なフィールドのみ許容される
// get-only プロパティも、結局 readonly フィールドを生成するので許容
public readonly double X;
public readonly double Y;
// フィールドや this の書き換えを行うメソッドは持てないので、
// 呼び出し側で defensive copy の必要がない
public void Hoge()
{
// ...
}
}
呼び出し例
public static class MyClass
{
// 原点の座標を何度も使うので readonly フィールドにもつ
private static readonly NoReadOnlyPoint s_noReadonlyOrigin = default;
private static readonly ReadOnlyPoint s_readonlyOrigin = default;
public static void Sample()
{
// 防衛的なコピーが発生
s_noReadonlyOrigin.Hoge();
// 防衛的なコピーが発生しない
s_readonlyOrigin.Hoge();
}
}
IL
MyClass.Sample:
IL_0000: nop
// readonly struct でない場合はコピーが発生
IL_0001: ldsfld MyClass.s_noReadonlyOrigin
IL_0006: stloc.0
IL_0007: ldloca.s 00
IL_0009: call NoReadOnlyPoint.Hoge
IL_000E: nop
// readonly struct の場合はコピーが発生しない
IL_000F: ldsflda MyClass.s_readonlyOrigin
IL_0014: call ReadOnlyPoint.Hoge
IL_0019: nop
IL_001A: ret
生成される C# コード
readonly struct
にした構造体には、コンパイラが [IsReadOnly] 属性をつける
[IsReadOnly]
private struct ReadOnlyPoint
{
public readonly double X;
public readonly double Y;
public void Hoge()
{
}
}
この属性によって readonly struct かどうかの判定をおこなうようだ
参照渡しの種類一覧
種類 | 使う場所 | 書き換え | C#のバージョン |
---|---|---|---|
ref parameters | 引数 | o | 1.0? |
out parameters | 引数 | o | 1.0? |
in parameters | 引数 | x | 7.2 |
ref returns | 戻り値 | o | 7.0 |
ref returns (readonly) | 戻り値 | x | 7.2 |
ref locals | ローカル変数 | o | 7.0 |
ref locals (readonly) | ローカル変数 | x | 7.2 |
参照引数についての詳細
ref parameters
読み書き両方できる参照渡しの引数。 渡す前にかならず初期化が必要
x と y を交換するメソッドの例:
void Main()
{
// 必ず初期化しておく
int x = 1;
int y = 2;
Swap(ref x, ref y); // x: 2, y: 1
}
public static void Swap<T>(ref T x, ref T y)
{
T tmp = x;
x = y;
y = tmp;
}
out parameters
出力用の参照引数。 渡す前に初期化が不要で、 C# 7 では out-var で変数の宣言と同時に受け取れる
void Main()
{
var list = new List<int>() { 1, 2, 3, 4 };
// 出力引数のうけとり
// C#7 からは 受けとりと同時に変数の宣言が可能
if (TryGetAt(list, 1, out var elem))
{
Console.WriteLine(elem);
}
else
{
Console.WriteLine("not found");
}
}
// IList<T> の指定したインデックスの値を返す
private static bool TryGetAt<T>(IList<T> list, int index, out T elem)
{
if (list.Count > index)
{
// out 引数に結果を入れる
elem = list[index];
return true;
}
else
{
// out 引数は必ず初期化しなければならない
elem = default;
return false;
}
}
in parameters
読み取り専用の参照渡し引数。 値渡しで発生する構造体のコピーを避けつつ、 ref で参照渡ししたときの書き換えのリスクもなくす。 ただし、予備知識に書いたとおり、 readonly struct でない値型を受け取ったときに、プロパティやメソッドの呼び出しを行うと無条件にコピーが発生するので注意。
例:
using System;
public class Program
{
static void F(in int x)
{
// 読み取り可能
Console.WriteLine(x);
// 書き換えようとするとコンパイル エラー
x = 2;
}
// 補足: in 引数はオプションにもできる
static void G(in int x = 1)
{
}
static void Main()
{
int x = 1;
// ref 引数と違って修飾不要
F(x);
// 明示的に in と付けてもいい
F(in x);
// リテラルに対しても呼べる
F(10);
// 右辺値(式の計算結果)に対しても呼べる
int y = 2;
F(x + y);
// in のオプション引数を省略した呼び出し
G();
}
}
コンパイル後は結局は ref に変換される
using System;
using System.Runtime.CompilerServices;
public class Program
{
// [IsReadOnly] がついた ref になる
private static void F([IsReadOnly] ref int x)
{
// 読み取り可能
Console.WriteLine(x);
}
private static void G([IsReadOnly] ref int x = 1)
{
}
private static void Main()
{
// in で修飾してもしなくても結局ただの ref になる
int num = 1;
Program.F(ref num);
Program.F(ref num);
// リテラルに対しての呼び出しは ローカル変数が作られて、その参照が渡される
int num2 = 10;
Program.F(ref num2);
// 式の計算結果に対して呼ぶ場合は先に式の計算結果をローカル変数に入れておいて、その参照が渡される
int num3 = 2;
num2 = num + num3;
Program.F(ref num2);
// オプション引数を省略した場合は、デフォルト値の参照が渡される
num2 = 1;
Program.G(ref num2);
}
}
(サンプルコードはこちらから引用させていただきました)
参照戻り値、参照ローカル変数
C# 7 から、戻り値とローカル変数にも参照渡しが使えるようになった。 C# 7.2 からは readonly な参照を返すこともできる
通常の ref returns では、readonly なフィールドを返すことはできないが, readonly な ref returns では、 readonly なフィールドを返せる。
using System;
public class Program
{
public void Sample()
{
var user = new User("hanako");
// 書き換えできる参照
ref var mutableId = ref user.MutableId;
mutableId = Guid.NewGuid();
// 書き換えできない参照
ref readonly var immutableId = ref user.ImmutableId;
// immutableId = Guid.NewGuid(); // 代入できない
// これは値渡し
var idValue = user.Id;
var idValue2 = user.MutableId;
var idValue3 = user.ImmutableId;
}
}
public class User
{
private Guid _id;
public string Name { get; }
// これは値渡し
public Guid Id => _id;
// 書き換えできる参照を返す
public ref Guid MutableId => ref _id;
// readonly な参照を返す
public ref readonly Guid ImmutableId => ref _id;
public User(string name)
{
_id = Guid.NewGuid();
Name = name;
}
}
コンパイルすると、 readonly でも readonly でなくてもどちらも同じコードになる (IsReadOnlyAttribute
がつく)。 ポインタをつかった unsafe コードが生成される。
using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Security.Permissions;
[assembly: AssemblyVersion("0.0.0.0")]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[module: UnverifiableCode]
public class Program
{
public unsafe void Sample()
{
User user = new User("hanako");
Guid* mutableId = user.MutableId;
*mutableId = Guid.NewGuid();
Guid* immutableId = user.ImmutableId;
Guid id = user.Id;
Guid guid = *user.MutableId;
Guid guid2 = *user.ImmutableId;
}
}
public class User
{
private Guid _id;
[DebuggerBrowsable(DebuggerBrowsableState.Never), CompilerGenerated]
private readonly string <Name>k__BackingField;
public string Name
{
[CompilerGenerated]
get
{
return this.<Name>k__BackingField;
}
}
public Guid Id
{
get
{
return this._id;
}
}
public unsafe Guid* MutableId
{
get
{
return ref this._id;
}
}
[IsReadOnly]
public unsafe Guid* ImmutableId
{
[return: IsReadOnly]
get
{
return ref this._id;
}
}
public User(string name)
{
this._id = Guid.NewGuid();
this.<Name>k__BackingField = name;
}
}
参考
- C# 7 Series, Part 6: Read-only structs - Mark Zhou's Tech Blog
- C# 7 Series, Part 7: Ref Returns - Mark Zhou's Tech Blog
- C# 7 Series, Part 8: “in” Parameters - Mark Zhou's Tech Blog
- 参照渡し - C# によるプログラミング入門 | ++C++; // 未確認飛行 C
- readonly の注意点 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C
- What's New in C# 7 - C# Guide | Microsoft Docs
- What's new in C# 7.2 | Microsoft Docs
- Reference semantics with value types | Microsoft Docs