はじめに
この記事は CyberAgent Developers #2 Advent Calendar 2018 の18日目の記事です。
昨日は kazun9 さんの バグをゼロにするためには何が必要か でした。
株式会社サムザップで Unity/C# エンジニアをしている石井です。
つい先日、Unity 2018.3 がリリースされました。
このバージョンから C# 7.3 が正式にサポートされ1、タプルやパターンマッチング等 C# の様々な新しい機能が利用出来るようになりました。
今回はそれら新しい機能のうちの in
パラメータ修飾子について紹介したいと思います。
in パラメータ修飾子
目的
参照型のインスタンス作成時などに行われるヒープメモリの割り当ては一般的にコストが高い処理で、
同時にGC(ガベージコレクション)を誘発するため、パフォーマンスを意識したコードではなるべく避けるべきだとされています。
そのメモリ割り当ての回避のために、スタックに割り当てられGCの対象にならない、値型である構造体がよく使われています。
しかし値型は、変数への代入時やメソッドの引数に渡す際インスタンス丸ごとのコピーが走るため、
サイズの大きな構造体では無視できないコストとなっていました。
C# 7.2 まででも ref
パラメータ修飾子を使うことで、値型を参照渡しにしコピーコストをカットすることは出来ましたが、
渡した先で書き換えられないことが保証されない等、少々使い辛い点があります。
// このメソッドは引数 bar を書き換えるのか? 効率化のために参照渡しにしているのか?
// メソッドのシグネチャからは分からず、実装を追わないといけない。
void CallByRef(ref Bar bar) { /* do something */ }
それらの問題を解消し、安全かつ効率的に値型を使用できるようにした機能が in
パラメータ修飾子です。
in
パラメータ修飾子を付けた引数は、複製の発生する値渡しではなく参照渡しで渡されます。
void CallByReadOnlyRef(in Bar bar) { /* do something */ }
Bar bar = new Bar();
CallByReadOnlyRef(in bar); // bar のコピーではなく参照がメソッドに渡される
そして in
パラメータ修飾子を付けた引数を受け取ったメソッドは、その引数の変更が禁止されます。
void CallByReadOnlyRef(in Bar bar) {
bar.x = 0; // フィールドの書き換え禁止、コンパイルエラーになる
}
使い方
ref
や out
パラメータ修飾子と同様に、メソッドの引数の型の前に in
キーワードを追加します。
struct Bar { /* some field ... */ }
// 読み取り専用の参照渡しで引数を受け取るメソッド
void CallByReadOnlyRef(in Bar bar) { /* do something */ }
void Foo() {
Bar bar = new Bar();
// ローカル変数を in 引数と明示してメソッドを呼び出す
CallByReadOnlyRef(in bar);
}
一方 ref
や out
と違い、呼び出し側は in
キーワードを省略することも出来ます。
struct Bar
{
public static Bar Instance { get; }
}
CallByReadOnlyRef(bar); // ローカル変数を指定
CallByReadOnlyRef(Bar.Instance); // プロパティを直接指定
CallByReadOnlyRef(default(Bar)); // 型の既定値を直接指定
上記呼び出しはコンパイラにより、以下のようなコードに変換されます。
CallByReadOnlyRef(in bar);
Bar tmp1 = Bar.Instance;
CallByReadOnlyRef(in tmp1);
Bar tmp2 = default(Bar);
CallByReadOnlyRef(in tmp2);
詳しい例はこちらを参照してください。
制限
in
パラメータ修飾子を付けた引数は、読み取り専用と参照の2つの性質により、大きく以下の制限が課されます。
- 再代入不可
- (構造体のみ)フィールドの書き換え不可
- 非同期メソッド(
async
)や、イテレータメソッド(yield
)の引数に渡せない
注意点
読み取り専用(readonly
フィールド、in
引数)の構造体については、フィールドの書き換え不可という制限を守るために、
メソッド・プロパティ呼び出し時にコンパイラにより防衛的コピーが追加されます。
struct A {
public int Value;
public void X() { /* do something */ };
}
void CallByReadOnlyRef(in A a) {
a.X(); // メソッド呼び出し自体は可能
}
// 上のメソッドは以下のようなコードに変換される
void CallByReadOnlyRef(in A a) {
// a からコピーした tmpA に対して メソッド呼び出しを行うことで、参照元が変更されないようにしている
A tmpA = a;
tmpA.X();
}
このように in
引数に通常の構造体を渡した場合、メソッド or プロパティ呼び出し毎にコピーが作成されてしまいます。
これの対処はフィールドにだけしかアクセスしないか、
構造体の宣言を struct
から、変更できないことを示す readonly struct
に変えることで回避できます。
readonly struct A {
public readonly int Value;
public void X() { /* do something */ }
}
void CallByReadOnlyRef(in A a) {
a.X(); // readonly struct A は不変なので参照元が変更される恐れは無く、コピーは作られない
}
小ネタ
処理を委譲するのによく使われる System.Action
や Sytem.Func
型ですが、
これらの引数に、ref
, out
, in
のパラメータ修飾子を指定することは出来ません。
Action<in DateTime> action; // コンパイルエラーになる
代わりにデリゲートを定義することで委譲を実現することが出来ます。
delegate void ActionDelegate(in DateTime);
ActionDelegate action;
action = (in DateTime dt) => { /* do something */ };
確認環境
Visual Studio 2017 15.9.4
参考
What's new in C# 7.0
What's new in C# 7.1
What's new in C# 7.2
What's new in C# 7.3
Write safe and efficient C# code | Microsoft Docs
readonly の注意点 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C