最近在研究C#语言,发现还有很多细节上的问题掌握的不好,于是决定从头开始再细学一次C#。
这次目的为查漏补缺,选取的书目为《C# 6.0 本质论》,并对以前不了解的细节做了个记录_(:з」∠)_
所以这篇文章不会包含基础的C#知识,另外C#版本为6.0 _(:з」∠)_
一、C#概述
1.C#编译器允许为C#源代码文件使用任何文件扩展名,但一般使用”.cs”;
2.在C#关键字前添加“@”字符前缀可作为标志符使用;
3.
static void Main( string [ ] args )
args数组的第一个元素不是可执行文件名,而是之后第一个参数(区别于C++)
要想获取完整命令可以使用 System.Environment.CommanLine;
4.字符串插值:
$"{ 变量名 } , { 变量名 }"
5.如果想省略Console.WriteLine() 的Console,
可以在文件开始使用
using static System.Console;
注:using static 只对静态方法和属性有效,对实例成员不起作用;
二、数据类型
6.var的目的是为了支持匿名类型,一般情况尽量不用;
7.值类型与引用类型本质区别:
值类型:数据类型总是值复制;
引用类型:数据类型总是引用复制;
8.堆:引用类型指向的内存区域;
9.可空修饰符”?” 将null赋值给值类型,在数据库编程中尤为有用;
10.多维数组:Length 为元素总数
int [ , ] cells;
交错数组:Length 为外部数组的元素数
int [ ][ ] cells;
11.数组的Clear()方法将数组的每个元素都设为默认值;
12.数组的BinarySearch()方法(二分查找)使用前要对数组进行排序( Array.Sort() );
13.要获取数组的特定维的长度使用
GetLength( int 维度 );
14.使用Clone()创建数组的全新副本;
15如何讲一个字符串反转:
(1)使用String.ToCharArray()转换为字符数组;
(2)使用Array.Reverse() 反转数组;
三、操作符和控制流
16.使用圆括号增加代码易读性;
17.不同于C++,C#中操作数总是从左到右求值;
18.当必须本地化时,要用组合格式而不是加法操作符来拼接字符串;
例如使用String.Format()而不是“字符串”+“字符串”
19.避免将二进制浮点类型用于相等性条件;
20.C#中0/0f得到NaN;
1/0f等于无穷;
21.空格解释操作符 ??
expression1 ?? expression2
还可以连接使用 x ?? y ?? z …
22.null条件操作符 ? (有点复杂,具体用法百度搜索)
例如:
string args[ ];
if( args?.Length == 0 )
...
即
( args != null ) ? ( int? ) args.Length : null ;
23.round-trip格式说明符返回的字符串转换为数值肯定能获得原始值;
String.Format( "{ 0 : R } ",1.1234679... );
24.C#中转换进制
Convert.ToString(value, 进制数);
25.C#不允许从一个switch小节自然贯穿到下一个switch小节;
(可使用goto语句实现)
26.预处理指令的作用
(1)最常用的就是控制何时以及如何包含代码的指令;
(2)处理不同平台的差异;
(3)用于调试,例如#if DEBUG;
四、方法参数
27.在方法中尽可能地确定单一的推出位置;
28.表达式主题方法适用于单行可表示的方法;
29.String和string类型在CIL中无区别;
30.using:可以省略类型名之前的命名空间部分;
31.using static :允许省略规定类型的任何成员之前的命名空间和类型名称;
32.一系列的方法调用时会创建“调用栈”;
33.ref参数只是传递变量的别名;
即:只是为现有的变量分配了参数名,而不是创建新变量并将实参的值复制给它;
用作ref参数的变量必须在传给被调用的方法之前赋值,因为被调用的方法可能冲这个变量中读取值;
34.out参数在功能上与ref参数完全一致;
区别:每个正常返回的代码路径都必须对所有的out参数进行赋值;
out参数在调用之前不需要赋值;
35.如果方法一定要返回多个值,考虑使用元组类型;
36.参数数组在希望参数数量可变时使用;
例如:
static string Combine ( params string[ ] paths )
要求:
(1)在方法声明的最后一个参数之前添加params关键字;
(2)将最后一个参数声明为数组,方法最多只能有一个参数数组;
37.可选参数一定放在所有必须的参数后面,另外默认值必须是常量;
38.考虑到命名参数,要将参数名视为API的一部分,尽量避免更改参数名;
39.编译器只使用调用者显式标识的参数,忽略调用者没有指定的所有可选参数;
即无可选参数的方法优先于有可选参数的;
40.编译器优先选择拥有最具体参数类型的方法;
41.所有的异常都是System.Exception类型;
42.catch快必须按照从具体最大具体排列;
43.finally块最适合用来执行资源清理;
44.避免捕获无法获知其正确行动的异常,对这种异常不进行处理比处理的不正确要好;
45.手动引发异常 throw new 异常类型(“…”)
46.throw; 保持了异常中的调用栈信息;
throw exception; 讲那些信息替换为当前信息;
调试时一般需要知道原始调用栈;
47.有事catch块能捕获到异常,但不能正确或完整处理,可以让这个catch块重新引发异常 throw;
48.要通过异常来指明错误;
不要通过它们作为返回值来指明错误;
49.避免使用异常处理来处理预料之中的情况;
异常是为了跟踪例外的,事先没有意料到的,而且可能造成严重后果的情况而设计的;
50.可以使用 TryParse() 代替 Parse() ,而非异常;
51.通常开发者必须假定用户会发生非预期的行为,应当防御性的编写代码,提前为所有能想到的“愚蠢的”用户行为拟定对策;
五、类
52.在C#中应该将new的作用理解为实例化对象,而不是分配内存;
53.可以像初始化字段那样初始化属性;
public string Salary { get; set; } = "Not enough";
54.属性和方法的选择
(1)方法用于代表行动,属性用于代表数据;
(2)属性倾向于对简单的计算提供简单的访问;
(3)调用属性的代价不应比访问字段高出太多;
55.如果没有额外的实现逻辑,要优先使用自动实现的属性;
56.要一直实现属性,不要直接调用字段;
57.属性和方法调用不允许作为ref和out参数使用;
原因:ref和out参数值在内部是实现时,需要将内存地址传给目标方法,但是属性不可能传送存储地址;
58.new操作符的细节
(1)new获取“空白内存”调用构造器;
(2)构造器初始化该内存;
(3)结束后,new返回内存引用;
59.this关键字在静态方法中无效;
60.静态构造器在首次访问类时自动调用;
不能显示调用,所以不允许任何参数;
61.考虑以内联方式初始化静态字段;
62.静态类在编译时会被标注为abstract和sealed,表明不能派生;
63.常量字段自动成静态字段,但是将常量字段显式声明为static会报错;
64.与const不同,readonly只能用于字段,不能用于局部变量;
65.和const字段不一样,每个实例的readonly字段都可以不同;
66.readonly字段既可以是实例字段,也可以是静态字段;
67.可以在执行时为readonly赋值,而非只能在编译时赋值,
const只能在编译时赋值;
68.将readonly应用于数组不会冻结数组的内容,而是冻结数组实例,包括个数;
69.要优先使用只读的自动实现的属性,而不是定义只读字段;
70.const无法用于数组,除非将数组初始化为null;
readonly可以;
原因:数组是引用类型,而const要求提供一个编译期常量,引用类型的编译器常量只有null;
71.如果需要数组元素也不可变,可以使用ReadOnlyCollection类型;
六、继承
72.扩展方法从技术上说不是类型的成员,所以不可继承,但可在派生类中使用;
73.在极少数需要多继承结构的时候,一般的解决方案是使用集合,即一个类包含另一个类的实例;
74.基类除构造器和析构器之外的所有成员都会在派生类中继承;
75.C#支持重写实例方法和属性,但不支持重写字段和任何静态成员;
76.对成员进行重载会造成“运行时”调用最深或者说派生的最远的实现
77.不要在构造器中调用会影响所构造对象的任何虚方法,因为有以下设计原则:
“总是调用派生的最远的虚成员,即使派生的构造器尚未完全执行完毕;”
(这与C++恰好相反)
78.new修饰符在基类面前隐藏了派生类重新声明的成员;
79.就CIL来说,new修饰符对编译器生成的代码没有任何影响;
从C#的角度看,它唯一的作用就是移除编译器警告;
80.多态性是指同一个签名可以有多个实现;
81.is操作符不仅检查转型是否成功,还会检查底层对象本身是否真的是目标对象;
七、接口
82.命名空间内只能包含public或internal的类定义,结构定义,接口定义等,如果没有指定,默认是internal;
protected表明存取限制于包含此定义的类或继承类命名空间是不继承的,所以protected用于命名空间的定义是无意义的,而protected可以用于嵌套类;
83.接口成员不能使用访问修饰符,默认都是public;
84.接口除了包含方法外还可以包含属性,索引器,事件;
85.接口基本上不具备继承的任何具体特点;
它仅仅承诺了能够调用的方法;
86.接口可以用于支持回调,
而继承不具备这个特点;
87.抽象类实现的具体方法为虚方法;
88.好的接口定义应该是具有专一性的,而不是多功能的,如果一个类只是实现了这个接口中的一个功能,而不得不去实现接口中的其他方法,就叫接口污染;
89.应当避免使用继承来实现组建功能,
而是使用黑箱复用,即对象组合;
90.如果抽象类实现接口,则可以把接口中的方法映射到抽象类中作为抽象方法而不必实现;
在抽象类子类中实现接口中方法;
91.抽象类主要应用于关系密切的对象;
接口适合为不相关的类提供通用功能;
92.显式接口和隐式接口的区别:
(1)显式接口中的方法没有修饰符;
隐式接口方法修饰符为public;
(2)显式接口中的方法可以看到从哪里来(通过接口访问,避免访问歧义),来源清晰,隐式接口看不出来源;
(3)显式接口会吧父级接口中的方法和属性完全继承;
隐式接口会过滤冗余的方法(多个接口有重名的方法)
93.避免显式是实现接口成员,除非有很好的理由,
但如果不确定成员的用途,就先选择显式实现;
八,值类型
94.结构属于值类型;
95.要创建不可变的值类型;
96.除了属性和字段,结构还可以包括方法和构造器;
结构不允许包含用户定义的默认(无参)构造器;
在没有提供默认构造器时,C#编译器自动生成一个默认构造器将所有字段初始化为各自的默认值;
97.C#禁止结构中的字段初始化器;
98.结构的构造方法中的所有显式声明的字段都必须初始化;
99.对于结构中的字段应该优先使用只读的自动实现的属性;
100.结构不支持终结器;
101.所有的值类型都有自动定义的无参构造器将值类型的实例初始化成默认状态;
102.除了枚举之外所有的值类型都派生自System.ValueType;
枚举继承自System.Enum,System.Enum继承自System.ValueType;
103.不允许在lock语句中使用值类型;
104.避免可变的值类型;
105.拆箱转换生成的是哪个值的副本而不是对存储位置的引用;
106.0能隐式转换为任何枚举;
107.要为所有的标志枚举提供等于0的None值;
108.除非它在逻辑上代表单个值,消耗16个字节或更少的存储空间,不可变,而且很少装箱,否则不要定义结构;
九、良构类型
109.为值类型调用ReferenceEquals()将总是返回false;
原因会装箱;
110.垃圾回收器只回收内存,不处理其他资源;
比如(数据库连接,句柄(文件,窗口)等,网络端口以及硬件设备(串口))
111.垃圾回收器处理的事引用对象,只回收堆上的内存;
112.执行垃圾回收时,垃圾回收器不只是枚举所有访问不到的对象,相反,他将所有可达对象紧挨着放到一起,从而覆盖不可访问的对象(垃圾)所占的内存;
113.进程中的所有托管线程都会在垃圾回收期间运行;
114..NET垃圾回收器会以更快的频率尝试清除生存时间较短的对象;
115.编译时不能确定终结器的确定执行时间;
唯一确定的是终结器会在对象最后一次使用之后,并在应用程序正常关闭前的某个时间运行;
116.声明终结器要在类名前加“~”前缀;
没有修饰符,没有返回值,没有参数;
终结器不能显式调用,只有垃圾回收器才能调用;
基类中的终结器作为对象终结调用的一部分被自动调用;
117.终结器不负责回收内存,它们主要负责释放数据库连接和文件句柄这样的资源;
118.要在catch块中使用throw,而不是throw<异常对象>语句;
119.不要引发NullReferenceException;
相反,在值意外为空时引发ArgumentNullException;
十一、泛型
120.泛型类或结构的构造器(和终结器)不要求类型参数;
121.要将只是类型参数数量不同的多个泛型放到同一个文件中;
122.假如同时制定了多个约束,那么类型约束必须第一个出现(就像在类声明中,基类必须先于所实现的接口出现)
123.
124.并非所有的对象都保证有公共默认构造器,
所以编译器不允许为未约束的类型参数调用默认构造器。
125.new() 构造器约束,要求类型实参必须有默认构造器;
126.只能对默认构造器进行约束,
127.泛型类型参数及约束不会被继承;
128.多个接口约束之间是and关系,且不能改为or;
129.委托类型,数组类型和枚举类型不能再基类约束中使用;
130.两个方法可以有相同的名称和形参类型;
只要类型参数的数量不同;
未完待续
1 comment
😛