代码风格最早出现的,也较为传统的是K&R风格。所谓K&R即指《The C Programming Language》一书的作者Kernighan和Ritchie二人,这是世界上第一本介绍C语言的书,而K&R风格即指他们在该书中书写代码所使用的风格。K&R风格在处理大括号时,使用了一种较为紧凑的格式,将左括号留在前一行的末尾,并尽可能地压缩·..
目录
1
基本的代码风格2
代码风格的必要性3
换行的讲究代码示例1-1:寻找最佳的断行位置代码示例1-2:代码示例1-3:代码示例1-4:代码示例1-5:每行只写一条语句分行定义变量4
避免代码过于拥挤使用空行分隔代码块使用空格降低代码密度5
符号两侧添加空格的基本规则:单目运算符在双目、三目运算符括号不要使用连续的两个或多个空格6
如何缩进嵌套或包含关系引起的缩进因换行而产生的缩进使用空格还是Tab键7
大括号大括号的位置空的大括号结构仅包含单个语句的结构体1基本的代码风格
编辑假设我们写的是文章而不是程序,那么你一定觉得诸如文章应该分为若干个自然段、每段开头空两格之类的规则是理所当然的。如果段落的开头不空两格,或者干脆把整个文章写成单独的一段,仔细想来似乎也不会影响文章实质内容的表达。既然如此,我们为什么还要在形式上下功夫呢?设想一下,如果你手中的这本书既无章节也无目录,正文中的不同内容都使用同样的字体字号印刷,几百页纸从头至尾洋洋洒洒如念经般地“一气呵成”,你还有耐心看下去吗?
这是一个人人都能理解的道理,可是当文章变成程序的时候,就不是每个人都能想得通的了。不仅仅是初学者,甚至一些熟练的开发人员,也会写出凌乱不堪的代码。许多人一定有过这样的经历:一年半载之后,自己原来写的程序就完全看不懂了。如果这段程序只是为了交作业,或者临时一用,那还可以不去追究,但如果这是一个
商业软件,现在需要根据客户的要求进行修改的话,工作量可就大了——你不得不先花时间把你原来的思路看懂。
肯定会有人反驳:代码是给机器运行的,又不是给人看的,写那么好看有什么用?
他的话只对了前半句:代码确实是给机器运行的,可是机器总共才需要看它几分钟?你花一个月编写的程序,机器顶多两三分钟就编译好了——在这两三分钟之前,这代码不都是你在看吗?开发软件编写代码不是一朝一夕的事情,更多的情况下,一个软件的开发要经历很长的时间,并且常常由多人合作完成。一个庞大的软件项目,可能会动用上千名
程序员工作数年!如果把代码写得连自己都看不明白,怎么与别人交流?同一个开发团队内,一定要保持良好且一致的代码风格,才能最大化地提高开发效率。
2代码风格的必要性
编辑有的初学者会问:我现在只是一个人写程序,并不需要和其他人合作,这些条条框框还有什么必要吗?
要知道,团队协作只是一个方面。我经常遇到这类情况,一些初学者拿着他的程序来说:“这个怎么不能编译?”我帮他把代码整理了半天,发现有一个地方丢了半个大括号。如果他写程序的时候能够稍加注意一些的话,相信此类错误完全可以避免。保持良好的编程习惯,能够避免的错误还远不止这些。
如果说程序代码中对算法的清晰表述是通过长期训练而获得的,那么本章要介绍的这些方法则无需伤神,你不必对代码做任何实质性的改动,只需要添加一些空行与空格,就可以使其可读性大大提高——这些规则就像写文章应该分段一样简单,只要愿意遵守,那么别人在第一眼看你的代码时,必能感觉到你那良好的编程修养,即所谓“见字如见人”。
3换行的讲究
编辑虽然你完全可以在C# 里将所有的代码都连在一行里书写,但想必没有人愿意这么做,谁也不会自己折磨自己的眼睛,何况大多数鼠标对于上下
翻页的支持都比左右翻滚好得多。我相信,这也是大多数人接受将每条语句分行书写的原因,很少有人会怀疑这一点的合理性。例如下面这行代码,虽然结构很简单,但是它实在太长了,所以被分成了两行:
代码示例1-1:
由于代码过长而进行断行
bitmap = new Bitmap(size.Width, size.Height,
System.Drawing.Imaging.PixelFormat.Format32bppArgb);
这一点我相信大家都能理解并愿意遵循,然而问题的焦点并不在于要不要换行,而在于在什么位置换行。
寻找最佳的断行位置
写程序不能像写文章那样,什么时候顶到了边界就换,而必须按照其语法规则,在可以换行的位置断开。例如,对于包含一个超长
表达式的语句来说,我们可以在某两个表达式项之间将其断开,如下 所示:
代码示例1-2:
通过断行使代码更加清晰
if (f == ImageFormat.Jpeg.Guid ||
f == ImageFormat.Tiff.Guid ||
f == ImageFormat.Png.Guid ||
f == ImageFormat.Exif.Guid)
{
supportsPropertyItems = true;
}
else
{
supportsPropertyItems = false;
}
原本一个很长的
条件表达式,通过在“||”运算符处换行,显得更加的清晰。有一点需要我们注意的是,当我们进行折行时,要将折行位置处的
分隔符(如前一例中的逗号,这一例中的“||”运算符等)留在上一行的行末,给人以“此行并未结束”的直观印象。这就好像在英文书写中,如果你需要将一个单词拆开,就需要在前一行末尾加上
连字符,以表示那个单词并没有结束。
可以看出,换行在防止代码超出屏幕边界的同时,还影响着代码的表达。因此如何选择合适的换行位置也是很有讲究的。有的时候,我们并不一定非要在临近右边界的时候才去换行,如果存在更为合理的分法,就应当采用,例如下面的情况:
double containerAspectRatio = (double)container.ClientWidth /
container.ClientHeight;
按理说这样的断行并没有什么问题,它在
表达式的两项之间断开,并将运算符留在了上一行的行末。但是,我相信如果换一种断行方式的话,能够更加清楚地表达出原来的逻辑:
代码示例1-3:
寻找最佳的断行位置
double containerAspectRatio =
(double)container.ClientWidth / container.ClientHeight;
如此一来,这个除法
算术表达式就显得较为完整,相比前一种写法而言更能体现其内在的逻辑关系。通常我们会选择整个
表达式中最高的关系层次进行断行,例如上述代码中的“赋值号”和“除号”都是可以考虑的断行点,但相比较而言,除号连接的这个
算术表达式只是整个
赋值表达式的右半部分,如果在除号处断行,那么不但整个表达式会被截断,连局部的这个除法表达式也会被截断;反之,我们选择在赋值号处换行,可以保持除法表达式的完整,最大限度地减少换行对语句整体结构的破坏。
同样的道理,为了将逻辑体现得更为清晰,我们甚至可以将
函数调用中的每一个参数都分行书写,如同下面这样:
代码示例1-4:
将函数调用中的每一个参数都分行书写
Rectangle imageBounds = new Rectangle(
itemBounds.X + padding,
itemBounds.Y + padding,
itemBounds.Width - padding * 2,
itemBounds.Height - padding * 2
);
当参数数量较多,参数较长或者包含
表达式的时候,这种排版比起单独写成一行更为直观醒目。
对于LINQ查询表达式来说,将每个子句单独写成一行也是好的习惯。因为这同时符合了T-SQL语言的文化传统。例如:
代码示例1-5:
将LINQ查询表达式中的每个子句单独写成一行
IEnumerable<int> highScoresQuery =
from score in scores
where score > 80
orderby score descending
select score;
每行只写一条语句
如果说换行是为了防止屏幕左右滚动的话,那么当这个情况不存在的时候,一些人就开始打算充分利用屏幕空间了:
private static void Swap(object a, object b)
{
object temp;
temp = a; a = b; b = temp;
}
看起来好像确实没有占据多少屏幕空间,这只是把三条很短的语句凑在一行了而已——关键的理由是:它不会引起屏幕的左右滚动。但是当人们已经习惯于一行一条语句的时候,很可能就会忽视这里有三条语句这个事实(不要指望每次遇到的都像这个例子一样地简单)。更为重要的一点是,
编译器总是按行来进行设计的,将多条语句写在一行会引起不必要的麻烦,例如:你将永远无法把
断点设置在后面的语句上(如图1-1):
图1-1:一行代码包含多条语句时的
断点设置
有的读者会觉得,如果代码复杂,当然应该分开书写,没有必要去节省那点屏幕,但是如果像这个例子中这么简单,写在一起也不会带来什么麻烦。单纯地看来,他的话不无道理,可是,对于一个开发团队,或者将要进入开发团队的人来说,最重要的是“统一”。如果我们允许将多条语句合并到同一行代码内,那么怎样的代码才算“简单”到可以合并的程度?是宽度小于50个
字符的可以合并,还是宽度小于51个字符的可以合并?当一条规定无法被准确地定义的时候,它也就无法执行,从而必将在整个开发团队中产生不一致性,最终导致更多的混乱。
分行定义变量
我们再来看一种情况,这类代码出现的几率更为频繁,它是将相同
数据类型的几个变量声明放在了同一条语句中:
int num, factor, index, length;
如果我说我反对这种写法,一定会有读者大叫起来:这明明是单独的一条语句,何况C# 允许我们在一条语句内声明多个
变量,如此一来还可以少写几个“int”,为什么不行?
这种写法,显而易见会给注释带来很大的麻烦。把它们都写在一起以后,我怎么给每个
变量添加注释呢?如果是分开书写的,那么我可以很容易地为每一个变量添加单独的注释,就像这样:
代码示例1-6:将每个
变量分行定义将有助于单独注释
// 要计算的数值
int num;
// 表示影响因子
int factor;
// 元素所在的索引号
int index;
// 数据列表的总长
int length;
如果觉得这种写法较为繁琐,一定要节约那几个“int”,以强调它们的
数据类型相同的话,也可以采取下面的写法:
代码示例1-7:
变量分行定义的折衷方案
int num, // 要计算的数值
factor, // 表示影响因子
index, // 元素所在的索引号
length; // 数据列表的总长
这种方式只使用了一条声明语句,但是每个
变量都书写在单独的行上,便于有针对性的注释。
4避免代码过于拥挤
编辑想想人们为什么喜欢为文章添加各级标题以及其他复杂的格式,是因为美观吗?也许是的,但我相信这些格式可以更容易地让人们理清思路。可是在程序中,我们无法使用这些手段,所有的代码都是纯文本的,即使Visual Studio的代码高亮功能可以为代码的不同部分标上不同的颜色,但这并不能真正影响到代码本身。因此,光是换行还是不够的,我们还需要更多的手段。
使用空行分隔代码块
适当地添加空行则是一个非常有效的代码整理方式——有点像文章中的分段,一段意思也许需要若干个句子才能表达清楚,在表达下一段意思之前,应当另起一段。
首先,每个C# 代码文件是从命名空间引用开始的,一组引用结束之后,则是命名空间的声明及类型的声明。很显然地,在命名空间引用与命名空间声明之间,应该留有一个空行以示区隔:
代码示例1-8:在命名空间引用之后添加空行
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Text;
using Avilla.Metadata;
using Avilla.Searching;
// 这里用空行隔开
namespace Avilla
{
// 下面的内容省略
一个空行,意味着不同的功能块的分隔,如果读者稍加留心,就会发现Visual Studio自动生成的代码,总是在类型的各个成员之间留有一个空行。我们在书写代码的时候,也可以模仿这一格式:
代码示例1-9:在类型的各个成员之间添加空行
/// <summary>
/// 表示一条搜索条件的抽象基类
/// </summary>
public abstract class SearchCondition
{
/// <summary>
/// 初始化一个 <see cref="SearchCondition"/> 类型的实例,并指明是否区分大小写
/// </summary>
/// <param name="caseSensitive">是否区分大小写</param>
protected SearchCondition(bool caseSensitive)
{
this.caseSensitive = caseSensitive;
}
// 这里用空行隔开
protected bool caseSensitive = false;
/// <summary>
/// 获取或设置一个 <see cref="Boolean"/> 类型的值,以指示是否区分大小写
/// </summary>
public bool CaseSensitive
{
get { return caseSensitive; }
set { caseSensitive = value; }
}
// 这里用空行隔开
/// <summary>
/// 获取表示此搜索条件的 SQL 筛选
条件表达式/// </summary>
/// <returns>
/// 返回一个字符串形式的
条件表达式,可直接用于 SQL 语言中的 WHERE 子句
/// </returns>
public abstract string GetFilterExpression();
}
}
这样排版无疑会使得每个成员的代码段更富独立性,绝大多数的
编译器,在自动生成代码时都会遵照此方式排版。您可能会发现,上例中的caseSensitive字段与CaseSensitive属性之间并未留有空行,这是为了强调字段与其对应用于公开访问的属性之间的联系,关于类似情况,我们将在后面的章节详细讨论。
然而,一个空行意味着的不仅仅是功能模块的界限,它更是对代码逻辑块的划分。我们无法期望每个操作都只通过一行代码一条语句来完成,大多数情况下,它们都需要许多行代码来执行一个完整的操作。例如,你想查询数据库,那么你需要先生成SQL代码,建立命令,然后执行这个命令并填充至数据集。这中间大约需要使用四五行代码,而这四五行代码便组成了一个相对紧密的逻辑块,它与其后面的其他逻辑块即可以通过一个空行来进行分隔。请看下面的一个例子:
代码示例1-10:用空行分隔逻辑块
public static string[] GetPhotoIds(string filterExpression, string sort, bool caseSensitive)
{
// 第一个逻辑段代码根据处理后的参数取得数据行
xml.Photos.CaseSensitive = caseSensitive;
DataRow[] rows =
xml.Photos.Select(filterExpression, sort ?? string.Empty);
// 遍历数据行,取出需要的元素
string[] ids = new string[rows.Length];
for (int i = 0; i < rows.Length; i++)
{
ids = (string)rows["Id"];
}
// 返回结果
return ids;
}
这个函数的目的是根据指定的筛选条件和排序规则返回照片的标识号(Photo IDs),函数内部自然形成了三个逻辑段:先是根据要求取得
原始数据,然后从原始数据中提取我们需要的部分,最后将结果返回。用空行将这三个逻辑区分隔开来将会更加有利于我们理解其思路。关于注释的合理使用,我们会在后面的章节中再专门介绍。
既然空行可以起到分隔代码,提高清晰度的作用,那么有的朋友也许会为了强调这种分隔效果,多加几个空行。可事实的效果是,连续的多个空行,在并未提高多少清晰度的同时,浪费了屏幕的空间,而且会让人觉得前后两个
代码段并不相关——事实上它们应该是相继执行的。空行的意义和文章的段落一样,仅在于表示一个停顿,而并非结束。
使用空格降低代码密度
Basic、Pascal与C这三种早期高级
程序设计语言的语法,至今仍在发挥着其重要的作用。Visual Basic仍然保留着Basic的句法简单、多用完整英文单词、贴
近自然语序的习惯(如And、Not、Inherits、Implements、Handles等等关键字);而Delphi更是延续着Pascal语言那标志性的BEGIN-END作风。C语言由于在
操作系统开发上取得了成功,使得它在软件开发历史上占据了绝对的优势,相比而言,它的语法更加具有影响力,广泛被C++、Java、C#,乃至用于编写网页
的ECMAScript/JavaScript和Flash的
脚本语言ActionScript所吸纳,因此也变化丰富。但是它那种善用符号的古老特色一直被保留了下来,有理由相信,C语
言是使用符号最多的语言。当其他语法体系都采用AND、OR等关键字作为
运算符时,C语言却使用了“&&”、“||”这样的符号,虽然在语法上并没有增加任
何复杂性,但各种奇形怪状难以记忆的符号还是会令初学者望而却步。让我们来比较一下下面的几行代码:
BASIC: If a>b And c<>d Or Not e>f Then ...
PASCAL: If (a>b) And (c<>d) Or (Not (e>f)) Then ...
C: if(a>b&&c!=d||!(e>f)) ...
这三行的意义是完全相同的,但明显可以让人感觉到清晰程度的差异,Basic和Pascal的代码看上去很容易明白,而C语言的代码却像蚂蚁一般缩成一团。
重要的原因在于:C语言的
运算符几乎都只由“符号”构成,与
变量名之间不需要用空格充当
分隔符。这样一来,由于缺少空格的稀释,C语言的代码就像被浓缩过似
的——现如今它除了影响我们阅读以外,没有什么好处。因此我们有必要人为地添加一点空格,帮它降低代码的“密度”。这里,我总结了一些关于如何在运算
5符号两侧添加空格的基本规则:
编辑单目运算符
1. 单目运算符(Unary Operators)与它的
操作数之间应紧密相接,不需要空格。例如:
代码示例1-11:
单目运算符的空格规则示例
y = ++x; // ++ 在这里是前缀单目运算,它与x之间无空格
在双目、三目运算符
2. 在双目、
三目运算符(Binary/Ternary Operators)的左右两侧分别添加空格。例如:
代码示例1-12:双目、
三目运算符的空格规则示例
int a = 3 + 5; // 在
双目运算符左右添加空格
int b = a * 6 + 7;
int c = a & b;
int d = b++ * c--; // 虽然有
单目运算符,但
双目运算符两侧仍应添加空格
int e = a > 0 ? 1 : 0; // 在三目运算符左右添加空格
括号
3. 括号(包括小括号、中括号与大括号)的内侧应该紧靠
操作数或其他运算符,不需要添加额外的空格。例如:
代码示例1-13:括号的空格规则示例
int f = (a + b) * c; // 括号内侧紧靠
操作数,因其他运算符添加的空格留在外侧
int g[MAX] = {1, 2, 3}; // 中括号与
表达式中的大括号也同样处理
不要使用连续的两个或多个空格
4. 不要使用连续的两个或多个空格。
其实,如果理解了这些规则,在实际书写的时候很容易遵循。对于任何一个
表达式,我们先把
单目运算符和括号去掉,然后在双目、三目运算符的左右两侧分别
添加一个空格,再将
单目运算符和括号填回去,放在靠近自己
操作数的一边即可。
关于
函数调用时,要不要在函数名和其后的括号之间添加空格的问题已经讨论了很久。其实这个是一个无伤大雅的事情,无论使用何种方式,都不会对代码
的可读性产生多少实质性的影响,纯粹是各人喜好罢了。不过在这里,我建议采用Visual Studio中的默认规则:在
函数调用时不添加空格,而在一些类似的带括号的语法结构中添加空格。请看下面这段代码:
代码示例1-14:
函数调用时的空格规则示例
string cmd = string.Empty;
// 函数形式的调用,括号前没有空格
cmd = Console.ReadLine();
// 语句结构,括号前有空格
if (cmd.Length > 0)
{
Console.WriteLine(cmd.ToUpper());
}
else
{
Console.WriteLine("(Empty)");
}
这段代码中的ReadLine、WriteLine都是
函数调用,因此与其后面的括号紧密相连,不需要添加空格。而if结构虽然同样带有类似的括号结构,但是它属于C# 的内部语法,为了以示区别,在if与括号之间添加了一个空格。除if外,switch、for、while等都应做同样的处理。
6如何缩进
编辑在有关代码风格的问题中,最为显眼的可以说就是代码的缩进(Indent)了。所谓缩进,是通过在每一行的代码左端空出一部分长度,更加清晰地从外观上体现出程序的层次结构。为了更好地描述这一点,先请读者欣赏下列这段代码:
int kmp_match(char[] t, char[] p, int[] flink, int n, int m)
{
int i = 0, j = 0;
while (i < n)
{
while (j != -1 && p[j] != t)
{
j = flink[j];
}
if (j == m - 1)
{
return i - m + 1;
}
i++;
j++;
}
return -1;
}
我想,就算让你检查一下它里面有没有大括号配对错误恐怕都很困难,更不用说这段代码有什么功能 了——你能一眼看清楚每个while循环的内容是什么吗?让我们换个方式,看看另一段程序:
代码示例1-15:正确缩进的例子
int kmp_match(char[] t, char[] p, int[] flink, int n, int m)
{
int i = 0, j = 0;
while (i < n)
{
while (j != -1 && p[j] != t)
{
j = flink[j];
}
if (j == m - 1)
{
return i - m + 1;
}
i++;
j++;
}
return -1;
}
两段程序,除了
缩进的区别以外,一字不差。孰是孰非,相信大家都能看得出来,
缩进的必要性不难理解。接下来的问题就是:应该如何
缩进。
嵌套或包含关系引起的缩进
当遇到有关命名空间、类、结构、函数、以及枚举等等复杂程序结构的定义的时候,我们通常需要将它的内容
缩进一层。在C# 语言中,大括号是一个非常明显的标志,凡是遇到大括号,都应该直接联想到
缩进。请看下面的示例:
代码示例1-16:包含关系下的
缩进namespace MyNamespace
{
// 命名空间内的内容应缩进
public class MyClass
{
// 类的成员应缩进
public string MyMethod()
{
// 方法函数的
函数体应缩进
return "Hello!";
}
private MyEnum myProperty = MyEnum.Alpha;
public MyEnum MyProperty
{
// 属性的内容应缩进
get
{
// 属性的get部分
函数体应
缩进return myProperty;
}
set
{
// 属性的set部分
函数体应
缩进myProperty = value;
}
}
}
public enum MyEnum
{
// 枚举类型内容应缩进
Alpha,
Beta,
Gamma
}
}
分支结构(包括if…else结构、switch结构等)和
循环结构(包括for结构、while/do…while结构等)都是存在嵌套关系的,因此从条理清晰的角度来说,它同样应该进行缩进书写:
代码示例1-17:嵌套关系下的缩进
// if...else结构
if (a > b)
{
// if 子句的
结构体内容应缩进
max = a;
min = b;
}
else
{
// else 子句的
结构体内容应
缩进max = b;
min = a;
}
// switch结构
switch (n)
{
// switch结构的内容应缩进
case 0:
// case 子句内容也应缩进
// ...
break;
case 1:
// ...
break;
default:
// ...
break;
}
// for结构
for (int i = 0; i < 100; i++)
{
// for 的循环体应缩进
s += data;
t *= data;
}
// while结构
i = 0;
while (data != 0)
{
// while 的循环体应缩进
s += data;
t *= data;
i++;
}
缩进时,应将内部结构中的所有语句都统一向右退一格,大括号则与原来的语句保持在同一个垂直位置上。每层
缩进的长度应该一致,通常为一个
制表符宽或四个空格。
还有一些细节的地方也与换行相关,例如if、switch、for这类具有嵌套结构的语句,在书写的时候都应避免将
结构体与语句本身写在同一行上,关于嵌套结构的书写方法,我们会在后面的章节中详细讨论。
因换行而产生的缩进
我们在前面提到过,当一条语句太长而超出一定的宽度时,应该折行书写。此时,从第二行起到该语句结束之间的各行应该缩进一层,至下一条语句时再恢复原来的缩进位置。
代码示例1-18:因换行而产生的缩进
int myVar = 0;
// 这是一条很长的语句,因而出现了换行,从第二行起都
缩进了一格:
myVar = myVar1 + myVar2 + myVar3 - myVar4 - myVar5 * myVar6 * myVar7 /
myVar8 / myVar9 + myVar10 + myVar11 - myVar12 - myVar13 * myVar14 *
myVar15 / myVar16;
// 后面的语句恢复正常的
缩进位置
Console.Write(myVar);
如果该语句是进行
函数调用,由于参数太多而造成的换行,那么在
缩进规则上有一些微小的差别:
代码示例1-19:
函数调用时分行书写参数而引起的缩进
Rectangle imageBounds = new Rectangle(
itemBounds.X + padding,
itemBounds.Y + padding,
itemBounds.Width - padding * 2,
itemBounds.Width - padding * 2
);
注意最后一行的括号与分号并没有缩进,因为这种结构其实是对类似if的大括号嵌套结构的模拟。
使用空格还是Tab键
如何缩进一向是一个有争议的问题。使用Tab及Shift + Tab键缩进在操作上非常方便,而使用空格可以保证代码在任何
编辑器下都能正确显示缩进格式。现在,我们依靠Visual Studio
开发环境则可以轻松解决这个矛盾:在“选项”对话框中对C#
编辑器的代码
缩进方式进行设置(如图1-2),选择“插入空格”模式。
图1-2:Visual Studio中关于代码
缩进的设置
这样一来,我们就可以在书写代码时使用Tab键和Shift + Tab键来调整
缩进,而Visual Studio会将其转换为空格保存至代码文件中。
7大括号
编辑从外观上来看,类C语言的最大标志就是它无处不在的大括号了。在C# 中,大括号仍然扮演着几种不同的角色:表示层次关系(如定义命名空间、类时使用的大括号)、表示
复合语句(如if、for中的大括号)、表示
数组元素。本节将讨论有关大括号书写的几个基本问题。
大括号的位置
代码风格中,如何摆放大括号一直是人们热衷的话题。其实,我更愿意用开放的态度去看待它,具体使用何种方式并不重要,重要的是,要保持方式风格的统一,不能在同一个项目中出现不同的风格。
代码示例1-20:K&R大括号位置风格
public int Max(int x, int y)
{
if (x > y) {
return x;
} else {
return y;
}
}
据说,微软公司内部使用的就是这种K&R风格,然而它在对外公开的文档中,却使用更为大家所熟知的一种风格,它将大括号单独写成一行。本书所有示例代码采用的都是这样一种格式:
代码示例1-21:C# 默认使用的大括号位置风格
public int Max(int x, int y)
{
if (x > y)
{
return x;
}
else
{
return y;
}
}
虽然生硬地规定应该使用哪一种是不提倡的,而且也没有必要,但是我们仍然建议读者尽量选择上述两种风格中的一种,并在自己的程序中保持风格的统一。
空的大括号结构
所谓复合体,即指用于充当某个语法结构成份的,被大括号括起来的多条语句。C# 的很多语法结构中都可以见到复合体的使用,如命名空间、类、结构、接口、枚举、方法、属性等的定义,以及if、switch、for、while、do…while、try…catch…finally结构等等。对于应该如何处理大括号的摆放以及内容的缩进排版等问题,我们都已经详细讨论过了,现在要考虑的是复合体自身的一种特殊情况:空的复合体。
接下来的问题是:为什么会出现空的复合体?有的时候来自于废弃的空函数。开发过程中,很可能只写了某个类或者函数的空声明,打算待日后再细化。然而由于设计上的变动,这个函数不再使用,而躲在代码某个角落的这个空函数又未能被及时删除,结果一直保留到产品发布。在这种情况下,建议在空复合体中添加“TODO”的注释:
代码示例1-22:为未实现的空
函数体中添加TODO注释
public void UnusedMethod()
{
// TODO: 未实现的方法
}
因开发时的疏忽造成的空复合体还不仅仅是上面这一种情况,我曾见到过有人将if结构中的if段留空,却在else里写上一堆代码,类似下面这样:
if (table.Rows.Count == 0)
{
}
else
{
foreach (DataRow row in table.Rows)
{
Console.WriteLine(row["Name"]);
}
}
也许他本来在if中是有代码的,后来程序不断地修改,结果在if段中无事可做了。这种情况也很容易避免,只需很简单地将if中的
条件表达式反转过来即可:
代码示例1-23:反转if
条件表达式以避免空的if子句
if (table.Rows.Count > 0)
{
foreach (DataRow row in table.Rows)
{
Console.WriteLine(row["Name"]);
}
}
如果不是开发时的疏忽,那么空复合体的出现就是有意而为之了。在
循环结构中,它出现的频率相对较高。早期,人们会采用空循环的方法来达到“延时”的效果,那个时候常常会看到类似这样的代码:
printf("Waiting...\n");
for (i = 0; i < 10000; i++)
{
}
printf("Done.");
或者干脆连大括号结构都没有,直接就是
printf("Waiting...\n");
for (i = 0; i < 10000; i++);
printf("Done.");
且不说现如今这种“延时”方式完全不可取,就从代码的外观来看,它也极易让人将for下面的那一行printf输出代码当成是循环的内容。
由于C# 语言继承了C语言语法灵活的特点,因此
循环语句本身就能承担很多复杂的工作,以至于根本就不需要循环体。下面的代码反映了一种典型的状况:
int sum = 0;
for (int i = 0; i < table.Rows.Count; sum += (int)table[i++]["Amout"])
{
}
这个for语句本身就把累加工作给做完了,使得循环体内已经无事可做。虽说从语法角度来说没有多少问题,但是它的可读性并不好,我们仍然希望for仅仅是做一些“循环”本身的事情,将具体的数据和逻辑操作放在循环体内,就像下面这样:
代码示例1-24:让for
循环语句本身仅控制循环,不要涉及具体事务
int sum = 0;
for (int i = 0; i < table.Rows.Count; i++)
{
sum += (int)table["Amout"];
}
这看起来更像一个for循环所体现出来的含义。
这样总结下来,似乎空复合体完全不应该出现在代码中,然而有一种情况下它的确可以而且应当存在,这就是
构造函数。
构造函数与其他的函数不同,即使没有任何一行代码内容,它本身的存在与否也有着极其重要的意义——直接决定这个类型是否能够被实例化。因此,当我们不希望某个类被外部实例化的时候,哪怕我们并不需要自定义
构造函数,我们仍需要为该类型添加一个非公开的空构造函数。为了避免日后将这个空函数当成是没有用的废弃函数,我们有必要特别加以说明:
代码示例1-25:通过注释强调空
构造函数public class PhotoCollection : IEnumerable, IEnumerable<Photo>
{
internal PhotoCollection()
{
// 空
构造函数}
// 其他类成员已省略
}
其他情况下,如果确实有必要使用空的复合体,也应当仿照上例书写,添加相应的注释说明,以防止产生误解。绝不能省略大括号结构,仅以一个分号代替。
仅包含单个语句的结构体
我们再来讨论另一种特殊的复合体。按照C# 的语法,如果if、while、for等等的结构体仅为一条语句,那么大括号是可以不写的——其实复合体就是将多条语句合并起来充当单条语句来用的。甚至我们可以将结构体与if、while、for写成一行,例如:
if (a > b) x++;
else y++;
或者是:
for (int i = 0; i < 10; i++)
dest = source;
虽然这在语法上没有什么问题,但当代码数量增加,上下文的代码变得复杂时,我们有可能会对if、while、for的
结构体究竟包含哪些语句产生误解。因此,我们建议,即使if、while、for结构的内容只有一条语句,也应该像处理多条语句那样,写在大括号内。因此,刚才那两段代码就应该写成下面