この記事はC# Advent Calendar 2019の15日目です。
初投稿です。
TL;DR
- C#8.0でInterfaceにも実装できるようになった
- しかしstructとの相性が悪い仕様でBoxingされる
- 条件によってはされないこともある?
Default Interface Methods とは
Default Interface Methods (以下DIMs) とは、C#8.0の新機能の一つで、今まで宣言しかできなかったinterfaceでメソッドを実装できるようになりました。JavaのDefault Methodsのようなものです。
https://github.com/dotnet/csharplang/blob/master/proposals/csharp-8.0/default-interface-methods.md
public interface IDim
{
//+1して返すだけの関数
int Inc(int x) => x+1;
}
これによって何がうれしいかといえば、
- interfaceにmethodまたはpropertyを後から足しても、既存のclass/structがコンパイルエラーにならない
- interfaceで実装することで継承先の実装の手間が省ける
というところがあります。
特に1が重要で、ライブラリなど第三者が利用するようなinterfaceにmethodを追加することが破壊的変更になっているため、なかなか追加しづらいという問題が解決されます。
structのgenericでの展開
もともとstructと素の(C#7.3以前の)interfaceはgenericを使うことでstructが展開されるため
- boxingされない
- 脱仮想化(devirtualize)される
- またmethodによってはinlineまでかかる
というパフォーマンス上のメリットがあります。
interface IA { int One(); }
struct B : IA { int One() => 1;}
int GetOne<TA>(TA a) => a.One();
//というのがあったとすると
var b = new B();
GetOne(b);
//とつかうとGetOne()内で直接B.One()というメソッドが呼ばれます
IA a = new B();
GetOne(a);
//とつかうとGetOne()内ではIA.One()から仮想関数として呼ばれる
classでは継承などでどのmethodが使われるか変わる可能性があるため仮想関数テーブルからmethodを呼び出します。そしてそもそもheap上に乗っています。
しかしstructは継承不可のため実装されているinterfaceのメソッドが決まり、またstack上にあり、仮想的に扱おうとするとheapに乗せられる(boxing)ためそれを回避するために展開されます。
(genericではなくinterfaceな変数として宣言するとboxingします。)
#DIMsでは?
しかしDIMsではstruct自体にmethodの実装がないです。ということで呼び出す関数をinterfaceから持ってこなければいけないのでboxingしなければなりません。
原理的にはstructにないmethodもboxingしないでもってこれますが…
interface-methods-vs-structsで結論づいているように言語仕様としてboxingを回避しないことを決め、これに対する変更は破壊的変更になるから今後これを覆すつもりがない、ということになりました。
(上のリンクによると、できるけどコストが割に合わないだろうということで回避しないそうです。)
これはなかなか困ったことで、まずboxingされるとそのメソッドでのstruct内部の状態変化(副作用)は無視されます。これは割と混乱するかもしれません。
さらにref structにinterfaceを付けれるようにするためのref interfaceの提案などでも
A ref interface cannot define DIMs (alternative: can only define a DIM which is guaranteed inlineable and does not box).
などと今後のパフォーマンス改善系の言語拡張の方向と相性が悪そうです。C#7.2から続く、heapへのallocationをなるべく回避することで高速化する、という言語の流れと逆行してるように感じますが…。
一応struct側で実装があればboxingされないのでdimをただコンパイルエラーをさけるための応急処置と考えれば使えそうです。
またUnityのJobSystem(というよりBurst Compilerがmanaged禁止のため)で使えなさそうですが…
そこらへんはBurstCompilerなりil2cppで頑張ってくれることを願ってます。
とりあえずsharplabでどのようなコードになるか見てみましょう。
...あれ?
Boxingされていない?
JITの結果を見る限り、空のstructでのdimは、なぜかboxingされていないように見えます
これはフィールドがない空のstructだから特殊処理されたのだろうと思うのですが…
こういうところの仕様が謎です。
これならshape and extension(特にstaticなメソッド周り)などの将来的機能には一応使える?
ただ仕様が分からないので将来的にどうなるか分からないのが不安ですね。
いまのところは「空のstructならboxingされない」ということで使いましょう。
追記(2019/12/16)
空じゃなくてもboxingされてないですね。
どうやらDIMなメソッドがinlineできるかできないかでboxingされるかされないかが決まるみたいです。
https://sharplab.io/#gist:cf33d44e64b869406abeff742ac3b8a3
よく考えてみたらinline化できるメソッドならそのメソッドがあるinterfaceの実体は必要なくなるってことですね。
だからboxingされなかったと思われます。
#まとめ
とりあえず実用的な機能であるが、structに対するパフォーマンス上のリスクがあることを気を付けなければならないと思われます。
警告もとくに出す予定がない(Analyzerで出す気はあるらしい)ので一番怖いことはlibrary側でinterfaceにDIMsを使われ、かつそのmethodを知らないうちにstructで使ってわれているとパフォーマンスが悪化する可能性があることでしょうか。
破壊的変更をせずにinterfaceを変えれるのはlibrary作者などにとっては利点なのでclassだけがターゲットだとかなら使いましょう。
詳しい方がいればコメント等で訂正、補足等してくれると幸いです。