上一节介绍到了如何进行Object方法重写和操作符重载,本篇博客来接着介绍合式类型剩余的内容:
程序集引用及命名空间定义
其中一些相对简单的内容就不进行过多的介绍了,例如引用其他程序集,只需要注意三种引用方式:
- 第一种方式是引用库项目文件,指出库的源代码在哪个项目中,并在两个项目之间建立依赖关系。编译好库之后才能编译引用了该库的程序。该依赖关系造成在编译程序时先编译库(如果还没有编译的话)项目拷贝,常用
- 第二种方式是引用程序集文件本身。换言之,引用编译好的库而不是项目。如果库和程序分开编译,比如由企业内的另一个团队编译,这种方式就非常合理。(dll拷贝,调试时候用)
- 第三种方式是引用NuGet包(项目拷贝高级版,从NuGet库拷贝项目)
注意库和包并非只能由控制台程序引用。事实上,任何程序集都能引用其他任何程序集。经常是一个库引用另一个库,创建一个依赖链。同时可以关注下类型的访问级别:
需要注意:成员的可访问性不能大于其包容类,类(除了嵌套类,其可使用任意修饰符)只能使用public和internal。命名空间比较简单,只需要注意如下设计规范就行了:
- 要为命名空间附加公司名前缀,防止不同公司使用同一个名称。
- 要为命名空间二级名称使用稳定的、不随版本升级而变化的产品名称。
- 不要定义没有明确放到一个命名空间中的类型。
- 考虑创建和命名空间层次结构匹配的文件夹结构。
总而言之就是,最好一个类在一个文件,用一个命名空间包裹一个类。
XML注释
虽然C#编译器在最终生成的可执行文件中忽略所有注释,但开发者可利用命令行选项,指示编译器将XML注释提取到单独的XML文件中。这样就可根据XML注释生成API文档。此外,C#编辑器可解析代码中的XML注释,并对其进行分区显示(例如,可使用有别于其他代码的一种颜色),或者解析XML注释数据元素并向开发者显示。这样就可以进行文档规范化操作,例如可以定义一些参数,生成API文档的时候予以解析
垃圾回收
垃圾回收是“运行时”的核心功能,旨在回收不再被引用的对象所占用的内存。这句话的重点是**“内存”和“引用”。垃圾回收器只回收内存,不处理其他资源,比如数据库连接、句柄(文件、窗口等)、网络端口以及硬件设备(比如串口)。此外,垃圾回收器根据是否存在任何引用来决定要清理什么。这暗示垃圾回收器处理的是引用对象,只回收堆上的内存。另外,还意味着假如维持对一个对象的引用,就会阻止垃圾回收器重用对象所用的内存。**
垃圾回收算法
.Net使用mark-and-compact算法,就是先把可达对象整理和覆盖不可访问对象内存,然后执行清理,整个操作流程如下,使用可达性算法进行:
- 一次垃圾回收周期开始时,识别对象的所有根引用。根引用是来自静态变量、CPU寄存器以及局部变量或参数实例(包括f-reachable对象)的任何引用基于该列表,垃圾回收器可遍历每个根引用所标识的树形结构,并递归确定所有根引用指向的对象。这样,垃圾回收器就可识别出所有可达对象。
- 执行垃圾回收时,垃圾回收器不是枚举所有访问不到的对象;相反,它将所有可达对象紧挨着放到一起,从而覆盖不可访问的对象(也就是垃圾,或者不可达对象)所占用的内存,为定位和移动所有可达对象,系统要在垃圾回收器运行期间维持状态的一致性。为此,进程中的所有托管线程都会在垃圾回收期间暂停。这显然会造成应用程序出现短暂的停顿。不过,除非垃圾回收周期特别长,否则这个停顿是不太引人注意的。为尽量避免在不恰当的时间执行垃圾回收,System.GC对象包含一个**Collect()**方法。可在执行关键代码之前调用它(执行这些代码时不希望GC运行)。这样做不会阻止垃圾回收器运行,但会显著减小它运行的可能性——前提是关键代码执行期间不会发生内存被大量消耗的情况
- 发现相较于长期存在的对象,最近创建的对象更有可能需要垃圾回收。为此,.NET垃圾回收器支持“代”(generation)的概念,它会以更快的频率尝试清除生存时间较短的对象(新生对象)。而那些已在一次垃圾回收中“存活”下来的对象(老对象)会以较低的频率清除。具体地说,共有3代对象。一个对象每次在一个垃圾回收周期中存活下来,它都会移动到下一代,直至最终移动到第二代(从第零代开始)。相较于第二代对象,垃圾回收器会以更快的频率对第零代的对象执行垃圾回收
相较于Java的成体系的垃圾回收算法,感觉CLR做的不是很好,也许是没有体系化的了解过吧。
弱引用
弱引用是什么?弱引用就是一直维持的一个引用,但是它并不会组织垃圾回收,为什么要有弱引用呢?这么去想象一下,假如要从数据库加载一个特别大的对象,如果是强引用,一旦用户不再使用该对象,断开引用就需要进行垃圾回收,但假如用户一会儿又想用了,又得重新加载和引用,但如果是弱引用,你可以在保持引用的状态下进行垃圾回收,但在你垃圾回收之前我还是一直保持引用状态,那么假如用户下次请求的时候刚好CLR还没有回收对象,就可以直接获取到对象了,实际上相当于一次内存里的缓存。
public class Program { private WeakReference Data; public FileStream GetData() { FileStream data = (FileStream)Data.Target; if(data != null) { return data; } else { // Load data // ... // Create a weak reference // to data for use later Data.Target = data; } return data; } public static void Main() { Console.WriteLine("No output in this example."); } }
创建弱引用(Data)之后,可查看弱引用是否为null来检查垃圾回收。但这里的关键是先将弱引用赋给一个强引用(FileStream data=Data),避免在“检查null值”和“访问数据”这两个动作之间,垃圾回收器运行并清除弱引用。强引用明显会阻止垃圾回收器清除对象,所以它必须先被赋值(而不是先检查Target是不是为null)
终结器
我们知道垃圾回收器不负责处理除了堆上的引用资源,那么除了内存管理外的一些数据库连接以及句柄等这些资源该怎么释放呢,这个时候就要用到终结器,同时终结器在垃圾回收前也发挥着不可替代的作用(虽然会降低性能,但是能保证对象被延迟清除):
class TemporaryFileStream { public TemporaryFileStream(string fileName) { File = new FileInfo(fileName); Stream = new FileStream( File.FullName, FileMode.OpenOrCreate, FileAccess.ReadWrite); } public TemporaryFileStream() : this(Path.GetTempFileName()) { } // Finalizer ~TemporaryFileStream() { Close(); } public FileStream Stream { get; } public FileInfo File { get; } public void Close() { Stream?.Dispose(); File?.Delete(); } } }
上边这个终结器用来删除文件和关闭流。需要注意的几点是:
- 终结器不能被显式调用,只能通过垃圾回收器调用,会在对象最后一次使用之后调用,开发人员只能定义不能调用
- 终结器不允许传递任何参数,不可重载,不可添加访问修饰符
这里的dispose一个对象不是垃圾回收一个对象,而是释放该对象中包装的资源,例如它字段所引用的对象,解除引用,具体的垃圾回收还是需要垃圾回收器的。这里有个漏洞会导致Dispose()执行不了,也就是在实例化TemporaryFileStream后,调用Dispose()前如果发生异常可能不会调用Dispose(),这个时候就要使用确定性终结:
public static void Search() { using(TemporaryFileStream fileStream1 = new TemporaryFileStream(), fileStream2 = new TemporaryFileStream()) { // Use temporary file stream; } }
相当于包裹了一个try finally块。所以实现了终结器的方法的对象的垃圾回收流程是这样的(如果终结器不调用System.GC.SuppressFinalize):整体流程是:先由可达性分析算法把待清理的对象引用放到f-reachable队列里,然后由终结器进行引用终结,终结完成后垃圾回收器才对对象进行垃圾回收。需要注意以下几点:
- 要只为使用了稀缺或昂贵资源的对象实现终结器方法,即使终结会推迟垃圾回收。
- 要为有终结器的类实现IDisposable接口以支持确定性终结。
- 要为实现了IDisposable的类实现终结器方法,以防Dispose()没有被显式调用。
- 要重构终结器方法来调用与IDisposable相同的代码,可能就是调用一下Dispose()方法。
- 不要在终结器方法中抛出异常。
- 要从Dispose()中调用System.GC.SuppressFinalize(),使垃圾回收更快地发生,并避免重复性的资源清理。
- 要保证Dispose()可以重入(可被多次调用)。
- 要保持Dispose()的简单性,把重点放在终结所要求的资源清理上。
- 避免为自己拥有的、带终结器的对象调用Dispose()。相反,依赖终结队列清理实例。
- 避免在终结方法中引用未被终结的其他对象(对象复活,在f-reachable队列中又被外部引用了)。
- 要在重写Dispose()时调用基类的实现。
- 考虑在调用Dispose()后将对象状态设为不可用。对象被dispose之后,调用除Dispose()之外的方法应引发ObjectDisposedException.异常。(Dispose()应该能多次调用。)
- 要为含有可dispose字段(或属性)的类型实现IDisposable接口,并dispose这些字段引用的对象
可以看到一个对象被回收会经历多么复杂的过程,那么我们最好让他在需要出现的时候再初始化岂不更好:
using AddisonWesley.Michaelis.EssentialCSharp.Chapter10.Listing10_21; class DataCache { // ... public TemporaryFileStream FileStream { get { if (_FileStream == null) { _FileStream = new TemporaryFileStream(); } return _FileStream; } } private TemporaryFileStream _FileStream = null; // ... }
只有调用FileStream属性的get方法时才加载TemporaryFileStream。
总结一下,其实也就是程序集如何加载,命名空间使用规范,垃圾回收如何执行以及终结器(析构函数),它的好处和缺点。