1、1第 1 章 基本的代码风格1.1 换行的讲究1.1.1 寻找最佳的断行位置 对于很长的条件表达式,通过在“ll”、 “如果这样更好:double containerAspectRatio=(double)container.ClientWidth|container.ClientHeight; 按逻辑进行分行Rectangle imageBounds=new Rectangle(itemBounds.X+padding,itemBounds.Y+padding,itemBounds.Width-padding,2,itemBounds.Height-padding,2);1.1.2 每行只写
2、一条语句private static void Swap(object a,object b) object temp;temp = a;a = b; b = temp;1.1.3 分行定义变量int num, factor, index, length; 的写法不妥分行有助于写注释:/要计算的数值int num;/表示影响因子 int factor;/元素所在的索引号int index;/数据列表的总长int length;或者:int num, /要计算的数值factor, /表示影响因子index, /元素所在的索引号length; /数据列表的总长但考虑到移植性还是前者比较好。21.2
3、 避免代码过于拥挤1.2.1 使用空行分隔代码块using System;using System.Collections.Generic;/这里用空行隔开namespace Avilla/下面的内容略1.2.2 使用空格降低代码密度 单目运算符(Unary Operators)与它的操作数之间应紧密相接,不需要空格。y=+x /+在这里是前缀单目运算,它与 x 之间无空格 在双目、三目运算符(Binary/Ternary Operators)的左右两侧分别添加空格。int a = 3 + 5; /在双目运算符左右添加空格int b = a * 6 + 7; int c = a b;int d
4、 = b+ * c-; int e = a 0 ? 1 : 0; /在三目运算符左右添加空格 在函数调用时不添加空格,而在一些类似的带括号的语法结构中添加空格。string cmd = string.Empty;/函数形式的调用,括号前没有空格 cmd = Console.ReadLine();/语句结构,括号前有空格If (cmd.Length 0) /if 与括号之间有空格 Console.WriteLine(cmd.ToUpper();elseConsole.WriteLine(”(Empty)”);1.3 如何缩进所谓缩进(Indent ),是通过在每一行的代码左端空出一部分空间,更加
5、清晰地从外观上体现出程序的层次结构。int kmp_match(char t, char p, int flink, int n, int m)int i = 0, j = 0;While (i y)return x; else return y; C#使用的大括号位置风格public int Max(int x,int y)if (x y)return x; elsereturn y; 41.4.2 空的复合体的大括号结构public void UnusedMethod()/TODO:未实现的方法 以下情况能避免则避免:If (table.Rows.Count = 0)Else/以下略具体怎
6、么避免见 PPT 讲义 P50public class PhotoCollection : IEnumerable, IEnumerableinternal PhotoCollection()/空构造函数 为了避免将这个空函数当成是没有用的废弃函数,须特别加以说明/其他类成员已省略1.4.3 仅包含单个语句的结构体if (day 12) return false;if (month = 2)if (year % 4 = 0 if (month = 2)if (year % 4 = 0 elsereturn day = 28;else if(month = 4 | month= 6 | mont
7、h = 9 | month = 11)return day=30;elsereturn day = 31; 加上大括号的写法读起来就比较容易了1.5 保持项目文件的条理性1.5.2 代码文件的结构 确定排列顺序的大体方式:1. 总体上按照 public、internal、protected 、private 的访问控制顺序排列;2. 总体上按照构造函数、Finalize 函数、字段、属性、方法、事件、委托的类型顺序排列;3. 尽可能将相关性强的成员排列在一起。 1.5.3 使用#region 标记来隐藏细节#region 和#endregion 标记两个指令的作用是将其中的代码折叠或展开,并不
8、会对程序的编译与运行产生任何影响,仅仅在外观上起到隐藏细节的作用。第 2 章 养成良好的注释习惯2.1 何时需要注释2.1.1 解释代码的意图下面的注释是没有意义的:/ 定义一个整型变量下面的注释有意义:/ 交换 a、b 变量的值temp = a; a = b; b = temp; 2.1.2 对局部变量的说明/ 图像的尺寸,以英寸为单位 6SizeF imageSize = new SizeF(5, 3, 5); 对于 i、j 之类的循环变量,由于其作用在所有的程序中都是显然易见的,可以不必添加说明。反之,如果与普通的循环变量相比存在着一定的特殊性,那么就应该考虑使用其他的循环变量名称。2.
9、1.3 充当代码标题理解 PPT 讲义 P88-89 的关于判断语句的讨论。2.1.4 指出例外情况但凡使用了非常规的手段或技巧时,一定要加以注释,说明并不是大意疏忽,而是有意为之,不仅如此,还应当解释说明为什么要采用这种反常的做法。2.1.5 开发过程的提示任何一个程序的开发总是需要经历一个过程的,而代码也是由简单粗糙逐步走向完善健全的。这期间,对于并未完善的代码加上注释是很有必要的。2.2 注释的格式“/可以注释至行末, “/* */”可以注释任意位置的一段文字。不应该在调整精美的注释格式上花费过多的时间。2.2.1 单行注释 注意,/ 之后与注释文本之间应留一个空格,避免拥挤; 注释前视
10、情况留一空行例子见 PPT 讲义 P98 应避免使用:/ 居中的注释文本/=带有符号装饰的注释文本 = 不要使用“/* */” 来标记单行注释2.2.2 多行注释注意 * 号对齐(例子见 PPT 讲义 P101)第 3 章 一般命名规范3.1 选用合适的名称3.1.1 使用字符的限制 下列标识符的命名都是不合适的:private int 年龄;private int ge;private int ; 放弃使用下划线,以下不合适:private int_age;private string user_name;public string get user_name();73.1.2 使用含义明确
11、的英语不合适的:public string Func(int a);public class MyClass 而下面这些命名则较为清晰:public string GetNameById(int id);public class Employee 3.2 大小写规则3.2.1 PascaI 规则所谓 Pascal 大小写规则,是指将每个单词的首字母大写,其余字母小写总的来说,所有能够从外部进行访问的(包括 public、internal 和 protected)成员的标识符名称都应该使用 Pascal 大小写规则。3.2.2 Camel 规则Camel 大小写规则与 Pascal 大小写规则的
12、唯一区别在于整个标识符的第一个单词,它要求该单词采用全部小写方式,之后的单词则与 Pascal 规则完全相同,均为首字母大写一般地,表示私有字段和函数参数的标识符应遵循 Camel 大小写规则。3.3 考虑跨语言编程3.3.1 不要通过大小写区分标识符下面的代码中进行的变量定义是不合适的:private int num;private double Num;private long NUM;虽然 C#是大小写敏感的语言, num、Num、NUM 分别代表不同的标识符,但一旦被其他语言,比如 VB.NET 采用,则可能出问题。3.3.2 避免与其他语言的关键字重复应当尽量避免为命名空间、结构、类
13、、枚举、委托等类型使用与不同语言关键字相同(即使大小写不同)的名称。成员及枚举项的名称则可以不受此限制,因为通常它们总是通过类型名称或实例名称限定引用,不会产生歧义和麻烦。第 4 章 处理数据4.1 关于数据类型4.1.1 整数整型是我们最为熟知的数据类型,也是使用得最多的,如 int、Iong4.1.2 浮点数 关于浮点常量的书写:欧美国家的人习惯于省略那个整数部分的“0”,直接以小数点开头,如:.123456 ,由于失去了整数部分“0”的提示,这个开头的小数点很容易被人忽略。在阅读代码时应注意这一点。8 下列程序为什么会没有“OK!”的结果?double i = 0.0;while (i
14、10)i += 0.1;Console.WriteLine(i.ToString();if (i = 6.0)Console.WriteLine(“OK!“); 应如何改进才能看到“OK!”? 见 PPT 讲义 P2114.1.3 布尔类型必须将完整的表达式书写出来: if (x % 2 = 0) Console.WriteLine(“偶数”);极大地提高代码的可读性,并且代码的可移植性也加强了。 4.4 魔数(PPT 讲义 P266-271)“魔数(Magic Number)也称“幻数”,是指那些直接以字面数值方式出现在代码中的常量第一类魔数,其数值本身已经包含了全部意义,不再需要额外的补充
15、信息,只要代码的功能或逻辑不变,这个数值也永远不会改变。第二类魔数魔数与第一类魔数的基本特点正好相反,其数值本身并不足以表示这个值的含义。会举例说明第一、第二类魔数。第 5 章 分支结构5.1 使用 if 结构5.1.1 “=”与“=”的问题 在 C/C+中,程序员经常犯的一个错误是在 if 结构中将等号“=”错写成赋值号“=”。 养成将完整的表达式书写出来的习惯:if (x2 = 0)Console.WriteLine(“偶数”); 5.2 使用 switch 结构 该程序的结果总是 a 除以 b 的商,问题出在漏写了 break;switch (opr)9case 0:r = a + b;
16、case 1:r = a - b;case 2:r = a * b;case 3:r = a / b; 滥用 default 的情况:本应处理 4 种不同的情况,但它却只列出了 3 种,最后的除法直接用 default 代替了。 (PPT 讲义 P296)switch (opr)case Operator.Addition :r = a + b;break;case Operator.Subtraction :r = a - b;break;case Operator.Multiply :r = a * b;break;default:r = a / b;break;上述写法会带来如下三个问题
17、:1.default 的语义与它的实际角色不匹配,其他不知情者可能会错误地以为这里只有 3种可能的情况,default 只是处理出错值;2.当可能的情况数目发生改变时比如说我们想添加一个取幂运算,我们不得不去修改这个 default 子句,使其继续匹配 “最后一种情况”;3.这个代码无法区分最后一种情况与错误值之间的差别。 无论觉得考虑得多么完善,都请在 switch 结构的末尾添加一个 default 子句,进行恰当的意外处理。 如果提供默认值或者输出错误信息并不合适,就考虑抛出一个异常。这至少可以让问题暴露出来,用于检查软件中潜在的问题而不是让软件产生奇怪的反应。 (讲义 P300 )5.
18、3 选择 if 还是 switch? 相比 if 结构而言,switch 结构在使用上存在着诸多的限制,它唯一可以绝对优于 if 结10构的就是它的简洁。因此,在 if 与 switch 之间选择也并不困难,只要程序逻辑可以直接用 switch 表达出来,那么就应当使用 switch 结构。 5.4 关于判断顺序的设计 事实上,大多数情况下各个分支出现的频率有着显著的差别。例如关闭计算机时,如果每次先判断重新启动的话,必定会大大增加表达式计算比较的次数,毕竟大多数情况下会选择关机或者休眠,重新启动和注销都是较少的情况。 预防因条件短路而丢失操作:if(validateUserName() 该例
19、子中,如果用户名填写有误,即 validateUserName 函数返回 false 的话,程序根本不会去检查密码的合法性这意味着 ValiatePassword 函数根本没有被执行。 要慎用 goto 语句(见讲义 P316-317 的讨论)第 6 章 循环结构6.1 使用 for 还是 While 简单的 for 循环可以避免视觉跳跃(例子见讲义 P327)如果 for 的条件较为复杂,则会产生视觉跳跃(例子见讲义 P329)while 结构将复杂的循环控制放在循环体内进行,从视觉上来说,这避免了对循环语句的逻辑分割,之前的跳跃也不复存在。 (例子见讲义 P330) 通常来说,for 更适
20、用于简单的数值迭代,通过循环变量由初值到终止的变化控制整个循环的进程;相反,如果循环访问的对象是 I/O 流或数据库等复杂对象,则 while 结构更加合适。 6.3 提高循环效率 当代码中出现循环结构时,必须特别留意其效率。如果循环不会对 GetMaxVaIue 方法的返回值造成影响的话,那么每次循环都重新计算一次 data.GetMaxVaIue 函数就是白白地浪费资源:for (int i = 0; i data.GetMaxValue(); i+)/可以预先计算出它的值,保存在局部变量中。这样在循环体内就不必再计算了:int maxValue = data.GetMaxValue();
21、 for (int i = 0; i maxValue; i+)/第 7 章 如何使用函数11 重点搞清函数重载的概念:函数要提供多个重载,其主要目的是方便调用者,主要表现在:(讲义 P371-379)1. 支持多种数据类型2支持多种数据提供方式3. 为复杂的参数提供默认值,简化调用 函数的参数中不要使用保留项,这是因为在 C#已经支持函数重载的情况下,使用预留参数的方法就不合适了。新版本的函数仍然可以继续维持原来的名称,只需提供更多参数的重载版本就可以了。 重载函数的参数一致性主要体现在两个方面:(讲义 P401-412)1名称的一致性2. 顺序的一致性第 8 章 结构与类 值类型和引用类型
22、:值类型包括: 所有简单数值类型,如 byte、shoft、int、Iong、fIoat 、doubIe 等等; bool、char、DateTime 类型; 所有的结构; 枚举类型。引用类型包括: string 类型; 数组类型; 类类型; 委托类型 结构与类的命名:(P452-)它与客观世界中的实体相似,因此应当使用名词或名词性短语作为结构或类的名称。 如何真正面向对象(讲义 P483- )面向对象的实质在于一种编程思想,而绝不仅仅是类、对象、接口、继承、多态、重写等几个概念或工具。也不是说,使用面向对象的语法机制写出来的就一定是真正的面向对象程序。第 9 章 封装 垃圾回收器(GC,Ga
23、rbage Collector)机制:C#为了让程序员从复杂的内存管理操作中解放出来,使用了垃圾回收器机制,自动回收那些不再使用的对象所占用的内存。程序员不再需要亲自释放自己申请的内存,一切全由 GC 代劳。与 GC 的便利性一起产生的,则是对象销毁的不可控性。直到内存不足时,GC 才会去清理那些可以被释放的对象所占用的空间。所以我们应尽可能地主动释放资源,不要等着 GC 来回收。第 10 章 继承与多态 自上而下逐步细化所谓自上而下逐步细化,是指从最基础的类型开始,一层层地添加细节,派生出各个12子类,以及子类的子类等等。这种方式适用于类型结构并不清晰,或者原先并不存在内部差异的情况。 (讲
24、义 P648-653) 自下而上地进行逐步抽象。这在较为复杂的问题中,往往更为常见,因为我们总是擅长列举我们已经知道的东西,对于进一步的提炼和归纳,往往需要投入更多的思考。 (讲义 P654-662) 继承限制抽象类型即在声明时使用了 abstract 关键字的类型,它本身不能被实例化,除了静态成员可正常使用之外,只能用来派生子类型。之所以称之为抽象,顾名思义,它并不表示一个具体的概念,而是表示一类具体概念的共性。 (理解 P666-668 的例子) 关于接口接口用于描述可属于任何类或结构的一组行为定义,它包含一些公开的、抽象的、不可实例化的成员,包括属性、方法、事件或索引器。类不但可以继承自
25、另一个类,也可以继承自接口。 接口的继承与类的继承在语义上却有着本质的差别。类的继承体现了普遍性与特殊性之间的关系,派生类是基类的具体情况或特例,而基类则代表了派生类最本质的特性。接口的继承(通常我们更愿意称之为对接口的实现 )则体现的是对某种约定的遵从,或者是存在一定的共性,但这种共性并非它们本质上的共同点。例如:苹果是需要削皮的,而土豆也需要削皮,然而需要削皮只能说是它们的一个共同点,并非本质上的共性。因此,我们将“可以削皮” 视为一个接口,苹果和土豆这两个类型则都继承了这个接口。第 11 章1、如果我们将值类型强制转换为 System.Object 类型,就可以将值类型转换为引用。这个操
26、作即是“ 装箱(Boxing) ”例:int a = 5;object obj = (object) a;装箱操作看上去与普通的赋值没有什么差别,事实上却要进行很多额外的操作。它需要为原本位于栈中的变量 a 在堆中建立一个副本该副本不但包含原变量 a 的值,还包含它的数据类型说明。对于偶尔的单次操作来说,并不会产生性能问题,但装箱操作往往都是成批出现的,这也许将导致可观的性能损失。 2、泛型机制使得一套代码可以在为尽可能多的不同数据类型提供通用性的前提下,仍然保证强类型操作。相比于使用万能的 Object 类型方式,泛型机制不但提高了性能,也增加了安全性与可靠性。使用了泛型的代码不但性能有所提
27、高,逻辑也更加清晰。通常来说,只要有可能,就应当使用泛型,避免将 object 作为通用类型。13第 12 章1、事件处理函数事件其实就是一个委托的实例,而事件处理函数即是具体“被委托”的方法。为了清晰地体现出事件处理函数与事件委托之间的联系,我们应当像 C#开发环境自动生成的事件处理函数一样,为其使用标准的函数声明。当事件处理函数是对某个对象实例的事件的响应时,其名称应由对象实例名称与事件名称组合而成,中间用下划线连接。例如,用于响应 buttonl 的 Click 事件的函数名称即为 buttonl_Click。当事件处理函数是对当前类型自身的事件的响应时,应使用该类型自身名称与事件名称的
28、组合。例如,响应 MainWindow 窗体类型的 Closing 事件的函数名称应为MainWindow_Closing。2、代码的分配通常来说,事件处理函数中只应该包括简单的输入预处理(输入数据有效性检查、类型转换等)和对操作的调用,不应该涉及实际的实现代码,更不应涉及复杂的逻辑。即使某些操作仅在这一个事件中被使用,如果它在逻辑上具有一定的独立性,也应当考虑将其分离出来,写成单独的函数,在事件处理函数中进行调用。3、何时应当提供事件事件可以分为两类,一类是反映用户交互的情况,另一类则是反映类型的状态改变。第一类事件通常包括用户鼠标操作(如 MouseClick、MouseDown)和键盘操
29、作(如KeyPress)之类。这些事件可以参考 SystemWindowsForm。Control 类型,如果我们自己的类型是由 CustomControl 继承的话,这些事件都已经随基类型提供。但有一点要注意的是,如果我们的类库或控件还包含子控件(比如说列表框中的每个列表项、树型目录中的每个结点等等),那么也应当提供这些子控件的所有用户交互操作事件,以便对具体的子项进行细节控制。第二类事件用于反映类型的状态改变,有一个非常简单的规则:为每个属性的更改都提供事件通知,如果逻辑上允许,尽可能为每个属性提供两个事件,一个在属性即将更改前触发,一个在更改完成后触发。在一个结构良好的类型中,属性即描述了对象的状态,外界需要了解的所有状态都可以通过各个属性获取。如果任何一个属性的更改都可以得到事件的通知的话,我们就可以认为事件已经涵盖了所有状态改变的情况。