どーいうことか
こんなクラスがあったとする。
public class SomeClass
{
private readonly int _value;
public SomeClass(int value)
{
_value = value;
}
public Func<int> GetFunction() => () => _value;
}
コレは全く問題なく通るのだけど、同じようなことを下記のような構造体でやった場合
public struct SomeStruct
{
private readonly int _value;
public SomeStruct(int value) : this()
{
_value = value;
}
//ここでCS1673が発生する。
public Func<int> GetFunction() => () => _value;
}
コメントにあるように、GetFunction
メソッドでCS1673が発生してコンパイルエラーとなる。
で、コレが何で発生するか、考えてみたので少々おつきあい頂ければ幸い。
クラスの場合コンパイラは何をしてるか
先のSomeClass
をコンパイルしたとき、どのようにコンパイラは裏方で仕事をしてるかというと、こんな風になってる。
public class SomeClass
{
private readonly int _value;
public SomeClass(int value)
{
_value = value;
}
public Func<int> GetFunction()
{
return new Func<int>(ReturenedFunction);
}
private int ReturenedFunction()
{
return _value;
}
}
ラムダ式をSomeClasss
の中のプライベートメソッドとして展開した上で、それを返す形を取っている。
構造体だとなぜマズいのか
で、ここからが本題。
上記のような展開操作をまねして下記のように構造体でもやってみたとする。
public struct SomeStruct
{
private readonly int _value;
public SomeStruct(int value) : this()
{
_value = value;
}
public Func<int> GetFunction()
{
return new Func<int>(ReturnedFunction);
}
private int ReturnedFunction()
{
return _value;
}
}
コレだと、問題なくコンパイルが出来る。
なのに、ラムダ式を使うとコンパイルエラーになる。
なぜ、このような差異が出るかと言えば、多分効率化の問題じゃないかなと予想した。
return new Func<int>(ReturnedFunction);
としたとき、this
そのものをボックス化した上で、そのボックス化されたSomeStruct
のReturnedFunction
を返す形になる。
ラムダ式を返す場合でも同様の操作を行えば、とりあえず意味は通るし使える反面、もっと複雑なシナリオでは例えば、必要なメンバフィールドが1個だけなのに対象となる構造体を丸抱えでボックス化する様な不効率な状態が発生しかねない。加えて、デリファレンスコストも増大することになる。そこを嫌ったからこそのCS1673なんじゃないかなと考察した次第。
解決方法
最後に、どうすれば解決できるかと言えば、コレは結構簡単で下記のようにローカル変数に代入した上でローカル変数をラムダの内部で使えば良いだけの話。
public struct SomeStruct
{
private readonly int _value;
public SomeStruct(int value) : this()
{
_value = value;
}
public Func<int> GetFunction()
{
int value = _value;
return () => value;
}
}
このようにすることで、GetFunction
メソッド内のvalue
変数はラムダ式の内部で利用されていることから、クロージャとなり、マネージドヒープに逃げるので、問題なく動作することに成り、また、必要なモノしかキャプチャしないし、ラムダ式の部分が隠しクラスのインスタンスメソッドに、valueの部分が隠しクラスのメンバ変数になるのでデリファレンスコスト的にも効率的にも望ましいンじゃないかなって思います。
2016/05/11:追記
パフォーマンスよりもしかしたら、Semantics的な見地なのかも。
参照型の場合、元のインスタンスのメンバフィールドが変化すれば応じて変化できるけど、
他方、構造体の方は変化できない(そりゃそーだ。Boxedした時点でコピーがこさえられるので、元とは独立してしまう)。
参照型の挙動は以下の通り。
using System;
namespace ConsoleApplication8
{
public class SomeClass
{
//行儀が悪いけどまぁ許してw
public int Value;
public SomeClass(int value)
{
Value = value;
}
public Func<int> GetFunction() => () => Value;
}
class Program
{
static void Main(string[] args)
{
var someRef=new SomeClass(42);
var someFunc = someRef.GetFunction();
//42が出力される
Console.WriteLine(someFunc());
someRef.Value = 114514;
//114514が出力される
Console.WriteLine(someFunc());
}
}
}
無理矢理ボックス化した構造体の場合
using System;
namespace ConsoleApplication8
{
public struct SomeStruct
{
//行儀が悪いけどまぁ許してw
public int Value;
public SomeStruct(int value)
{
Value = value;
}
public Func<int> GetFunction() => Implements;
private int Implements() => Value;
}
class Program
{
static void Main(string[] args)
{
var someStruct=new SomeStruct(42);
var someFunc = someStruct.GetFunction();
//42が出力される
Console.WriteLine(someFunc());
someStruct.Value = 114514;
//ボックス化されてるモノは変化しないので42のまま出力される
Console.WriteLine(someFunc());
}
}
}
この挙動の差を明確にするため、一度ローカル変数に代入する操作を強制したのかも知れないと考えた。
パフォーマンスに対する差異よりむしろこっちの意味の方が強い気がしたりしなかったり。