反编译C#代码来看看闭包到底是什么
原文地址:https://zhuanlan.zhihu.com/p/3161634
C#的闭包,是一个语法糖。 它实质上是将匿名函数转换成一个类,函数作为其中的类方法,并调整外部调用代码来实现的。既然是对象,自然就有自己的堆内存分配。 但它并不是无脑地每次创建委托就生成一个新的对象,而是做了编译期间优化,实际程序中生成的对象是少于我们的预期的。 下面就是通过反编译来查看C#是如何编译匿名函数的。 首先是这个经典闭包示例,认为结果是0,1,2,3,4,5,6,7,8,9的统统出去抽自己10个耳光,不解释。 public class Test : MonoBehaviour
{
Action[] actions = new Action[10];
void Awake()
{
for (int i = 0;i < 10;i++)
{
actions[i] = () =>
{
Debug.Log(i);
};
} foreach (var action in actions)
{
action();
}
}
} 反编译的结果是这样的: public class Test : MonoBehaviour
{
private Action[] actions = new Action[10]; private void Awake()
{
<>c__DisplayClass1_0 CS$<>8__locals0 = new <>c__DisplayClass1_0();
CS$<>8__locals0.i = 0;
while (CS$<>8__locals0.i < 10)
{
this.actions[CS$<>8__locals0.i] = new Action(CS$<>8__locals0.<Awake>b__0);
int i = CS$<>8__locals0.i;
CS$<>8__locals0.i = i + 1;
}
foreach (Action action in this.actions)
{
action();
}
}
} private sealed class <>c__DisplayClass1_0
{
public int i; internal void <Awake>b__0()
{
Debug.Log(this.i);
}
} 估计你们看得都很费劲,我把里面的名字改一下再看吧(之后的代码都是如此) public class Test : MonoBehaviour
{
private Action[] actions = new Action[10]; private void Awake()
{
//生成一个闭包对象(产生额外堆内存分配)
AnonymousClass anonymous = new AnonymousClass();
//闭包里使用到的外部变量全部替换成这个闭包对象的属性,这就是一般说的值对象装箱
anonymous.i = 0;
while (anonymous.i < 10)
{
this.actions[anonymous.i] = new Action(anonymous.Action);
int i = anonymous.i;
anonymous.i = i + 1;
}
foreach (Action action in this.actions)
{
action();
}
}
} private sealed class AnonymousClass
{
public int i; internal void Action()
{
Debug.Log(this.i);
}
} 所以,闭包产生的堆内存有两个,一个是闭包本身的这个对象AnonymousClass,另一个就是它所用到的外部变量i。外部变量越多,堆内存自然也就越多。但外部变量没有,闭包本身还是会占用一个空对象的内存(大概17B?) 光这么看,似乎引用点外部变量也没事?并非如此。 下面是能够正常输出0,1,2,3,4,5,6,7,8,9的经典写法 public class Test : MonoBehaviour
{
Action[] actions = new Action[10];
void Awake()
{
for (int i = 0;i < 10;i++)
{
int j = i;//不同之处就是将i先存到另一个变量中,再在闭包引用这个值
actions[i] = () =>
{
Debug.Log(j);
};
} foreach (var action in actions)
{
action();
}
}
} 结果是: using System;
using UnityEngine; public class Test : MonoBehaviour
{
private Action[] actions = new Action[10]; private void Awake()
{
for (int i = 0; i < 10; i++)
{
AnonymousClass anonymous = new AnonymousClass();
//闭包对象每个循环都创建了一次
anonymous.j = i;
this.actions[i] = new Action(anonymous.Action);
}
foreach (Action action in this.actions)
{
action();
}
}
} private sealed class AnonymousClass
{
public int j; internal void Action()
{
Debug.Log(this.j);
}
} 虽然闭包对象还是以前的样子,但是却一共创建了10个,GC也就变成了10倍。当然,也因为这个原因能够打印出正确的结果了。 这是理所当然的,因为你需要让它打印出10个不同的结果,它就得保持10个不同的状态嘛。所以你看,并不是只要用循环就一定会产生循环次数的闭包,关键还要看使用的属性是循环内的还是循环外的。其实只要别引用循环内的数据,循环内多次生成闭包函数是没关系的。 然后下一步自然就是,如果我闭包里什么变量都不引用,会是什么样呢? public class Test : MonoBehaviour
{
Action[] actions = new Action[10];
void Awake()
{
for (int i = 0;i < 10;i++)
{
actions[i] = () =>
{
Debug.Log("");//不再引用i
};
} foreach (var action in actions)
{
action();
}
}
} 结果是: public class Test : MonoBehaviour
{
private Action[] actions = new Action[10]; private void Awake()
{
for (int i = 0; i < 10; i++)
{
this.actions[i] = (AnonymousClass.action != null) ?
AnonymousClass.action :
AnonymousClass.action = new Action(AnonymousClass.instance.Funtion);
}
foreach (Action action in this.actions)
{
action();
}
} private sealed class AnonymousClass
{
public static readonly Test.AnonymousClass instance =
new Test.AnonymousClass();
public static Action action ; internal void Funtion()
{
Debug.Log("");
}
}
} 虽然有很多操作的样子,其实就是将这个匿名函数变成了一个静态单例。不过C#生成的这代码并不是很smart,有不少多余的处理,但反正都是个静态类,也无所谓了。 静态类只会实例化一次,所以可以认为没有引用外部变量的闭包函数都没有GC。 那么引用成员属性呢? public class Test : MonoBehaviour
{
Action[] actions = new Action[10];
void Awake()
{
for (int i = 0;i < 10;i++)
{
actions[i] = () =>
{
Debug.Log(actions.Length);
};
} foreach (var action in actions)
{
action();
}
}
} 结果是: public class Test : MonoBehaviour
{
private Action[] actions = new Action[10]; private void Awake()
{
for (int i = 0; i < 10; i++)
{
this.actions[i] = new Action(this.Function);
}
foreach (Action action in this.actions)
{
action();
}
} private void Function()
{
Debug.Log(this.actions.Length);
}
} 只引用成员属性的闭包函数,等效于成员方法 那,如果是引入的是方法内的临时引用对象呢? public class Test : MonoBehaviour
{
Action[] actions = new Action[10];
void Awake()
{
for (int i = 0;i < 10;i++)
{
Action[] v = actions;//只是简单换个变量保存一下
actions[i] = () =>
{
Debug.Log(v.Length);
};
} foreach (var action in actions)
{
action();
}
}
} 结果是: public class Test : MonoBehaviour
{
private Action[] actions = new Action[10]; private void Awake()
{
for (int i = 0; i < 10; i++)
{
AnonymousClass anonymous = new AnonymousClass();
anonymous.v = this.actions;
this.actions[i] = new Action(anonymous.Action);
}
foreach (Action action in this.actions)
{
action();
}
}
} private sealed class AnonymousClass
{
public Action[] v; internal void Action()
{
Debug.Log(this.v.Length);
}
} 龟龟,这也太蠢了吧?和第二个使用循环内值类型的情况一模一样(10个匿名对象),就不知道优化成上一个的结果么?(0个匿名对象)。也就是说即使是引用对象,在循环里建个临时变量放在闭包里引用也会导致严重的GC问题。 再试试一个方法内多个引用对象的情况 public class Test : MonoBehaviour
{
Action[] actions = new Action[10];
void Awake()
{
int x = 1;
int y = 1;
int z = 1;
for (int i = 0;i < 5;i++)
{
actions[i] = () =>
{
Debug.Log(x + y);
};
}
for (int i = 5; i < 10; i++)
{
actions[i] = () =>
{
Debug.Log(y + z);
};
} foreach (var action in actions)
{
action();
}
}
} 结果是: using System;
using UnityEngine; public class Test : MonoBehaviour
{
private Action[] actions = new Action[10]; private void Awake()
{
AnonymousClass anonymous = new <>c__DisplayClass1_0();
anonymous.x = 1;
anonymous.y = 1;
anonymous.z = 1;
for (int i = 0; i < 5; i++)
{
this.actions[i] = anonymous.action1 ?? (anonymous.Action1 = new Action(anonymous.Action1));
}
for (int i = 5; i < 10; i++)
{
this.actions[i] = anonymous.action2 ?? (anonymous.Action2 = new Action(anonymous.Action2));
}
foreach (Action action in this.actions)
{
action();
}
}
} private sealed class AnonymousClass
{
public Action action1;
public Action action2;
public int x;
public int y;
public int z; internal void Action1()
{
Debug.Log(this.x + this.y);
} internal void Action2()
{
Debug.Log(this.y + this.z);
}
} 当你一个方法内用到多个闭包函数,而它们引用的是不同的临时变量的话,匿名对象会保存全部的变量的总和。 但这样做的目的是为了只生成一个匿名对象,实际上是一种优化。 当它没得优化的时候: public class Test : MonoBehaviour
{
Action[] actions = new Action[10];
void Awake()
{
int x = 1;
int y = 1;
for (int i = 0;i < 5;i++)
{
actions[i] = () =>
{
Debug.Log(x + y);
};
}
for (int i = 5; i < 10; i++)
{
int z = 1;
actions[i] = () =>
{
Debug.Log(y + z);
};
} foreach (var action in actions)
{
action();
}
}
} 结果是: public class Test : MonoBehaviour
{
private Action[] actions = new Action[10]; private void Awake()
{
AnonymousClass1 anonymous1 = new AnonymousClass1();
anonymous1.x = 1;
anonymous1.y = 1;
for (int i = 0; i < 5; i++)
{
this.actions[i] = anonymous1.action1 ?? (anonymous1.action1 = new Action(anonymous1.Action1));
}
for (int i = 5; i < 10; i++)
{
AnonymousClass2 anonymous2 = new AnonymousClass2();
anonymous2.anonymous1 = anonymous1;//把另一个匿名对象存过来取y值
anonymous2.z = 1;
this.actions[i] = new Action(anonymous2.Action2);
}
foreach (Action action in this.actions)
{
action();
}
}
}
private sealed class AnonymousClass1
{
public Action action1;
public int x;
public int y; internal void Action1()
{
Debug.Log(this.x + this.y);
}
}
private sealed class AnonymousClass2
{
public Test.AnonymousClass1 anonymous1;
public int z; internal void Action2()
{
Debug.Log(this.anonymous1.y + this.z);
}
}
总之,就是和我们手写这种情况的时候,差不多的处理。
结论就是多闭包函数同时出现没问题。
好,最后我们总结一遍:
- 闭包对象的生成次数和引用的临时变量的应用域有关,如果引用循环内的临时变量,每个循环都会生成一份。引用方法里的临时变量,每次调用方法都会生成一份。所以,尽量引用应用域靠外,也就是生命周期更长的变量,不要贪图方便转存对象到临时变量。而且,这个规则和那个变量是值类型还是引用类型无关,只要是临时变量,管你是不是值对象都会触发装箱导致GC。
- 单个闭包对象的内存占用和引用到的变量的数量有关,但毕竟这些对象都不大,更重要的还是控制闭包对象的生成数量,也就是1所说的内容。
- 一个方法内有多个闭包函数是没关系的,不需要拆解到多个方法内。
- 如果闭包函数没有引用任何临时变量,可以认为,它和成员方法等效。
我知道有很多人都排斥使用闭包函数,认为它容易导致“问题”(好多人说的都是“内存泄露”这种无稽之谈,不过GC问题确实还是存在的)
闭包在不同应用域传递变量的方法是创建额外对象,而手写代码则可以选择利用成员属性来代替,这确实会导致闭包产生更多的GC。但在上面的那些用例里,闭包会造成的大量额外GC,也只有“明明可以把变量放在外侧,却非要转存一份在内部”这一种情况,而多出来的GC其实也就是一个空对象,不在逐帧事件里使用并没有什么问题。
闭包毕竟会让代码变得简洁,而低GC写法往往都很丑陋。这种可读性和效能的矛盾一直都存在,适当的权衡是需要的。
反编译C#代码来看看闭包到底是什么的更多相关文章
- JD-GUI反编译后代码逻辑分析
一,用jd-gui.exe等工具查看源代码.如何你不会,可以参看此文章: http://blog.csdn.net/hp_2008/article/details/8207879 可以到以下连接下载可 ...
- Android Apk的反编译与代码混淆
一.反编译 1.获取工具: 既然是反编译,肯定要用到一些相关的工具,工具可以到这里下载,里面包含三个文件夹,用于反编译,查看反编译之后的代码: 其实这两工具都是google官方出的,也可在google ...
- Android - 使用JD-GUI反编译Android代码
使用JD-GUI反编译Android代码 本文地址: http://blog.csdn.net/caroline_wendy Android程序出现Bug时,须要依据Bug寻找问题出错的地方; 须要使 ...
- 实现android apk反编译后代码混淆
通常情况下我们需要对我们开发的android代码进行混淆,以免代码在反编译时暴露敏感信息及相关技术代码: 反编译测试工具:onekey-decompile-apk-1.0.1. 在高级版本的adt创建 ...
- apk应用的反编译和源代码的生成
对于反编译一直持有无所谓有或无的态度.经过昨天一下午的尝试,也有了点心得和体会: 先给大家看看编译的过程和我们反编译的过程概图吧: 例如以下是反编译工具的根文件夹结构: 三个目录也实际上是下面三个步骤 ...
- (转)unity3D 如何提取游戏资源 (反编译)+代码反编译
原帖:http://bbs.9ria.com/thread-401140-1-1.html 首先感谢 雨松MOMO 的一篇帖子 教我们怎么提取 .ipa 中的游戏资源.教我们初步的破解unity3d资 ...
- android APK反编译及代码混淆
反编译.查看源代码,需要用到两个工具:dex2jar 和 jdgui dex2jar(google code) jdgui(google code),最新版本请见 官方 操作很简单,步骤如下: 1.将 ...
- iOS 代码安全加固--反编译和代码混淆
一.class-dump反编译 1.将打包的ipa反编译下,.ipa改成.zip,并解压 6.右击—显示包内容,找到如下有个白框黑底的 7.将其复制到桌面xx文件夹中,在终端中输入相关命令 cd 进 ...
- android 反编译和代码解读
二 错误代码还原规则 if…else 语句: 反编译代码 if (paramBoolean) paramTextView.setTextColor(-16727809); while (true) { ...
随机推荐
- 在3G移动通信网络信令流程里获取用户电话号的一种方法(中国电信cdma2000)
首先这些关于电话号的的寻找都是在分组域进行的 然后是首先在rp接口的A11接口寻找,没有看到,于是到pi接口,研究radius协议 发现在协议里也不含有与用户电话号码mdn相关的元素 然后偶遇一篇文档 ...
- vue学习15-自定义组件model使用
<!DOCTYPE html> <html lang='en'> <head> <meta charset='UTF-8'> <meta http ...
- java单例模式(饿汉式和懒汉式)
1 /* 2 * 设计模式:对问题行之有效的解决方式.其实它是一种思想. 3 * 4 * 1,单例设计模式 5 * 解决的问题:就是可以保证一个类在内容中的对象唯一性. 6 * 7 * 必须对于多个程 ...
- HTTP状态码1XX深入理解
前段时间看了<御赐小仵作>,里面有很多细节很有心.看了一些评论都是:终于在剧里能够看到真正在搞事业.发了工资第一时间还钱的正常人了.我印象比较深的是王府才能吃上的葡萄.觉得非常合理.剧里说 ...
- Kubernetes常见的部署方案(十四)
一.常见的部署方案 滚动更新 服务不会停止,但是整个pod会有新旧并存的情况. 重新创建 先停止旧的pod,然后再创建新的pod,这个过程服务是会间断的. 蓝绿 (无需停机,风险较小) 部署v1的应用 ...
- Mac iterm2 配色以及终端大小写敏感解决方案
iterm2是mac下非常好用的一款终端.但默认的配色实在不好用,经过一翻搜索终于找到了比较满意的,以下贴出博主的解决方案 配色 首先修改 ~/.bash_profile 加入一下代码 #enable ...
- ApacheCN Python 译文集 20211108 更新
Think Python 中文第二版 第一章 编程之路 第二章 变量,表达式,语句 第三章 函数 第四章 案例学习:交互设计 第五章 条件循环 第六章 有返回值的函数 第七章 迭代 第八章 字符串 第 ...
- CF1408G Clusterization Counting
首先,我们需要给一个连通块找到一个直观的合法判定解. 那么我们必须以一种直观的方式将边按照权值分开,这样才能直观地判定一个合法的组. 一个常见的方式是将边从小到大依次加入进来,那么在任意时刻图上存在的 ...
- File 类的 getPath()、getAbsolutePath()、getCanonicalPath() 的区别【转】
File 类的 getPath().getAbsolutePath().getCanonicalPath() 的区别 感谢大佬:https://blog.csdn.net/zsensei/articl ...
- JAVA多线程学习- 三:volatile关键字
Java的volatile关键字在JDK源码中经常出现,但是对它的认识只是停留在共享变量上,今天来谈谈volatile关键字. volatile,从字面上说是易变的.不稳定的,事实上,也确实如此,这个 ...