文章

Code

记录一些开发当中的问题...

Code

Code

C#

C#委托与匿名方法内存分配

多个方式使用委托与匿名方法的内存差异

1
2
3
4
5
6
7
public void Test()
    {
        for (int i = 0; i < 100; i++)
        {
            Action act = ???
        }
    }

举例三种常用的赋值方式,并且每一种方式通过SharpLab查看编译后的C#代码 1.Action对象赋值 2.方法名赋值 3.匿名方法赋值

SharpLab地址:https://sharplab.io/

1.Action对象赋值 通过将一个方法赋值给一个Action对象a,然后再将a赋值给act

1
2
3
4
5
6
7
8
9
10
11
12
13
public void Test()
    {
        Action a = ActionMethod;
        for (int i = 0; i < 100; i++)
        {
            Action act = a;
        }
    }

    public void ActionMethod()
    {

    }

SharpLab查看编译后的C#代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void Test()
    {
        Action action = new Action(ActionMethod);
        int num = 0;
        while (num < 100)
        {
            Action action2 = action;
            num++;
        }
    }

    public void ActionMethod()
    {
    }

可以看到本质上是通过调用Action的构造方法,使用ActionMethod构造了一个Action对象然后在循环进行赋值 整个过程产生了1次内存分配

2.使用方法名赋值 直接用ActionMethod赋值给act

1
2
3
4
5
6
7
8
9
10
11
12
public void Test()
    {
        for (int i = 0; i < 100; i++)
        {
            Action act = ActionMethod;
        }
    }

    public void ActionMethod()
    {

    }

然后通过SharpLab查看编译后的C#代码

1
2
3
4
5
6
7
8
9
10
11
12
13
public void Test()
    {
        int num = 0;
        while (num < 100)
        {
            Action action = new Action(ActionMethod);
            num++;
        }
    }

    public void ActionMethod()
    {
    }

在循环中每一次赋值时都构造了新的Action对象 整个过程产生了100次内存分配

3.使用匿名方法赋值 通过使用 Lambda表达式产生出一个匿名方法赋值给 act

1
2
3
4
5
6
7
public void Test()
    {
        for (int i = 0; i < 100; i++)
        {
            Action act = () => { };
        }
    }

通过SharpLab查看编译后的C#代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[Serializable]
    [CompilerGenerated]
    private sealed class <>c
    {
        public static readonly <>c <>9 = new <>c();

        public static Action <>9__0_0;

        internal void <Test>b__0_0()
        {
        }
    }

    public void Test()
    {
        int num = 0;
        while (num < 100)
        {
            Action action = <>c.<>9__0_0 ?? (<>c.<>9__0_0 = new Action(<>c.<>9.<Test>b__0_0));
            num++;
        }
    }

编译器会生成一个匿名类,将匿名方法保存到其中的静态Action字段中 整个过程产生了1次内存分配

总结

因此在对于需要重复赋值Action变量的情况 Action对象赋值(1)=匿名方法赋值(1) > 方法名赋值(100)

匿名方法闭包的区别

以上的讨论中涉及的匿名方法没有捕获到外部变量(无闭包) 但一般在使用匿名方法时都会对外部变量进行捕获(有闭包)

而被捕获的外部变量有3种可能的情况: 1.捕获到静态字段 2.捕获到实例字段 3.捕获到外部方法的局部变量

捕获到静态字段 在匿名方法中声明一个int变量x,去捕获静态字段staticInt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TestClass
{
    private static int staticInt;

    public void Test()
    {
        for (int i = 0; i < 100; i++)
        {
            Action act = () => {
                int x = staticInt;
            };
        }
    }

}

然后通过SharpLab查看编译后的C#代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class TestClass
{
    [Serializable]
    [CompilerGenerated]
    private sealed class <>c
    {
        public static readonly <>c <>9 = new <>c();

        public static Action <>9__1_0;

        internal void <Test>b__1_0()
        {
            int staticInt = TestClass.staticInt;
        }
    }

    private static int staticInt;

    public void Test()
    {
        int num = 0;
        while (num < 100)
        {
            Action action = <>c.<>9__1_0 ?? (<>c.<>9__1_0 = new Action(<>c.<>9.<Test>b__1_0));
            num++;
        }
    }
}

当捕获到的是静态字段时,编译器的处理就和无闭包时差不多,会直接在匿名方法中去访问静态字段

整个过程产生了1次内存分配

捕获到实例字段 在匿名方法中声明一个int变量x,去捕获实例字段instanceInt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestClass
{
    private  int instanceInt;

    public void Test()
    {
        for (int i = 0; i < 100; i++)
        {
            Action act = () => {
                int x = instanceInt;
            };
        }
    }
}

然后通过SharpLab查看编译后的C#代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TestClass
{
    private int instanceInt;

    public void Test()
    {
        int num = 0;
        while (num < 100)
        {
            Action action = new Action(<Test>b__1_0);
            num++;
        }
    }

    [CompilerGenerated]
    private void <Test>b__1_0()
    {
        int num = instanceInt;
    }
}

捕获的是实例字段时,编译器将不会生成匿名类,而是直接用匿名方法构造临时Action 整个过程产生了100次内存分配

捕获到外部方法的局部变量 在匿名方法中声明一个int变量x,去捕获循环中声明的局部变量instanceInt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TestClass
{
    public void Test()
    {
        for (int i = 0; i < 100; i++)
        {
            int localInt = 0;
            Action act = () => {
                int x = localInt;
            };
        }
    }
}

然后通过SharpLab查看编译后的C#代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class TestClass
{
    [CompilerGenerated]
    private sealed class <>c__DisplayClass0_0
    {
        public int localInt;

        internal void <Test>b__0()
        {
            int num = localInt;
        }
    }

    public void Test()
    {
        int num = 0;
        while (num < 100)
        {
            <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
            <>c__DisplayClass0_.localInt = 0;
            Action action = new Action(<>c__DisplayClass0_.<Test>b__0);
            num++;
        }
    }
}

编译器会为每一次捕获的局部变量创建匿名类对象来保存该局部变量,然后使用匿名方法去创建Action对象并赋值给act 整个过程产生了200次内存分配

总结

在使用匿名方法时

无闭包(1)=捕获静态字段(1) > 捕获实例字段(100) > 捕获局部变量(200)

本文由作者按照 CC BY 4.0 进行授权