- 概要
- Constraints on type parameters について
- interfaceを継承した構造体のboxing
- Generic Interfaceによる構造体のBoxingの回避
- 余談:なぜBoxingが必要なのか
- 雑感
どうも、最近GC Allocにおびえているすぎしーです。
今日はUnityじゃなくてC#がメインの話題です。
概要
今回はGeneric Interfaceと値型のBoxing回避についてお話します!
Boxingは余計なGC Allocが発生してしまいますが、回避が難しいパターンも存在します。
ただ、一見変わった方法でそのBoxingを回避する方法があったので紹介したいと思います!
Constraints on type parameters について
要するにGenericで指定される型に制約を付けられるC#の機能のことです。
C#では、where句を使うことでGenericの型に制約を付けることができます。
例:structで制約を付けた場合
例えば、以下のようにwhere T : structとつけると、Tの指定をstruct(構造体)のみに制限させることができます。
public class GenericData<T> where T : struct { T Value { get; } }
以下のようなコードで確認することができます。
// エラーなし GenericData<StructSample> structData; public struct StructSample { }
// エラー: The type 'ClassSample' must be a non-nullable value type in order to use it as parameter 'T' in the generic type or method 'GenericData<T>' GenericData<ClassSample> classData; public class ClassSample { }
// エラー: The type 'IInterfaceSample' must be a non-nullable value type in order to use it as parameter 'T' in the generic type or method 'GenericData<T>' GenericData<IInterfaceSample> interfaceData public interface IInterfaceSample { }
interfaceを継承した構造体のboxing
C#の構造体はinterfaceを継承することができます。
// ILoggerを継承した構造体 public struct Data : ILogger { public int value; public void Log() { Debug.Log("I am `Data`"); } } public interface ILogger { void Log(); }
さて、このinterfaceを継承した構造体ですが意図しないBoxingがあっさり発生します。。。
var data = new Data { value = 0, };
// ILogger型の変数に構造体Dataを渡す
ILogger logger = data;
intefaceを引数としたメソッドに渡した場合のBoxing
先述したBoxingですが、引数の型がinterfaceのメソッドに引数として渡しても発生します。
var data = new Data { value = 0 }; Func(data); // 引数として渡す // 引数の型がinterfaceのメソッド public void Func(ILogger logger) { // 何もしない }
なぜBoxingが発生したのか
そもそも「Boxingは参照型の変数に値型のデータを渡すことで発生」します。
var data = new Data { value = 0 }; // 参照型変数のloggerに値型を渡すためBoxingが発生 ILogger logger = data
interfaceの変数もclass, delegate同様参照型として扱われます。
interfaceを継承している構造体であって、参照型にするにはBoxingを行って参照型に変換してやる必要があります。
※「なぜBoxingが必要なのか」を後述しています。
Generic Interfaceによる構造体のBoxingの回避
さて、いよいよ本題です!
先述の「intefaceを引数としたメソッドに渡した場合のBoxing」に限っては回避方法があります!
それはFunc(ILogger logger)メソッドを以下のように書き換えるだけです。
public void Func<T>(T logger) where T : ILogger { // 何もしない }
ポイントは where T : ILogger の部分で"Generic Interface"と呼ばれるものです。
Generic Interfaceとは
Generic InterfaceはGenericの制約にinterfaceを指定したもののことです。
上記参考ページでも以下のような記述があり、Generic Interfaceを使うことでBoxingの回避が可能なことがわかります。
The preference for generic classes is to use generic interfaces, such as IComparable<T> rather than IComparable, in order to avoid boxing and unboxing operations on value types.
なぜBoxingが回避されたのか
Generic型の引数は渡されたデータの型になります。
where T : ILoggerとinterfaceの制約が付いたとしてもFunc<T>(T logger)のTはあくまで引数の型、つまり構造体Dataになります。
つまり、「引数Tが値型となり、参照型への変換がそもそも不要のため、Boxingも発生しなかった」ということになります。
逆にFunc(ILogger logger)の場合は引数は参照型で固定のため、値型を渡してしまうとBoxingが発生してしまったということだったんですね。
Generic InterfaceでのBoxing回避はメソッド限定
この回避方法はメソッドのみ限定で可能です。
後述していますが「メソッドのコールスタック」と「メソッドの引数」は基本的にスタック領域に置かれます。
Generic型に値型を指定した場合、Tは参照型に変換されることなくそのままスタック領域の格納され、 メソッドからはその領域のデータにアクセスされます。
メソッドもその引数もスタック領域に置かれているため、実現できた回避方法と言えそうです。
余談: OpCodes.Constrained Field
さらに細かく話すとGeneric Interfaceの場合、以下の特殊なオペコードが使われるようになりスタック領域のデータが参照されるようです。
気になる方はどうぞ~。
docs.microsoft.com stackoverflow.com
余談:なぜBoxingが必要なのか
Boxingは値型のデータを参照型として扱うために必要な機能と言えます(意図しないBoxingの場合は別ですが)。
参照型(objectなど)と値型(struct)の大きな違いとして、その「データを格納する領域」に違いがあります。
| データの格納場所 | |
|---|---|
| 値型(Value Type) | スタック領域(一時領域)、ヒープ領域(永続領域)の両方 |
| 参照型(Reference Type) | 基本的にヒープ領域(永続領域)のみ |
| 関数のコールタック(余談) | スタック領域(一時領域) |
スタック領域は一時的、ヒープ領域は永続的にデータを保持します。
そして、Boxingは「一時的なスタック領域にある値型」を「ヒープ領域に移して参照型にする」ために行います。
コードとイメージは以下のような感じです。
// ヒープ領域に確保 var loggerHolder = new LoggerHolder(); // ローカル変数のためスタック領域 var data = new Data() { a = 1f, b = 2f, c = 3f, d = 4f, e = 5f }; // dataのBoxingを行い、ヒープ領域確保、データをコピーして永続化 loggerHolder.logger = data

参照型はいつ参照しても期待するデータにアクセスできないといけません。
interfaceの変数は参照型のため、構造体が対象のinterfaceを継承していたとしても必ずヒープ領域に持っていって参照型にしておく必要があります。
これがBoxingが発生する主理由の1つと言えます。 (他にもスタック領域は急に領域を確保できないからヒープ領域を使うなど有りますが、細かくは割愛しますw)。
雑感
1つのGC Allocの回避を紹介するのに、ずいぶんと長文になってしまいました。。。
記事を1週間以上投稿しなかったのは、夏バテが原因だった気がします(言い訳)。
まあ、使命感でやってるわけではないので気長にやっていきます。
皆さんのC#の知識向上につながれば幸いですー。 それでは~。


