LoginSignup
4
4

More than 5 years have passed since last update.

小ネタ:構造体の内部でメンバフィールドを含むラムダ式が構築できない理由 (追記有り)

Last updated at Posted at 2016-05-10

どーいうことか

こんなクラスがあったとする。

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そのものをボックス化した上で、そのボックス化されたSomeStructReturnedFunctionを返す形になる。
ラムダ式を返す場合でも同様の操作を行えば、とりあえず意味は通るし使える反面、もっと複雑なシナリオでは例えば、必要なメンバフィールドが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());

        }
    }
}

この挙動の差を明確にするため、一度ローカル変数に代入する操作を強制したのかも知れないと考えた。

パフォーマンスに対する差異よりむしろこっちの意味の方が強い気がしたりしなかったり。

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4