[编程基础]闭包

前言

在一个机缘巧合之下,了解到了闭包概念,当时对里面的实现感觉没有头绪,所以就暂时放过不去研究了。前段时间看到一本《代码的未来》里面系统性的介绍了一下闭包概念,感觉自己找到点头绪,然后就开始仔细研究一波。

闭包所需要的知识点

  • 函数对象
  • 作用域
  • 生存周期

函数对象

所谓的函数对象:就是作为对象来使用的函数,在c#中为委托(delegate),这里的对象不是面向对象中的那个对象,而是编程语言中的数据对象。

delegate void dhello();
void hello (){
    Console.WriteLine("Hello");
}
dhello d = hello;
d();

上述代码为简单的定义一个委托,然后给委托对象赋值一个函数,然后执行这个对象。在delegate的定义中签名必须和被委托的方法的签名一致,形参也必须得一致。就可以直接调用该方法,或者把它当成参数传递给另一个方法调用。在.NET Framework 2.0引入了匿名委托

delegate void dhello();
dhello d = delegate{
    Console.WriteLine("Hellp");
}
d();

在这里委托正文就是一直表达式,可以不用定义方法名。这样叫做创建的“内联”委托,无需指定任何其他类型或者方法,只需在所需位置内联委托定义就好。
在c# 3.0版本中,我们对上述代码还能在简易点,我们引入lambda表达式来实现上述功能

delegate void dhello();
dhello d = ()=>{
    Console.WriteLine("Hello");
}
d();

这个只是使用委托的更方便的语法。它们将声明签名和方法正文,但在分配到委托之前没有自己的正式标识。与委托不同,可将其作为事件注册的左侧内容或在各种LINQ子句和方法中直接分配。

高阶函数

函数对象最大的用途就是高阶函数。所谓的高阶函数,就是用函数作为参数的函数。

delegate void hello();
delegate void hello1(hello h);
void hello2 (hello h){
h()
}
hello1 h1 = hello2;
hl(()=>{Console.WriteLine("Hello");});

通过将一部分处理以函数对象的形式转移到外部,从而实现了算法的通用性。

函数指针的局限

函数指针的方法体是不能访问外部的局部变量的,如果把这个变量变成全局的是可以的,但是只是为了能访问到而变成一个全局变量是不划算的。

作用域

作用域是指变量的有效范围,也就是某个变量可以被访问的范围。作用域是嵌套的,因此位于内侧的代码块可以访问以其自身为作用域的变量,以及以外侧代码块为作用域的变量。

delegate void hello2(int n);
delegate void hello3(List<int> l, hello2 h2);
void foreached(List<int> list1, hello2 h2)
{
   int i = 0;
   while (i < list1.Count)
   {
      h2(list1[i]);
      i++;
    }
}
 List<int> a1 = new List<int>() { 1, 2, 3, 4, 5 };
 int i = 0;
 hello3 h3 = foreached;
 h3(a1, (int c) => { Console.WriteLine("index" + i + "=" + c);i++; });

在函数对象中能对外部变量i进行访问(引用,更新),这就是闭包的构成要件之一。
按照作用域的思路,可能大家觉得上述闭包性质也是理所当然的。但是我们如果加入另外一个概念-生存周期,结果可能就会出乎意料了。

生存周期

所谓的生存周期,就是变量的寿命。相对于表示程序中变量可见范围的作用域来说,生存周期这个概念指的是一个变量可以在多长的周期范围内存在并被能够被访问。

delegate void hello();
 hello extent()
            {
                int n = 0;
                return () => { n++; Console.WriteLine("n=" + n); };
            }
 hello h = extent();
                h();
                h();
输出:n = 1
      n = 2

这个函数变量在每次执行的时候,局部变量n都会被更新,从而输出逐次累加的的结果。按道理n是在extent函数中声明的,函数都已经执行完毕了。变量脱离了作用域之后不应该就消失吗?从这个结果来看,这个变量在函数执行完毕以后没有消失,还在某一个地方继续存活下来的。
这就是生命周期。也就是说,这个从属于外部作用域的局部变量,被函数对象给“封闭”在里面了。
闭包这个词原本就是封闭的意思。被封闭起来的变量的寿命,与封闭它的函数对象寿命相等。也就是说,当封闭这个变量的函数对象不再被访问,被垃圾回收器回收的以后,这个变量的寿命也就同时终结了。
在函数对象中,将局部变量这一环境封闭起来的结构被称为闭包。

[编程基础]c#托管资源和非托管资源

前言

GC(垃圾回收)实现的时候,对引用资源管理都是对托管资源的管理,而在c#中还有一些非托管资源,现在简单讲下,两者的区别,和对非托管资源是怎么释放内存的。

托管资源

.NET中的类型都是从System.Object类型派生出来的。
分为两大类:
引用类型(reference type),分配在内存堆上。
值类型(value type),分配在堆栈上。

分配内存

值类型在栈里,先进后出,值类型变量的生命有先有后,这个确保了值类型变量在退出作用域以前会释放资源。比引用类型更简单和高效。堆栈是从高地址往低地址分配内存。
引用类型分配在托管堆上,声明一个变量在栈上保存,当实例化一个对象的时候,会把对象的地址存储在这个变量里。托管堆相反,从低地址往高地址分配内存。
初始化新进程的时候,CLR会为进程保留一个连续的地址空间区域。这个保留的地址空间被称为托管堆。托管堆维护着一个指针,用它指向将在堆中分配的下一个对象的地址。一开始该指针指向托管堆的基址。托管堆上包含了所有的引用类型。从托管堆中分配内存要比非托管内存分配速度快。由于CLR通过为指针添加值来为对象分配内存,所以这几乎和从堆栈中分配内存一样快。另外由于连续分配的新对象在托管堆中是连续存储,所以应用程序可以快速访问这些对象。

释放内存

这个就是GC所做的事情了,参考c#GC。

非托管资源

应用使用完非托管资源后,必须要显式释放这些资源。最常见的非托管资源类型是包装操作系统资源的对象。虽然垃圾回收器可以跟踪封装非托管资源的对象的生存期,但是无法了解如何发布并清理这些非托管资源。所以清理需要做以下操作:

  • 实现清理模式:需要提供IDisposable.Dispose实现以启用非托管资源的确定性释放。当不再需要此对象时,类型使用者可以调用Dispose。Dispose方法立即释放非托管资源。

  • 在类型使用者忘记调用Dispose的情况下,准备释放非托管资源。有两种方法可实现此目的:

    • 使用安全句柄包装非托管资源。安全句柄派生自System.Runtime.InteropServices.SafeHandle类并包含可靠的Finalize方法。在使用安全句柄时,只需要实现IDisposable接口并在Dispose实现中调用安全句柄的IDisposable.Dispose方法。如果未调用安全句柄Dispose方法,则垃圾回收器将自动调用安全句柄的终结器。
    • 重写Object.Finalize方法。当类型使用者无法调用IDisposable.Dispose以确定性地释放非托管资源时,终止会启动对非托管资源的非确定性释放。

然后,类型使用者可直接调用IDisposable.Dispose实现以释放非托管资源使用的内存。在正确实现Dispose方法时,安全句柄的Finalize方法或Object.Finalize方法的重写会在未调用Dispose方法的情况下阻止清理资源。

[编程基础]c#GC简介

GC的前世今生

GC的概念并非才诞生不久,早在1958年,由大名鼎鼎的图灵奖得主john mccarthy所实现的lisp语言就已经提供了GC的功能,这是GC的第一次出现。lisp的程序员认为内存管理太重要的,所以不能由程序员自己来管理。
但是后来的日子里lisp却没有成气候,采用内存手动管理的语言占据了上风,以c为代表。出于同样的理由,不同的人却又不同的看法,c程序员也认为内存管理太重要的,所以不能由系统来管理,并且讥笑lisp程序慢如乌龟的运行速度。的确,在那个对每一个byte都要精心计算的年代GC的速度和对系统资源的大量占用使很多人都无法接受。而后,1984年由dave ungar开发的smalltalk语言第一次采用了Generational garbage collection的技术,但是amalltalk也没有得到十分广泛的应用。
直到20世纪90年代中期GC才以主角的身份登上历史的舞台,这不得不归功于Java的进步,今日的GC已非吴下阿蒙。Java采用VM(virtual machine)机制,由VM来管理程序的运行当然也包括对GC管理。90年代末期.net出现了,.net采用了和Java类似由CLR(Common Language Runtime)来管理。这两大阵营的出现将人们引入了以虚拟平台为基础的开发时代,GC也在这个时候越来越得到大众的关注。
为什么要使用GC呢?也可以说为什么要使用内存自动管理?有以下一个原因:
1. 提高了软件开发的抽象度;
2. 程序员可以将精力集中在实际的问题上而不用分心来管理内存的问题;
3. 可以使用模块的接口更加的清晰,减少模块间的耦合;
4. 大大减少了内存人为管理不当所带来的bug;
5. 使内存管理更加高效。
总的来说就是GC可以使程序员可以从复杂的内存问题中摆脱出来,从而提高了软件开发的速度,质量和安全性。

什么是GC

GC就是垃圾收集,这里仅就内存而言。Garbage Collector以应用程序的root为基础,遍历应用程序在Heap上动态分配的所有对象,通过识别它们是否被引用来确定哪些对象是已经死亡的,哪些仍然需要被使用。已经不再被引用程序root引用或者别的对象所引用的对象就是已经死亡的对象,即所谓的垃圾,需要被回收。

1,Mark-Compact 标记压缩算法

简单的吧.NET的GC算法看作Mark-Compact算法。阶段1:Mark—Sweep 标记清除阶段,先假设heap中所有对象都可以回收,然后找出不能回收的对象,给这些对象打上标记,最后heap中没有打标记的对象都是可以被回收的;阶段2:Compact 压缩阶段,对象回收之后heap内存空间变得不连续,在heap中移动这些对象,使它们重新从heap基地址开始连续排列,提高局部性。
<a href="https://i.loli.net/2019/10/08/TLm9Uy5Kaq2frvF.png"><img src="https://i.loli.net/2019/10/08/TLm9Uy5Kaq2frvF.png" alt="" /></a>
Heap内存经过回收,压缩之后,可以继续采用前面的heap内存分配方法,即仅用一个指针记录heap分配的起始地址就可以。主要处理步骤:将线程挂起->确定roots->创建reachable objects graph->对象回收->heap压缩->指针修复。可以这样理解roots:heap中对象的引用关系错综复杂(交叉引用,循环引用),形成复杂的graph,roots是CLR在heap之外可以找到的各种入口。
GC搜索roots的地方包括全局对象,静态变量,局部对象,函数调用参数,当前CPU寄存器中的对象指针等。主要可以归为两类:已经初始化了的静态变量,线程仍在使用的对象。Reachable objects:指根据对象引用关系,从roots出发可以到达的对象。从roots出发可以创建reachable objects graph,剩余对象即为unreachable,可以被回收。
发现无法访问的对象时,它就使用内存复制功能来压缩内存中可以访问的对象,释放分配给不可访问对象的地址空间块。
指针修复是因为compact过程中移动了heap对象,对象地址发生变化,需要修复所有引用指针,包括stack,cpu register中的指针以及heap中其他对象的引用指针。传给COM+的托管对象也会成为root,并且具有一个引用计数以兼容COM+的内存管理机制,引用计数为0时,这些对象才可能成为被回收对象。Pinned objects指分配之后不能移动的对象,GC在指针修复时无法修改非托管代码中的引用指针。pinned objects会导致heap出现碎片。

2,托管堆

在垃圾回收器由CLR初始化以后,它会分配一段内存用于存储和管理对象。此内存为托管堆。
每个托管进程都有一个托管堆,进程中的所有线程都在同一个托管堆上为对象分配内存。
堆上分配的对象越小,垃圾回收器必须执行的工作就越少。分配对象时,请勿使用超出你需求的舍入值。
当触发垃圾回收时,垃圾回收器将回收由死对象占用的内存。回收进程会对活动对象进行压缩,以便将它们一起移动,并移除死空间,从而使堆更小一些。这将确保一起分配的对象都位于托管堆上,从而保留它们的局部性。
GC分配两个托管堆:一个用于小型对象(小型对象堆或SOH),一个用于大型对象(大型对象堆或LOH)
对象小于85000字节分配到小型SOH上,大于则分配到LOH上。
触发垃圾回收后,GC将寻找存在的对象并将它们压缩。但是由于压缩成本很高,GC会扫过LOH,列出没有被清除的对象列表以供以后重新使用,从而满足大型对象的分配请求。相邻的被清除对象将组成一个自由对象。

3,代数

虽然对象的生命周期因应用而异,但对于大多数应用来说,大部分的对象在创建不久即变成垃圾。因此针对不同age的对象也就不难理解了。
在.NET垃圾回收器中分三代,0,1,2。
小对象始终在第0代中进行分配,或者根据它们的生存期,可能会提升为第一代或者第二代。
大型对象始终在第二代中分配。
回收一代时,同时也会回收它前面所有代。
用户代码分配只能在SOH中的第0代或LOH中分配。只有GC可以在第一代和第二代中分配对象。
在mono中为写屏障来记录老代对新代的引用。

4,Finalization Queue和Freachable Queue

这两个队列和.NET对象所提供的Finalize方法有关。这两个队列并不用于存储真正的对象。而是存储一组指向对象的指针。当程序中使用new操作符在Managed Heap上分配空间时,GC会对其进行分析,如果该对象含有Finalize方法则在Finalization Queue中添加一个指向该对象的指针。
在GC被启动以后,经过Mark阶段分辨出哪些是垃圾。再在垃圾中搜索,如果发现垃圾中有被Finalization Queue中的指针所指向的对象。则把这个对象从垃圾中分离出来,并将指向它的指针移动到Freachable Queue中。这个过程被称为对象的复生(Resurrecction)。那为什么要救活它呢?因为这个对象的Finalize方法还没有被执行,所以还不能死去。当Freachable Queue中的指针指向的对象执行完Finalize方法以后,这个指针就会从队列中剔除,这个对象就能在下一次GC中被回收了。
.NET Framework的System.GC类提供了控制Finalize的两个方法,ReRegisterForFinalize和SuppressFinalize。前者是请求系统完成对象的Finalize方法,后者是请求系统不要完成对象的Finalize方法。ReRegisterForFinalize方法其实就是将指向对抗的指针重新添加到Finalization Queue中。如果在对象的Finalize方法中调用ReRegisterForFinzlize方法,这样就会形成一个在堆上永远不会死去的对象。

5,弱引用

如果应用程序代码访问一个正由该程序使用的对象时,垃圾回收器就不能回收该对象,那么就认为这个应用程序对该对象有强引用。
弱引用允许应用程序访问对象,同时也允许垃圾回收器收集相应的对象。如果不存在强引用,则弱引用的作用时间只限于收集对象前的一个不确定时间段。使用弱引用时,应用程序仍可以对该对象进行强引用,这样做可防止该对象被收集。但始终存在这样的风险:垃圾回收器在重新建立强引用之前先处理改对象。
若要对某对象建立弱引用,请使用要跟踪的对象实例创建WeakRefernce。然后将Target属性设置为该对象,将该对象的原始引用设置为null。

 aa a = new aa();
            a.a = 2;
            WeakReference c = new WeakReference(a);
            a = null;
            GC.Collect();
            Console.WriteLine(c.Target != null);
            Console.WriteLine(c.IsAlive);
            Console.WriteLine(c.Target);
/*
延时执行if判断的时候,c.Target是为null的
 var t1 = Task.Run(async delegate { await Task.Delay(5000); if (c.IsAlive)
                {
                }
            });
*/
/*
这段代码如果执行的话,在上一个GC期间c.Target是不为Null的,但是如果不执行的话,c.Target是null
  if (c.IsAlive)
                {
            }
*/
            GC.Collect();
/*
在GC调用完以后c.target为null。
*/
          Console.WriteLine(c.IsAlive);
            Console.ReadKey();

WeakRefernce也是一个引用对象,当这个对象也在被引用的时候,期内部的target是不会被回收的。

短弱引用和长弱引用

  • 短弱引用:
    垃圾回收功能回收对象后,短弱引用的目标将会变成null。弱引用本身是托管对象,与其他任何托管对象一样需要经过垃圾回收。
  • 长弱引用:
    在对象的Finalize方法已调用后,长弱引用获得保留。这样,便可以重新创建该对象,但该对象仍保持不可预知的状态,仍然可能在下一次的GC中被回收掉。若要使用长弱引用,请在WeakReference构造函数中指定true。
    如果对象类型不包含Finalize方法,或者Finalize方法没有被执行(调用了GC.SuppressFinalize();),那么将被应用为短弱引用。弱引用只在目标被收集前有效,运行终结器后可以随时收集目标。
    如果要建立强引用并重新使用对象,请将WeakReference的Target属性强行转换为对象类型。使用前请先判断Target是否为null。