一些编写更好 C#/.NET 代码的实践






4.83/5 (454投票s)
今天我将分享一些我在职业生涯中学到的良好实践,这些实践虽然属于较低的层面,但对所有级别的开发者都非常重要。
引言
开发者们总是喜欢争论编码标准。但遵循编码标准对于在项目或代码中实现一致性至关重要。大家应该达成共识,约定俗成具有巨大的影响力。我将分享一些我在职业生涯中学到的良好实践,这些实践虽然属于较低的层面,但对所有级别的开发者都非常重要。
快速测试
让我们以 FizzBuzz 示例进行演示。FizzBuzz 测试要求编写一个程序,该程序会遍历 1 到 100 的数字。对于 3 的倍数,程序应输出“Fizz”;对于 5 的倍数,应输出“Buzz”。如果以上两个条件都满足,则应输出“FizzBuzz”。如果以上两个条件都不满足,则应只输出数字。
示例 1
public void Test()
{
for (int i = 1; i < 101; i++)
{
if (i % 3 == 0 && i % 5 == 0)
{
Console.WriteLine("FizzBuzz");
}
else if (i % 3 == 0)
{
Console.WriteLine("Fizz");
}
else if (i % 5 == 0)
{
Console.WriteLine("Buzz");
}
else
{
Console.WriteLine(i);
}
}
}
您怎么看?我们需要改进代码吗?
示例 2
public void Check()
{
for (int i = 1; i <= 100; i++)
{
string output = "";
if (i % 3 == 0) { output = "Fizz"; }
if (i % 5 == 0) { output = output + "Buzz"; }
if (output == "") { output = i.ToString(); }
Console.WriteLine(output);
}
}
您现在怎么看?我们是否需要让代码变得更好?
好的,让我来帮助您改进它。命名是我们作为软件开发人员最难做的工作之一。因为我们在命名事物上花费大量时间,需要命名的事物非常多,例如属性、方法、类、文件、项目等。我们应该在命名事物上投入一些精力,因为名称可以承载意义。为代码添加意义,这就是可读性的全部意义所在。
public void DoFizzBuzz()
{
for (int number = 1; number <= 100; number++)
{
var output = GetFizzBuzzOutput(number);
Console.WriteLine(output);
}
}
private static string GetFizzBuzzOutput(int number)
{
string output = string.Empty;
if (number%3 == 0)
{
output = "Fizz";
}
if (number%5 == 0)
{
output += "Buzz";
}
if(string.IsNullOrEmpty(output))
{
output = number.ToString();
}
return output;
}
您现在怎么看?这比之前的示例更好吗?它更具可读性吗?
什么是更好的代码?
首先为人们编写代码,其次为计算机编写代码。可读的代码在编写时花费的时间并不比令人费解的代码长,至少从长远来看是这样。如果可以轻松地阅读您编写的代码,那么就更容易确保代码正常工作。这应该是编写可读代码的充分理由。但代码在审查时也会被阅读。在您或其他人修复错误时,代码会被阅读。在代码被修改时,代码会被阅读。在有人尝试在类似项目或不同项目但具有相似功能或部分功能的项目中使用您的代码时,代码也会被阅读。
“如果你只为自己编写代码怎么办?你为什么要让它变得可读?”
好的,编写可读代码的主要原因是,一两周后您将开始处理另一个项目。当任何其他人类需要修复该项目中的错误时,会发生什么?我敢保证,您也会迷失在自己编写的糟糕的代码中。
从我的角度来看,更好的代码应该具备以下特征:
- 易于编写、修改和扩展的代码
- 代码清晰且能传达意义
- 有价值且注重质量的代码
因此,在编写代码时要考虑到人类读者,同时在必要程度上满足机器的需求。
如何提高可读性?
首先,您必须阅读他人的代码,并弄清楚其中哪些是好的,哪些是坏的。什么让您容易理解,什么让您感到更复杂。然后将这些应用到您自己的代码中。最后,您需要一些时间和经验,并需要一些练习来提高代码的可读性。在任何软件公司中实施标准都非常困难但显而易见,可以通过培训、同行代码评审、引入自动化代码审查工具等方法来实现。最流行的工具如下:
- FxCop 是一个对 .NET 代码进行静态代码分析的工具。它提供数百条规则,执行各种类型的分析。
- StyleCop 是一个开源项目,它分析 C# 源代码以强制执行一组样式和一致性规则。它可以从 Visual Studio 内部运行,也可以集成到 MSBuild 项目中。StyleCop 也被集成到许多第三方开发工具中。
- JetBrains ReSharper 是一款著名的生产力工具,它让 Microsoft Visual Studio 成为一个更好的 IDE。全球成千上万的 .NET 开发者都在惊叹,没有 ReSharper 的代码检查、自动化代码重构、闪电般的速度导航和编码辅助,他们是如何生活的。
什么是约定?
根据 维基百科,“编码约定是一套特定编程语言的指南,它建议在该语言编写的程序每个方面所应遵循的编程风格、实践和方法。这些约定通常涵盖文件组织、缩进、注释、声明、语句、空格、命名约定、编程实践、编程原则、编程经验法则、架构最佳实践等。这些是关于软件结构质量的指南。强烈建议软件程序员遵循这些指南,以帮助提高源代码的可读性并简化软件维护。编码约定仅适用于软件项目的维护者和同行评审者。约定可以被正式化为整个团队或公司遵循的一套文档化规则,也可以像个人习惯性的编码实践一样非正式。编译器不强制执行编码约定。因此,不遵循部分或全部规则不会对从源代码创建的可执行程序产生任何影响。”
您应该能够区分属性、局部变量、方法名称、类名称等,因为它们使用不同的命名约定。这类约定是有价值的。您可以在互联网上找到大量的指南和约定。所以,找到一个约定或建立自己的约定,并坚持下去。
以下内容(为类库开发设计的指南)是由微软特殊兴趣小组开发的。我只做了一些补充。
大小写约定
以下是我们 C# 编码标准、命名约定和最佳实践的一些示例。请根据您的具体需求使用它们。
Pascal 驼峰式命名法
标识符的第一个字母以及每个后续连接单词的第一个字母都大写。您可以对三个或更多字符的标识符使用 Pascal 命名法。
Camel 驼峰式命名法
标识符的第一个字母小写,而每个后续连接单词的第一个字母都大写。
参考:标识符的大小写规则
一些命名约定指南的示例
您会在互联网上找到充足的资源。我只强调一些我最喜欢的。
我在下面提供了一些基本示例,但正如我之前提到的,请找到适合您的良好约定并坚持下去。
请对类名和方法名使用 Pascal 命名法。
public class Product
{
public void GetActiveProducts()
{
//...
}
public void CalculateProductAdditinalCost()
{
//...
}
}
请对方法参数和局部变量使用 camel 命名法。
public class ProductCategory
{
public void Save(ProductCategory productCategory)
{
// ...
}
}
请不要使用缩写。
// Correct
ProductCategory productCategory;
// Avoid
ProductCategory prodCat;
请不要在标识符中使用下划线。
// Correct
ProductCategory productCategory;
// Avoid
ProductCategory product_Category;
请在接口名称前加上字母 I
。
public interface IAddress
{
}
请将所有成员变量声明放在类的顶部,静态变量放在最顶部。
public class Product
{
public static string BrandName;
public string Name{get; set;}
public DateTime DateAvailable {get; set;}
public Product()
{
// ...
}
}
请为枚举使用单数名称。例外:位域枚举。
public enum Direction
{
North,
East,
South,
West
}
请不要在枚举名称后加上 Enum 后缀。
//Avoid
public enum DirectionEnum
{
North,
East,
South,
West
}
为什么我们需要约定?
大型项目中的程序员有时会过度使用约定。他们制定了太多的标准和指南,以至于记住它们变成了一项全职工作。计算机并不关心您的代码是否可读。它比读取高级语言语句更擅长读取二进制机器指令。
约定提供了几个具体的好处。它们让您可以预设更多。通过做出一个全局的决定而不是许多局部的决定,您可以专注于代码更重要的特征。
- 它们帮助您跨项目转移知识
- 它们帮助您更快地学习新项目中的代码。
- 它们强调了相关项之间的关系。
您应该编写可读的代码,因为它有助于他人阅读您的代码。命名是我们作为软件开发人员最难做的工作之一。因为我们在命名事物上花费大量时间,需要命名的事物非常多,例如属性、方法、类、文件、项目等。我们应该在命名事物上投入一些精力,因为名称可以承载意义。为代码添加意义,这就是可读性的全部意义所在。
所以,这将帮助您晚上睡个好觉。
开发者应遵循的顶级规则
保持类的大小较小
我见过也写过一些巨大的类。但不幸的是,那些类从未取得好结果。后来我找到了原因,那些庞大的类试图做太多事情。这违反了单一职责原则。S
在 SOLID (面向对象设计) 中。
“单一职责原则指出,每个对象都应该只有一个职责,并且该职责应该完全由该类封装。其所有服务都应与其职责紧密对齐。”
或者
根据 Martin 的定义:“一个类永远不应该有不止一个改变的理由。”
为什么将这两个职责分离成不同的类很重要?因为每个职责都是一个改变的轴。当需求改变时,这种改变会通过类之间职责的变化而显现。如果一个类承担了不止一个职责,那么它就有不止一个改变的理由。如果一个类有多个职责,那么这些职责就会耦合。一个职责的变化可能会损害或阻碍该类满足其他职责的能力。这种耦合会导致设计脆弱,在改变时会以意想不到的方式中断。
避免过时的注释
让我们从过时的注释开始。根据 Robert C. Martin:
“已经过时、不相关且不正确的注释就是过时的注释。注释很快就会过时。最好不要编写会过时的注释。如果您发现过时的注释,最好尽快更新或删除它。过时的注释往往会偏离它们曾经描述的代码。它们在代码中成为过时和误导的孤岛。”
这个话题会在所有级别的开发者之间引发一些有趣的讨论。尽量避免对单个方法或简短类进行注释。因为我见过的大部分注释都在试图描述目的/意图。有些注释是无意义的。开发者编写注释是为了提高可读性和可维护性。请确保您的注释不会造成任何噪音。如果您能用更有意义的名称来命名一个方法,而不是使用注释,那将是很好的。我之所以这样建议,是因为方法名比注释更有效。大多数注释都是无意义的噪音。让我们来看看下面的注释:
//ensure that we are not exporting
//deleted products
if(product.IsDeleted && !product.IsExported )
{
ExportProducts = false;
}
// This is a for loop that prints the 1 million times
for (int i = 0; i < 1000000; i++)
{
Console.WriteLine(i);
}
如果我们像 CancelExportForDeletedProducts()
这样命名方法,而不是使用注释,会发生什么?所以,方法名比注释更有效。方法是执行的,它们是真实的。但注释在某些情况下是好的,例如,当 Visual Studio 使用注释创建 API 文档时,这些注释是不同的,您可以使用“///”来表示这些注释,以便其他开发者能够获得 API 或库的智能提示。
我并不是说必须始终避免注释。根据 Kent 的口头传统,重点更多地在于关于整个系统如何工作的注释,而不是单个方法的注释。如果注释试图描述目的/意图,那么它是错误的。如果您查看代码中注释很多的地方,您会注意到其中大部分注释是因为代码写得很糟糕。请阅读以下书籍以获取更多详细信息:
- 《C# 和 ASP.NET 专业重构》 作者:Danijel Arsenovski
- 《重构:改善既有代码的设计》 作者:Martin Fowler,Kent Beck,John Brant,William Opdyke,don Roberts
避免类中使用不必要的区域
区域是 VS 的一项功能,允许您围绕代码块进行分组。它可以是单个方法或多个方法。区域的存在是因为它更容易在大型文件中进行导航。区域被用来隐藏丑陋的代码或已爆炸的类。如果一个类做了太多事情,它也违反了单一职责原则。所以下次您考虑在文件中添加一个新区域时,请退一步问问自己,是否可以将您的区域分离成一个单独的类。
保持方法简短
方法中的代码行数越多,越难理解。每个人都推荐 20-25 行代码是可以接受的。但有些极客为了安全起见,偏爱 1-10 行,这是他们的个人偏好。没有硬性规定。提取方法是最常见的重构之一。如果您认为一个方法太长或需要注释才能理解其目的,您可以轻松地在那里应用提取方法。人们总是询问方法的长度。但长度不是问题。当您处理复杂方法时,跟踪所有局部变量可能会很复杂且耗时。此时提取方法可以节省大量时间。您可以使用 Visual Studio 的提取方法功能,该功能将跟踪传递到新方法中的变量,以及作为方法返回值输出参数的变量。
使用 ReSharper
使用 Microsoft Visual Studio
有关每个步骤的更多详细信息,请访问 MSDN 链接。
根据《重构:改善既有代码的设计》作者:Martin Fowler, Kent Beck (合著者), John Brant (合著者), William Opdyke, don Roberts
“提取方法是我最常进行的重构之一。我查看一个过长的方法,或者查看需要注释才能理解其目的的代码。然后,我将那段代码变成自己的方法。我偏爱简短、命名良好的方法,原因有几个。首先,当方法粒度很小时,它增加了其他方法可以复用该方法的几率。其次,它允许更高级别的方法读起来更像一系列注释。当方法粒度很小时,重写也更容易。如果您习惯于看到更大的方法,这需要一些时间来适应。而且,小方法
只有当您有好的名称时才真正起作用,所以您需要注意命名。人们有时会问我,我期望方法有多长。对我来说,长度不是问题。关键在于方法名称和方法体之间的语义距离。如果提取能提高清晰度,那就去做,即使提取后的名称比代码长。”
避免过多的参数
声明一个类来代替过多的参数。创建一个将所有这些参数组合在一起的类。这通常是更好的设计和有价值的抽象。
//avoid
public void Checkout(string shippingName, string shippingCity,
string shippingSate, string shippingZip, string billingName,
string billingCity, string billingSate, string billingZip)
{
}
//DO
public void Checkout(ShippingAddress shippingAddress,BillingAddress billingAddress)
{
}
我们应该引入类来代替所有的参数。
避免复杂的表达式
if(product.Price>500 && !product.IsDeleted &&
!product.IsFeatured && product.IsExported)
{
// do something
}
复杂的表达式背后有意义,只是被这些多个表达式隐藏了。我们可以通过使用属性将复杂的表达式封装到该对象中。这样代码会更容易阅读。
将警告视为错误
如果您注意代码,您会发现一个变量被声明但从未被使用。通常,当我们构建项目时,我们会收到一个警告,并且可以在没有错误的情况下运行我们的项目。但我们应该尽可能多地消除警告。所以,请按照以下步骤设置您的构建,将警告视为错误:
最小化返回语句的数量
最小化每个例程中的返回语句数量。如果读一个例程时,您不了解它可能在上方返回过,那么它就更难理解了。
当返回语句可以提高可读性时,请使用它。在某些例程中,一旦您知道了答案,就想立即将其返回给调用例程。如果例程的定义方式不需要进行任何清理,那么不立即返回意味着您必须编写更多的代码。
//avoid
if(product.Price>15)
{
return false;
}
else if(product.IsDeleted)
{
return false;
}
else if(!product.IsFeatured)
{
return false;
}
else if()
{
//.....
}
return true;
//DO
var isValid = true;
if(product.Price>15)
{
isValid= false;
}
else if(product.IsDeleted)
{
isValid= false;
}
else if(!product.IsFeatured)
{
isValid= false;
}
return isValid;
您可以想象 4 个出口点分散在 20-30 行代码中。这使得您更难理解方法内部发生的情况,以及什么将执行,什么时候执行。
我收到了很多回复,有些同意,但大多数都不同意这是一个应该强制执行的“好”标准。为了找出潜在的问题,我做了一些单元测试,发现具有多个出口点的复杂方法通常有针对每个路径的一组测试。
if( BADFunction() == true)
{
// expression
if( anotherFunction() == true )
{
// expression
return true;
}
else
{
//error
}
}
else
{
//error
}
return false;
if( !GoodFunction())
{
// error.
return false
}
// expression
if( !GoodFunction2())
{
//error.
return false;
}
// more expression
return true;
参考:“代码大全”作者:Steve McConnell 了解更多细节。
将声明移近引用处
有些程序员习惯于将临时变量声明放在方法的开头。但将变量声明放在离它第一次被引用的行很远的地方,会使代码更难阅读,因为它不清楚变量是如何初始化的。此外,提前声明变量会使方法更难重构。如果您尝试提取方法的一部分,其中引用了该变量,您将必须将该变量作为参数传递给新提取的方法,即使声明本可以放在提取的块内部。
断言
断言是在开发过程中使用的代码——通常是一个例程或宏——允许它在运行时进行自我检查。为真表示一切正常运行。为假表示它检测到了代码中的意外错误。断言通常接受两个参数:一个布尔表达式,描述假设应该为真,以及一个消息,用于在不为真时显示。
断言在大型、复杂和高可靠性系统中特别有用。
示例:如果系统假设最多有 100,000 条用户记录,则系统可能包含一个断言,即记录数小于或等于 100,000。因此,断言在该范围内将保持沉默。如果它遇到更多记录,它将大声“断言”程序中存在错误。
检查循环端点
一个循环通常有三个感兴趣的情况:第一种情况、任意选取的中间情况和最后一种情况。如果您有任何不同于第一种或最后一种情况的特殊情况,也要检查那些。如果循环包含复杂的计算,请拿出计算器手动检查计算。
结论
根据组织行为、项目性质和领域实施公司标准是很明显的。所以我想再说一遍:“找到一个约定并坚持下去”。
如果您认为我遗漏了任何重要的指南,请在评论部分留下。我将尝试将其包含在主帖子中。快乐编程。
文章更新日志
- 2013 年 6 月 10 日:新增观点“将声明移近引用处”
- 2013 年 5 月 30 日:更新了内容和示例。
- 2013 年 3 月 4 日:更新了内容“避免过时的注释”。
- 2013 年 3 月 1 日:添加了内容“避免过时的注释”。
- 2013 年 3 月 1 日:添加了内容“尽量避免注释”。
- 2013 年 2 月 28 日:添加了“保持类的大小较小”。
- 2013 年 2 月 15 日:添加了内容和参考。
- 2013 年 2 月 10 日:添加了内容(保持方法简短)。
- 2013 年 2 月 9 日:添加了内容和示例(保持方法简短)。
- 2013 年 2 月 8 日:修改了“尽量避免注释”。
- 2013 年 2 月 8 日:添加了内容和代码示例(避免多个出口点)。
- 2013 年 2 月 6 日:添加了内容和参考。
- 2013 年 2 月 4 日:初次发布。