《你必须知道的.Net》读书笔记
- 值类型位于线程的堆栈,而引用类型位于托管堆,值类型和引用类型的角色也会发生转变,这个过程成为装箱与拆箱
- GC管理所有的托管堆的对象,当内存回收执行时,GC检查托管堆中不再被使用的对象,并执行内存的回收操作。
- 线程的堆栈,用于分配值类型实力。堆栈主要由操作系统管理,而不受垃圾收集器的控制,当值类型实例所在的方法结束时,其存储单位自动释放。栈的执行效率高,而存储容量有限。
- GC堆,用于分配小对象实例。如果引用类型对象的实力大小小于85000字节,实力将被分配在GC堆上,当有内存分配或者回收时,垃圾收集器可能会对GC堆进行压缩。
- LOH堆,用于分配大对象实例。如果引用类型对象的实例大小不小于85000字节时,该实例将被分配到LOH堆上,而LOH堆也不会被压缩,而只在完全GC回收是被回收。
- 对于值类型来说,一般创建在线程的堆栈上。但并非所有的值类型都在那里。例如作为类的字段时,值类型作为实例成员的一部分也被创建在托管堆上,装箱发生是,值类型的字段也会拷贝到托管堆上。
- 引用类型的实例分配位于托管堆上,而线程堆栈是生命周期开始的地方。托管堆最重要的是GC堆和加载堆。GC堆用于存储对象实例,受GC管理,加载堆最重要的信息就元数据相关的信息,其中564321·123记录了存储的元数据信息,例如基类型,静态段,实现的接口,所有的方法等。加载堆不受GC控制,其生命周期从创建到AppDomain卸载。
- TypeHandle,类型句柄,指向对应实例的方法表,每个对象创建时都包含该附加的成员,并且占用4个字节的内存空间。
- SyncBlockIndex,用于线程的同步,每个对象创建是也包含该附加成员,它指向一块被称为Synchronization Block的内存块,用于管理对象同步,同样占用4个字节的内存空·123456间。
- NextObjPtr,由托管堆维护的一个指针,用于标识下一个新建对象分配时在托管堆中所处的位置。
- 栈的分配是向低地址扩展,而堆的分配向高地址扩展。
- 如果试图分配所需空间而发现内存不足时,GC将启动垃圾收集操作来回收垃圾对象所占的内存。
- 调用对象构造器进行对象的初始化操作,完成创建过程。初始化对象的2个附加成员:TypeHandle和SyncBlockIndex。将TypeHandle指针指向LoaderHeap是的MethodTable,CLR将根据TypeHandle来定位具体的Type;将SyncBlockIndex指针指向Synchronization Block的内存块,用于多线程环境下对实例对象的同步操作。
- 对于值类型嵌套引用类型的情况,引用类型变量作为值类型的成员变量,在堆栈保存该成员的引用,而实际引用类型仍然保存在GC堆上;对于引用类型嵌套值类型的情况,则该值类型字段作为引用类型实例的一部分保存在GC堆上。
- 对象的创建过程是按照顺序完成了对整个父类及其本身字段的内存创建,并且字段的顺序是从上到下排列,最高层的字段排在最前面。
- 任何类型方法表中,开始的4个方法总是继承自system.object类型的虚方法,它们是 tostring(),equals(),GetHashcode(),Finalize();
- 密封类Sealed不可以被继承。继承关系中,我们更多关注其共性而不是特性,因为共性是层次复用的基础,而特性是系统扩展的基点。从宏观来看,继承多关注与共同性;而多态多着眼于差异性。继承的层次应该有所控制,否则类型之间的关系维护会消耗更多的精力。
- 封装的特征 :隐藏了系统的实现细节,保证系统的安全性和可靠性。提供稳定不变的对外接口,因此,系统相对稳定的部分常被抽象为接口。封装保证了代码模块化,提高了软件的复用和功能分离。
- .net的动态绑定成就了面向对象的多态特性。动态绑定,又叫做晚期绑定,是区别与静态绑定而言的。静态绑定在编译期就可以确定关联,一般是以方法重载来实现的;而动态绑定则在运行期间通过检查虚拟方法表来确定动态关联写的方法,一般以继承和虚方法来实现。
- 接口本质上是可以定义了抽象方法的类,该类仅提供了方法的定义,没有方法的实现,其功能有接口的实现类来实现。
- 接口的规则重要规则:接口隔离原则强调接口应该被实现为具有单一功能的小接口,而不要实现为具有多个功能的胖接口,类对于类的依赖应建立在最小的接口之上。禁止为已发布的接口,添加新的成员,这意味着你必须重新修改所有实现了该接口的类型,在实际应用中,这往往是不可能的事情。
- 通用类型系统CTS定义了如何在运行库中声明,使用和管理类型,同时也是运行库支持跨语言集成的一个重要组成部分。它建立了一个支持跨语言集成,类型安全和高性能代码执行的框架,提供了一个支持完整实现多种编程语言的面向对象的类型,定义了各种语言必须遵守的规则,有助于确保不同语言编写的对象能够交互作用。
- 公共语言规范CLS是CTS的子集,它是面向.net开发语言必须支持的最小集合。
- 通用中间语言CIL是一种基于堆栈的语言,它和java世界里的字节码不同,他是即时编译的。
- 公共语言运行时是.net框架的核心,它管理代码执行,提供CTS和基础性服务。
- .NET框架类库FCL提供了一整套的标准类型,我们要知道如何去使用。
- 类型转换,通常有is 和 as 两种方式,另外(typename)valuename,是通用方法:Convert类提供了灵活的类型转换方法;parse方法,适用于数字类型的转换。
- 可以给类型创建别名,例如 using mynet=anytao.net.byldc;实际上,我们常用的int ,char,string对应的是system.int32,system.char,system.string的别名
- 一个对象获得类型的办法是 obj.gettype(),typeof符,则常用在反射时,获得自定义类型的type对象,从而获取关于该类型的方法,属性等。
- 值类型和引用类型在内存中的分配区别是决定其应对不同的根本原因,由此我们可以很容易的解释为什么参数传递时,按值传递不会改变形参值,而按地址传递会改变形参值。
- string类型是一个特殊的引用类型,但它在应用表现上又凸显出值类型的特点。他在参数传递时还是按地址传递,不过由于特殊的恒定特性,在函数内部新建了一个string对象并完成初始化,但是函数外部取不到这个变化的结果,因此对外表现的特性就是按值传递。
- 通常可以使用IsValueType来判断是否一个值类型
- .NET中以操作符ref和out来标识值类型按引用类型方式传递,其中区别是:ref在参数传递之前必须初始化,而out则在传递前不必初始化,且在传递时必须显式赋值
- 作用域结束时,值类型会自行释放,减少托管堆的压力,因此具有性能上的优势。例如,通常struct比class更高效,而引用类型的内存回收,有GC完成。
- 值类型变量不可为null值,值类型都会自行初始化为0值,而引用类型变量默认的情况下,创建为null值,表示没有任何指向托管堆的引用地址。
- 对于值类型Equals是实现实例数据的判等,而引用类型是实现是否引用同一内存地址的判断(都为null也相等),与==等同,而ReferenceEquals只能用于引用类型,对于值类型返回的都是false
- GC执行时,会遍历所有的托管堆对象,按照一定的递归遍历算法找出所有的可达对象和不可访问的对象,如果某对象没有被任何引用访问,将被列入执行垃圾收集的目标。
- 泛型类型参数,可以是静态的,例如MyGeneric<int>;也可以是动态的,此时它其实就是一个占位符,例如MyGenric<T>中T可以是任何类型的变量,在运行期动态替换为相应的类型参数。
- 有一种灵活的机制来实现可变数目的参数,那就是param修饰符。如 param int[] args,param的参数必须是一维数组,可以是任何类型,但param必须在参数列表的最后一个且只能使用一次。
- 按值传递的实质的是传递值,不同的是这个值在值类型和引用类型的表现是不同的:参数为值类型是,“值”为实例本身,因此传递的是实例拷贝,不会对原来的实例产生影响:参数为引用类型时,“值”为对象的引用,因此传递的是引用地址拷贝,会改变原来对象的引用指向,这是二者在统一概念上的表现区别,理解了本质也就抓住了根源。
- .net平台下对象的创建,生存期管理及资源回收都由CLR负责,CLR引入垃圾收集器来负责执行内存的清理工作,GC通过对托管堆的管理,能有效解决C++程序中内斯的内存泄漏,访问不可大对象等问题。必须明确的是垃圾回收不能解决所有资源的清理,对于非托管资源,例如数据库连接,文件具备,com对象,网络连接,套接字,位图,互斥体,GDI+对象等,仍然需要开发者自行清理。非托管资源的清理主要由2种方式:Finalize方法和Dispose
.net对象的清理,主要包括以下几个方面:对象创建时内存分配,垃圾回收,非托管资源的释放。 - 堆栈的内存地址是由高位到低位向下填充,也就是表示入栈时栈顶向低地址扩展,出栈时,栈顶向高地址回退。数据的压栈和出栈是有顺序的,栈内是先进后出的形式,具体而言:首先入栈的是返回地址:然后是参数,一般一由右向左的顺序入栈;最后是局部变量,依次入栈。方法执行后,出栈的顺序正好相反,首先是局部变量,再就是参数,最后是那个地址指针。
- 静态字段也保存在方法表中,位于方法表的槽数组后,其生命首期为从川剧到AppDomain卸载,因此一个类型无论创建多少个对象,其静态字段在内存中只有一份。静态字段只能由静态构造函数进行初始化,静态构造函数确保在任何对象创建前,或者在任何静态字段或方法被引用前执行。
- 垃圾收集器周期性的执行内存清理工作,一般在一下情况垃圾收集器将会启动:内存不足溢出时,更确切的说是第0代对象充满时;调用GC.Collect方法强制执行垃圾回收;Windows报告内存不足时,CLR将强制执行垃圾回收;CLR卸载AppDomain,GC将对所有代龄的对象进行垃圾回收;
- GC在垃圾回收后,堆上将出现多个被收集对象的“空洞”,为避免托管堆的内存碎片,会重新分配内存,压缩托管堆。
- 对于重写了Finalize方法的类型来说,可以通过GC.SuppressFinalize来免除终结。一般情况下在自定义类型中应避免重写Finalize方法。
- using语句简化了资源清理代码实现,并且能够确保Dispose方法得到调用,凡是实现了Dispose模式的类型,均可以using语句来定义其引用范围。而且尽可能已using来执行清理资源。
- 在简单的字符串操作中使用string,在复杂的字符串操作中使用stringBuilder,简单的说,stringBuilder对象的创建代价较大,在字符串连接目标较少的情况下,应优先使用string类型;而在有大量字符串连接操作的情况下,优先考虑stringBuilder。同时使用stringBuilder时最好指定合适的容量值,否则由于默认容量的不足而频繁进行内存分配操作会影响系统性能。
- base常用于在派生类对象初始化是和基类通信,而this仅仅局限与对象内部,外部无法看到。另外静态成员不是对象的一部分,因此不能在静态成员中引用this。base用与在派生类中访问重写的基类成员;而this用于访问本类的成员,当然也包括继承而来共有和保护的成员。尽量少用或者不用this和base,容易引起不必要的后果。
- using引入命名空间,并不等于编译器编译时加载该命名空间所在的程序集,程序集的加载决定与程序中对该程序集是否存在调用操作。using也可为命名空间创造别名 using MSWord = Microsoft.Office.Interop.Word;用using语句来释放资源时,编译器将自动为using生成try-finally语句,并在finally块中调用对象的Dispose方法,来清理资源。
- using只能用于实现了IDispose接口的类型,禁止为不支持IDisposeale接口的类型使用using语句,否则会出现编译时错误。using语句适用与清理单个非托管资源的情况,而多个非托管对象的清理最好try finally来实现,因为嵌套的using语句可能存在隐藏的bug。内层using块引发异常时,将不能释放外层using块的对象资源。using语句支持初始化多个变量,但前提是这些变量的类型必须相同,(可以用IDisposeable接口来完成所有类型的初始化)另外,using初始化的对象,也可以在using语句之前声明。
- Dispose方法用于清理对象封装的非托管资源,而不是释放对象的内存,对象的内存依然是有垃圾回收器来控制。
- 定制特性attribute,本质上是一个类,其为目标元素提供关联的附加信息,并在运行期一反射的方式来获取附加信息。我理解的定制特性,就是为目标元素,可以是数据集,模块,类,属性,方法,甚至是函数参数等假如附加信息,类似与注释,但是可以在运行期已反射的方式来获得。定制特性的主要应用在序列化,编译器指令,设计模式等方面。
- 一些特性的举例:flags:以flags特性来将枚举值看作位标记,而非单独的数值;DllImport:DllImport特性,可以让我们调用非托管代码,所以我们可以使用DllImport特性引入对Win32 API函数的调用,对于习惯了非托管代码的程序员来说,这无疑是救命的稻草。Serializable特性表明了应用的元素可以被序列化。Conditinal:用与条件编译,在调试时使用。
- MSDN认为,特性描述如何将数据序列化,指定用于强制安全性的特性,并限制实时(JIT)编译器的优化,从而使代码易于调试,属性还可以记录文件名或代码作者,或在窗体开发阶段控制控件和成员的可见性。
- 如果预计要创建组建的多个版本,则创建抽象类。抽象类提供简单易行的方法来控制组建的版本。通过更新基类,所有继承类都随更改自动更新。另一方面,接口一旦创建就不能更改。如果需要接口的新版本,必须创建一个全新的接口。如果创建的功能将在大范围的全异对象间使用,则使用接口。抽象类应主要用于关系密切的对象,而接口最适合为不相关的类提供通用功能。
- 任何类型都可以安全的转换为其基类类型,可以由隐式的转换来实现;任何类型转换为其派生类型时,必须进行显式转换,转换的原则是:(类型名)对象名
- Is/as操作符,是C#中用于类型转换的,提供了对类型兼容性的判断,从而使得类型转换控制在安全的范畴,提供了灵活的类型转换控制。检查对象类型的兼容性,并返回结果,Is返回true或者false,对于as,如果不兼容就返回null
- GetType方法为非虚的,用于在运行时通过查询对象元数据来获取对象的运行时类型。通过反射机制,就可以根据GetType方法返回的Type对象在运行期枚举出元数据表中定义的所有类型的信息,并根据System.Reflection空间中的方法获取类型的信息。包括:字段,属性,方法,参数,事件;
- GetType方法与typeof运算符的区别:GetType是非强类型的方法,而typeof运算符支持强类型;如Type tp = Type.GetType("InsideDotNet.Framework.object.MyType"); Type tp=typeof(InsideDotNet.Framework.object.MyType) ;另外 Type.GetType支持运行时跨程序集反射,已解决动态引用,而typeof只能支持静态引用 Assembly ass = Assembly.LoadFrom(@"C:\Anytao.utility.exe"); Type tpd=ass.GetType("Anytao.Utility.Message.AnyMsg");
- GetHashCode方法,用于获取对象的哈希值,已应用于哈希算法,加密和校验等操作中。相同的对象必然具有相同的哈希值。因此GetHashCode的行为依赖于Equals方法进行判断,在覆写equals方法时,也必须覆写GetHashCode,已同步二者在语义上的统一。
- 字符串的恒定性,是指字符串一经创建,就不可改变。具体而言,字符串一旦创建,就会在托管堆上分配一块连续的内存空间,我们对其的任何改变都不会影响到原string对象,而是重新创建出新的string对象。对字符串的任何操作,包括字符串比较,字符串链接,字符串格式化等会创建新的字符串,从而伴随着性能与内存的双重损耗。CLR以字符串驻留机制来解决这个问题:对于相同的字符串,CLR不会为其分别分配内存空间,而是共享同一内存。CLR初始化是,会创建一个空哈希表,当JIT编译方法时,会首先在哈希表中查找每一个字符串常量,如果key值相同,则不会创造出内存空间。如果是动态生成的字符串,这样的字符串是不会便添加到哈希表中维护的。
- Parse,TryParse,Convert这三种方法的区别主要是对异常的处理机制上:如果转换失败,则Parse方法总会抛出异常,主要包括ArgumentNullException,OverflowException,FormatException等;TryParse 则不会抛出任何异常,而返回false标志解析失败;Convert 方法在str为null是不会抛出异常,而是返回0.
- string和System.String编译为IL代码时,会产生完全相同的代码。String对象是恒定不变的,而System.Text.StringBuilder 对象表示的字符串是可变的。StringBuilder是.NET提供的动态创建String对象的高效方式,以克服String对象恒定性所带来的影响,克服了对String对象进行多次修改带来的创建大量String对象的问题。因此,基于性能的考虑,我们应该尽可能使用StringBuilder来动态创建字符串,然后以ToString方法将其转换为String对象应用。
- 枚举类型是值类型,分配与线程的堆栈上,自动继承于Enum类型,但是本身不能被继承,Enum类型是引用类型,分配于托管堆上。Enum类型本身不是枚举类型,但是提供了操作枚举类型的共用方法。
- 位标记集合是一种由组合出现的元素形成的列表,通常设计为以“位或”运算组合新值;枚举类型则通常表达一种语义相对独立的数值集合。
- 委托无处不在,事件模型建立在委托的机制上,Lambda表达式本质上就是一种匿名的委托。委托表示了对其回调方法的签名,可以将方法当作参数进行传递,并根据传入的方法来动态的改变方法调用。只要为委托提供相同的签名的方法,就可以与委托绑定。将多个方法绑定到一个委托变量,在调用一个方法时,可以一次执行其绑定的所有方法,这种技术叫做多播委托。以+=和-=操作符分别进行绑定和解除绑定的操作,多个方法绑定到一个委托变量就形成了一个委托链,对其调用时,将会依次调用所有绑定的回调方法。
- 匿名方法以内联方式放入委托对象的使用位置,而避免创建一个委托来关联回调方法,也就是由委托调用了匿名的方法,将方法代码和委托实例直接关联,在语法上有简洁和直观的好处。button1.Click += delegate { MessageBox.Show("Hello world."); };或//匿名方法 CalculateDelegate mySubstractDelegate = delegate(Int32 x, Int32 y){ Console.WriteLine(x - y); };应用Lambda表达式来实现相同的过程 CalculateDelegate myDelegate = (x, y) => Console.WriteLine(x - y);
- 以Delegate作为委托类型的后缀,以EventHandle作为事件委托的后缀,是规范的命名规则。多播委托返回值一般以void,不推荐在多播委托中返回非void的类型。
- Try语句不能单独存在,必须和零到多个catch字句或者finally字句配合使用,其中,catch字句包含各种异常的响应代码,而finally字句则包含资源清理代码。编译器要求将特定程度较高的异常放到前面(如DivideByZeroException类),而将特定程度不高的异常放到后面(如示例中最下面的catch字句可以响应任何异常),catch子句的执行代码通常会执行从异常恢复的代码,在执行末尾可以通过throw关键字再次引发由catch捕获的异常,并添加相应的信息通知调用端更多的信息内容。finally块之后的代码段不总是被执行,因为在引发异常并且没有被捕获的情况下,将不会执行该代码,因此,对于必须执行的处理环节,必须放在finally字句中。
- C#除可单独声明泛型类型(包括类与结构)外,也可以在基类中包含泛型类型的声明。但基类如果是泛型类,它的参数类型要么已实例化,要么来源与子类(同样是泛型类型)声明的类型参数。泛型类型的成员可以使用泛型类型声明中的类型参数。但类型参数如果没有任何约束,则只能在该类型上使用从System.Object继承的共有成员。
- 泛型接口的类型参数要么已实例化,要么来源于实现类声明的类型参数。泛型委托支持在委托返回值和参数上应用参数类型,这些参数类型同样可以附带合法的约束。
- 泛型方法简介:C#泛型机制只支持 在方法声明上包含类型参数;不支持在除方法以外的其它成员(包括属性,事件,索引器,构造器,析构器)的声明上包含类型参数,但这些成员本身可以包含在泛型类型中,并使用泛型类型的参数类型。泛型方法的声明与调用 //不是泛型类,是一个具体的类,这个类不需要泛型类型的实例化 public class Finder { // 但是是一个泛型方法,请看泛型方法的声明,参数要求泛型化 public static int Find<T> ( T[] items, T item) { for(int i=0;i<items.Length;i++){ if (items[i].Equals(item)) { return i; } } return -1; }} // 泛型方法的调用<int>不是放到Finder后面,而是放在Find后面。 int i=Finder.Find<int> ( new int[]{1,3,4,5,6,8,9}, 6);
- 泛型类型的重写abstract class Base { public abstract T F<T,U>(T t, U u) where U: T; public abstract T G<T>(T t) where T: IComparable; }
- 泛型约束:• C#泛型要求对“所有泛型类型或泛型方法的类型参数”的任何假定,都要基于“显式的约束”,以维护 C#所要求的类型安全。 • “显式约束”由where子句表达,可以指定“基类约束”,“接口约束”,“构造器约束”“值类型/引用类型约束”共四种约束。 • “显式约束”并非必须,如果没有指定“显式约束”,泛型类型参数将只能访问System.Object类型中的公有方法。其中构造器约束class A { public A() { } } class B { public B(int i) { } } class C<T> where T : new() { //可以在其中使用T t=new T(); …. } C<A> c=new C<A>(); //可以,A有无参构造器 C<B> c=new C<B>(); //错误,B没有无参构造器
- T?是可空类型,可空类型表示值为null的值类型,Nullable<T>和T?是等效的
- ??运算符,如果左侧操作数为null,则返回右侧操作数的值