C# 7.2: in パラメータ修飾子

## はじめに
この記事は [CyberAgent Developers #2 Advent Calendar 2018](https://adventar.org/calendars/2953) の18日目の記事です。
昨日は [kazun9](https://kazun9.hatenablog.com/) さんの [バグをゼロにするためには何が必要か](https://kazun9.hatenablog.com/entry/2018/12/17/000245) でした。

株式会社サムザップで Unity/C# エンジニアをしている石井です。

つい先日、Unity 2018.3 がリリースされました。
このバージョンから C# 7.3 が正式にサポートされ[^1]、タプルやパターンマッチング等 C# の様々な新しい機能が利用出来るようになりました。
今回はそれら新しい機能のうちの `in` パラメータ修飾子について紹介したいと思います。

## in パラメータ修飾子
### 目的
参照型のインスタンス作成時などに行われる**ヒープメモリの割り当て**は一般的にコストが高い処理で、
同時にGC（ガベージコレクション）を誘発するため、パフォーマンスを意識したコードではなるべく避けるべきだとされています。

そのメモリ割り当ての回避のために、スタックに割り当てられGCの対象にならない、値型である構造体がよく使われています。
しかし値型は、変数への代入時やメソッドの引数に渡す際**インスタンス丸ごとのコピー**が走るため、
サイズの大きな構造体では無視できないコストとなっていました。

C# 7.2 まででも `ref` パラメータ修飾子を使うことで、値型を参照渡しにしコピーコストをカットすることは出来ましたが、
渡した先で書き換えられないことが保証されない等、少々使い辛い点があります。

```cs
// このメソッドは引数 bar を書き換えるのか？ 効率化のために参照渡しにしているのか？
// メソッドのシグネチャからは分からず、実装を追わないといけない。
void CallByRef(ref Bar bar) { /* do something */ }
```

それらの問題を解消し、安全かつ効率的に値型を使用できるようにした機能が `in` パラメータ修飾子です。

`in` パラメータ修飾子を付けた引数は、複製の発生する値渡しではなく参照渡しで渡されます。

```cs
void CallByReadOnlyRef(in Bar bar) { /* do something */ }

Bar bar = new Bar();
CallByReadOnlyRef(in bar); // bar のコピーではなく参照がメソッドに渡される
```

そして `in` パラメータ修飾子を付けた引数を受け取ったメソッドは、その引数の変更が禁止されます。

```cs
void CallByReadOnlyRef(in Bar bar) {
    bar.x = 0;  // フィールドの書き換え禁止、コンパイルエラーになる
}
```

### 使い方
`ref` や `out` パラメータ修飾子と同様に、メソッドの引数の型の前に `in` キーワードを追加します。

```cs
struct Bar { /* some field ... */ }

// 読み取り専用の参照渡しで引数を受け取るメソッド
void CallByReadOnlyRef(in Bar bar) { /* do something */ }

void Foo() {
    Bar bar = new Bar();
    // ローカル変数を in 引数と明示してメソッドを呼び出す
    CallByReadOnlyRef(in bar);  
}
```

一方 `ref` や `out` と違い、呼び出し側は `in` キーワードを省略することも出来ます。

```cs
struct Bar
{
    public static Bar Instance { get; }
}

CallByReadOnlyRef(bar);                // ローカル変数を指定
CallByReadOnlyRef(Bar.Instance);       // プロパティを直接指定
CallByReadOnlyRef(default(Bar));       // 型の既定値を直接指定
```

上記呼び出しはコンパイラにより、以下のようなコードに変換されます。

```cs
CallByReadOnlyRef(in bar);

Bar tmp1 = Bar.Instance;
CallByReadOnlyRef(in tmp1);

Bar tmp2 = default(Bar);
CallByReadOnlyRef(in tmp2);
```
詳しい例は[こちら](https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/in-parameter-modifier)を参照してください。

### 制限
`in` パラメータ修飾子を付けた引数は、**読み取り専用**と**参照**の2つの性質により、大きく以下の制限が課されます。

* 再代入不可
* （構造体のみ）フィールドの書き換え不可
* 非同期メソッド（`async`）や、イテレータメソッド（`yield`）の引数に渡せない

### 注意点
読み取り専用（`readonly` フィールド、`in` 引数）の構造体については、フィールドの書き換え不可という制限を守るために、
メソッド・プロパティ呼び出し時にコンパイラにより防衛的コピーが追加されます。

```cs
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` に変えることで回避できます。

```cs
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` のパラメータ修飾子を指定することは出来ません。

```cs
Action<in DateTime> action; // コンパイルエラーになる
```

代わりにデリゲートを定義することで委譲を実現することが出来ます。

```cs
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](https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-7)
[What's new in C# 7.1](https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-7-1)
[What's new in C# 7.2](https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-7-2)
[What's new in C# 7.3](https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-7-3)
[Write safe and efficient C# code | Microsoft Docs](https://docs.microsoft.com/en-us/dotnet/csharp/write-safe-efficient-code)
[readonly の注意点 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C](https://ufcpp.net/study/csharp/resource/readonlyness/)

[^1]: [Introducing Unity 2018.3 - Unity Blog](https://blogs.unity3d.com/jp/2018/12/13/introducing-unity-2018-3/)