读书笔记:《C#与.NET4高级程序设计》-核心部分
虽然使用完全限定名定义一个类型可以提高程序的易读性,但C#的using关键字能够减少按键次数。一般选择C# using 关键字的简化方式,而不使用完全限定名(除非它们的定义含糊不清,可能发生歧义)。然而,请记住using关键字只是特定类型的完全限定名的简单速记符号,每种方法最后都会得出相同的底层CIL(事实上,CIL代码总是使用完全限定名),并且对程序集的大小和性能没有任何影响。
使用Visual Studio找到了你希望激活的代码块之后,就按两次Tab键。它会自动完成整个代码块并且留下一组占位符,然后你就可以填充它来完成代码块。如果再按Tab键,就可以切换每个占位符并且填充内容(按Esc键来退出代码段编辑模式)。
在调用方法含有重载时,智能感知会列出这个方法所有版本的列表。我们可以通过使用键盘的上下键在各个重载方法之间切换。
使用Visual Studio的“类图”(注意要选定项目才能加载项目内的所有类)功能来设计能够大大节约时间。
数值类型都支持MaxValue和Minvalue属性,这两个属性说明了给定类型的可以存储的范围。
System.Numerics.BigInteger数据类型可用来表示较大的数值,它没有固定的上下限。
System.Numerics.Complex结构用来对数学中的复数数据进行建模。
StringBuilder 的独特之处在于,当我们调用这个类型的成员时,都是直接修改对象内部的字符数据(因此更高效),而不是获取按修改后格式的数据副本。
使用var声明本地变量并不能带来什么好处。这样做会给其他阅读代码的人带来困扰,由于你无法快速判断实际的数据类型,因此难以理解变量的整体功能。所以,如果需要int,就应该声明为int。但隐式类型在LINQ技术中确实是不可或缺的。事实上,可以说只有在定义LINQ查询的返回数据时才应该使用var关键字。在产品代码中滥用隐式类型(通过var关键字)被认为是一种糟糕的设计。
输出参数和引用参数之间的区别:
◎输出参数(out)不需要在它们被传递给方法之前初始化(如果这样做了,原来的值在调用后就会丢失),因为方法在退出之前必须为输出参数赋值。
◎引用参数(ref)必须在它们被传递给方法之前初始化,因为是在传递一个对已存在的变量的引用。如果不赋值给它初始值,就相当于要对一个未赋值的本地变量进行操作。
◎参数数组(params)允许将一组可变数量的参数作为单独的逻辑参数进行传递。方法只能有一个params修饰符,而且必须是方法的最后一个参数。事实上,你不会经常使用params修饰符。但要知道的是,基础类库中的许多方法都使用了这个C#语言特性。
◎可选参数允许方法的调用者不指定不必要的参数,而是使用这些参数的默认值。分配给可选参数的值必须在编译时确定,而不能在运行时确定。并且必须放在必须参数的后面。
◎命名参数允许你在调用方法时以任意顺序指定参数的值。因此,你可以使用冒号操作符通过名称来指定参数,而不必按位置传递参数。命名参数是为了调用可选参数而设计的。
按值传递引用类型和按引用传递(out或ref)引用类型是不同的:
◎如果按引用传递引用类型,被调用者可能改变对象的状态数据的值和所引用的对象。
◎如果按值传递引用类型,被调用者可能改变对象的状态数据的值,但不能改变所引用的对象。
问题 |
值类型 |
引用类型 |
这个类型分配在哪里 |
分配在栈上 |
分配在托管堆上 |
变量是怎样表示的 |
值类型变量是局部复制 |
引用类型变量指向被分配的实例所占的内存 |
基类型是什么 |
必须派生自System.ValueType |
可以派生自除了System.ValueType以外的任何类型,只要那个类型不是密封的 |
这个类型能作为其他类型的基类型吗 |
不能。值类型总是密封的,不能被继承 |
能。如果这个类型不是密封的,它可以作为其他类型的基类 |
默认的参数传递行为是什么 |
变量是按值传递的(也就是说,一个变量的副本传入被调用的函数) |
对于值传递,对象按地址复制。对于引用传递,对象按引用复制 |
这个类型能够重写System.Object.Finalize()吗 |
不能。值类型不会放在堆上,因此不需要被终结 |
可以间接地重写(析构函数) |
可以为这个类型定义构造函数吗 |
是的,但是默认的构造函数被保留(也就是自定义构造函数必须全部带有参数) |
当然 |
这个类型的变量什么时候消亡 |
当它们越出定义的作用域时 |
当托管堆被垃圾回收时 |
可空运算符?是使用Nullable<T>的一种简化表示,这种语法只对值类型是合法的(引用类型本来就可空),利用空值合并运算符??能够将空值被一个非空值替代(若原本是非空值将不变)。
每一个C#类都提供了内建的默认构造函数,需要时可以重新定义。根据定义,默认的构造函数不会接受参数。除了把新对象分配到内存中,默认构造函数确保所有字段数据都设置为正确的默认值(数值型为0字符串为空字符串,引用类型为null)。
this 关键字的另一种用法是使用一项名为构造函数链的技术来设计类。当类定义了多个构造函数时,这个设计模式就会很有用。让一个接受最多参数个数的构造函数做“主构造函数”,并且实现必须的验证逻辑。其余的构造函数可以使用this关键字把传入的参数转发给主构造函数,并且提供所有必需的其他参数。这样,整个类中只会有一个构造函数需要我们去操心,其余构造函数基本上都是空的。使用这项技术可以简化编程任务,因为真正的工作都交给一个构造函数来做,而其他构造函数只是在推卸责任。调用顺序是先调用主构造函数再回到调用的构造函数执行剩余的代码。base关键字与之类似,但代表调用的是基类的构造函数。(在.NET 4.0中可以使用可选参数更加简便)
如果同种类的所有对象都经常使用某个值,那么非常适合使用静态数据。
静态构造函数是特殊的构造函数,并且非常适用于初始化在编译时未知的静态数据的值(例如,我们需要从外部文件读取值或者生产随机数等):
◎一个类只可以定义一个静态构造函数。
◎静态构造函数不允许访问修饰符并且不能接受任何参数。
◎无论创建了多少类型的对象,静态构造函数只执行一次。
◎运行库创建类实例或调用者首次访问静态成员之前,运行库会调用静态构造函数。
◎静态构造函数的执行先于任何实例级别的构造函数。
静态类只能包含静态成员或字段,只包含静态功能的类或结构通常称为工具类。在设计工具类时,将类定义为静态类是一个非常好的做法。
OOP的3个核心原则:
◎封装:怎样隐藏一个对象的内部实现并且保护数据完整性?
◎继承:怎样促进代码重用?
◎多态:怎样让你同样的方式处理相关对象?
自动属性是C#语言提供了另外一种使用最少的代码定义简单的字段封装服务的方法。自动属性必须同时声明get; 和set;但可以用访问修饰符限定它。
为了简化新建对象的过程,C#提供了对象初始化器语法。使用这项技术,只用少量代码就可以创建对象并设置一些属性和公共字段。在语法上,对象初始化器的组成为:大括号内部用逗号分隔的指定值列表(和创建数组并赋初始值有些相似)。若是使用默认的构造函数还允许省略“()”。对象初始化器语法只是先使用构造函数然后再设置各个属性状态数据的语法的简写形式。例如:
class Program { static void Main() { Rectangle rectangle = new Rectangle { TopLeft = new Point { X = 0, Y = 0 }, BottomRight = new Point { X = 1, Y = 1 } }; } class Point { public int X { set; get; } public int Y { set; get; } } class Rectangle { public Point TopLeft { set; get; } public Point BottomRight { set; get; } } }
始终不会改变的值应该定义成常量,而不应该使用幻数。例如圆周率,如果每个地方都用3.14表示的话,如果需要修改成更加精确的数将要修改每个用到的地方。常量是隐式静态的并且必须在声明时赋初始值。
只读字段(readonly)只能在声明时或构造函数内赋值,赋值后不能改变。
自定义异常类(可以通过输入Exception后按两次Tab键获得):
◎继承自ApplicationException类;
◎有[System.Serializable]特性标记;
◎定义一个默认的构造函数;
◎定义一个设定继承的Message属性的构造函数;
◎定义一个处理“内部异常”的构造函数;
◎定义一个处理类型序列化的构造函数。
对于处理多个异常:最前面的catch捕获最特定的异常(派生关系链中排在最上面的派生类型),最后面的catch捕获最普通的异常(通常是System.Exception)
New关键字返回的是一个指向托管堆上对象的引用,而不是真正的对象本身。如果在方法作用域中将引用变量声明为本地变量,这个引用变量保存在栈内,以供应用程序以后使用。
对象的代:
第0代:从没有被标记为回收的新分配的对象。
第1代:在上一次垃圾回收中没有被回收的对象(也就是,它被标记为回收,但因为已经获取了足够的堆空间而没有被删除)。
第2代:在一次以上的垃圾回收后仍然没有被回收的对象。
第0代和第1代称为暂时代,垃圾回收过程对于暂时代的处理是不同的。垃圾回收器首先要调查所有的第0代对象。如果标记和清除这些对象得到了所需数量的空闲内存,任何没有被回收的对象都被提升到第1代。如果算上所有的第0代对象后,仍然需要更多的内存,就会检查第1代对象的“可访问性”并相应地进行回收。没有被回收的第1代对象随后被提升到第2代。如果垃圾回收器仍然需要更多的内存,它会检查第2代对象的可访问性。这时,如果一个第2代对象在垃圾回收后仍然存在,它仍然是第2代对象,因为这是预定义的对象代的上限。这样,通过给堆上的对象赋一个表示代的值,尽快地删除一些较新的对象(如本地变量),而不会经常“打扰”一些旧对象(例如程序的应用程序对象)。
使用GC.Collect()强制垃圾回收可能会有好处:
◎应用程序将要进入一段代码,之后不希望被可能的垃圾回收中断;
◎应用程序刚刚分配了非常多的对象,你想尽可能多地删除已获得的内存。
当手动强制垃圾回收时,应该总是调用GC.WaitForPendingFinalizers()。这样你可以稍等片刻,以确定在程序继续执行之前,所有可终结的对象都必须执行所有必要的清除工作。
延时对象实例化:使用泛型类Lazy<>,该类所定义的数据在代码库实际使用它之前是不会被创建的。