65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 应用程序开发的一些最佳实践(已解释)

2010 年 10 月 17 日

CPOL

15分钟阅读

viewsIcon

623066

在本文中,我将详细讨论 C# 的最佳编码实践。

引言

几天前,在我的一篇早期博文中,我根据我过去几年的经验列出了“C# 应用程序开发的一些最佳实践”,这篇文章受到了读者的热烈欢迎。我也收到了很多反馈。我的许多读者也提出了宝贵的建议。在本文中,我将讨论其中大部分要点。我将定期更新本文,加入新的最佳编码实践。希望在这里也能收到更多反馈和/或建议。让我们开始讨论 C# 应用程序开发的最佳编码实践。以下是一些要点:

使用正确的命名约定

您应该优先使用正确的命名约定来保持代码的一致性。如果整个解决方案都使用一致的命名,代码将非常容易维护。以下是一些 .NET 开发人员通常遵循的命名约定:

  • 声明变量时,始终使用驼峰命名法(单词首字母小写,后续每个单词的首字母大写)。
     
  • 声明属性时,使用帕斯卡命名法(单词首字母大写,后续每个单词的首字母大写)。
     
  • 避免为属性、变量或方法名使用全大写或全小写。声明常量变量时使用全大写。
     
  • 切勿使用以数字开头的名称。
     
  • 始终优先为类、属性、方法等使用有意义的名称。这对于您将来维护代码非常有帮助。例如,“P”不能为类提供正确的含义。您会发现很难了解该类。但如果您使用“Person”,您就能轻松理解。
     
  • 切勿构建因大小写不同而变化的名称。这是一个非常糟糕的实践。这在开发代码时没有用处,因为您不知道“person”类和“Person”类是什么!!!但根据上述场景,可以非常清楚地理解“person”是“Person”类的实例变量。
     
  • 不要使用 .NET Framework 中已有的名称。新接触您代码的人很难轻易理解。
     
  • 避免为标识符添加前缀或后缀。虽然在一些指南中,他们使用“m_”作为变量声明的前缀,在另一些指南中,他们使用“_”。我认为这并不是那么有用。但这取决于您组织的编码实践。这一点因不同组织而异,并且没有严格的指导。
     
  • 声明接口时,始终使用“I”作为前缀。这是声明接口的常用做法。
     
  • 为自定义异常类始终添加“Exception”作为后缀。这将使您的异常类更具可见性。
     
  • 切勿在类名前缀或后缀其属性名。这会不必要地增加属性名的长度。如果“Firstname”是“Person”类的属性,您可以直接从该类中轻松识别。无需编写“PersonFirstname”或“FirstnameOfPerson”。
     
  • 为布尔属性(如“IsVisible”、“HasChildren”、“CanExecute”)添加“Is”、“Has”或“Can”前缀。这些为属性提供了正确的含义。
     
  • 不要为控件添加前缀,而是编写合适的名称来标识控件。

决定使用值类型还是引用类型

每当您需要创建一种类型时,首先问自己一个问题:“您想要什么?为什么需要它?”如果您能回答这个问题,您就可以决定要使用哪种类型。如果您想存储数据,请使用值类型;如果您想通过定义行为来创建类型的实例,请使用引用类型。值类型不是多态的,而引用类型可以。在内存利用方面,值类型比引用类型更有效,产生的帮助碎片和垃圾回收更少。如果您想将值传递给方法实现,请决定您想做什么,并根据您的需求在值类型和引用类型之间做出决定。引用类型变量的使用实际上会更改原始值,而值类型的使用会创建原始变量的副本并传递给方法。因此,它保护您的原始值免遭意外更改。让我们在实际示例中看看它们。在下面的代码中,我们将变量“i”作为值类型传递,并在方法实现中将其增加 10。由于它是按值传递的,您将看到程序代码的输出是原始值“5”。

static void Main(string[] args)
{
	int i = 5;
	SetValues(i);

	System.Console.WriteLine("Currently i = " + i); // will print "Currently i = 5"
}

static void SetValues(int x)
{
	x += 10;
}

在下面的代码中,由于我们将“i”作为引用发送,它将更改方法内部“i”的原始值,因此您将在屏幕上看到输出 15。

static void Main(string[] args)
{
	int i = 5;
	SetValues(ref i);

	System.Console.WriteLine("Currently i = " + i); // will print "Currently i = 15"
}

static void SetValues(ref int x)
{
	x += 10;
}

因此,在开始实现之前就做出决定,否则一旦实现,在您的庞大应用程序中进行更改将非常困难。

始终使用属性而不是公共变量

这样做的原因是,它使得您的代码在 OOP 环境中得到良好的封装。通过使用 getter 和 setter,您可以限制用户直接访问成员变量。您可以限制显式设置值,从而保护您的数据免遭意外更改。此外,属性为您提供了更轻松的数据验证。让我们看一个小代码。

public class Person
{
	public string name;

	public string GetNameInLowerCase()
	{
		return string.IsNullOrWhiteSpace(name) ? 
				string.Empty : name.ToLower();
	}

	public string GetNameInUpperCase()
	{
		return string.IsNullOrWhiteSpace(name) ? 
				string.Empty : name.ToUpper();
	}
}

在上面的示例中,您会看到每次都需要检查 Null 值,因为外部人员可能会轻易意外地更改值并导致代码中的 Bug。如果您不希望代码中出现该 Bug,您应该做的是使用变量的属性实现(使其成为 private),然后访问该属性。这提供了更高的代码可靠性。让我们看一个示例。

public class Person
{
	public string name;

	public string Name
	{
		get
		{
			return string.IsNullOrWhiteSpace(name) ? string.Empty : name;
		}
		set { name = value; }
	}

	public string GetNameInLowerCase()
	{
		return Name.ToLower();
	}

	public string GetNameInUpperCase()
	{
		return Name.ToUpper();
	}
}

现在在这种情况下,您不必每次都担心检查值是否为 Null。属性的 getter 实现会处理。因此,一次实现,多次使用。所以,如果有人显式地将“Name”属性设置为 null,您的代码将不受任何影响。这只是一个示例代码,用于与您讨论需要属性而不是 public 成员变量。实际实现可能会根据您的需求而有所不同。

public class Person
{
	public string Name { get; private set; }
	public string Age { get; }
}

如果您不希望任何人从外部显式设置属性,您只需在属性实现中仅实现 getter,或将 setter 标记为 private。此外,您可以从任何接口实现属性,从而提供更多的 OOP 环境。

在需要时使用可空数据类型

有时,您可能需要将 null 作为整数、双精度或布尔变量的值来存储。那么您该如何做呢?普通声明不允许您将 null 存储为值。C# 现在具有可空数据类型的特性。只需在声明中稍作改动。这样就可以了!您可以存储 null 值。您只需要使用“?”修饰符。您必须将其放在类型之后。要定义一个整数,您通常会进行如下声明:

int index = 0; // simple declaration of int

要将其转换为可空数据类型,您需要稍作修改,声明如下:

int? index = null; // nullable data type declaration

一旦为数据类型添加了“?”修饰符,您的变量就将变为可空,您就可以为其存储“null”值。通常,这在与布尔值一起使用时很有帮助。

优先使用运行时常量而不是编译时常量

始终优先使用运行时常量而不是编译时常量。在这里,您可能会问什么是运行时常量,什么是编译时常量。运行时常量是在运行时进行计算并使用“readonly”关键字声明的。另一方面,编译时常量是 static 的,在编译时进行计算并使用“const”关键字声明。

public readonly string CONFIG_FILE_NAME = "web.config"; // runtime constant
public const string CONFIG_FILE_NAME = "web.config"; // compile time constant

那么,为什么需要优先使用 readonly 而不是 const 变量呢?编译时常量(const)必须在声明时初始化,并且以后不能更改。此外,它们仅限于数字和字符串。IL 会将 const 变量替换为其值,从而覆盖整个代码,因此速度稍快。而在另一方面,运行时常量(readonly)在构造函数中初始化,并且可以在不同的初始化时间进行更改。IL 引用的是 readonly 变量而不是原始值。所以,当您遇到一些关键情况时,请使用 const 来使代码运行更快。当您需要可靠的代码时,请始终优先使用 readonly 变量。

在强制类型转换时,优先使用“is”和“as”运算符

在强制类型转换时,最好使用“is”和“as”运算符。与显式强制类型转换相比,使用隐式强制类型转换。让我用代码示例向您说明。

// this line may throw Exception if it is unable to downcast from Person to Employee
var employee = (Employee) person;

在上面的代码中,假设您的 person 是 Customer 类型,当您将其转换为 Employee 类型时,它将抛出 Exception,在这种情况下,您必须使用 try{} catch{} 块来处理。让我们使用“is”和“as”运算符进行相同的转换。请看下面的代码。

// check if the person is Employee type
if(person is Employee)
{
	// convert person to Employee type
	employee = person as Employee;
}

// check if the person is Customer type
else if(person is Customer)
{
	// convert person to Customer type
	customer = person as Customer;
}

在上面的代码中,您可以看到,在第二行我正在检查 person 是否为 Employee 类型。如果是 Employee 类型,它将进入该块。否则,如果它是 Customer 类型,它将进入第 12 行的块。现在,使用“as”运算符进行转换,如第 5 行所示。在这里,如果无法转换,它将返回 null,但不会抛出任何异常。因此,在下一行,您可以检查转换后的值是否为 null。根据该值,您可以执行您想要的操作。

字符串连接优先使用 string.Format() 或 StringBuilder

string 中的任何操作都会创建一个新对象,因为 string 是不可变对象。如果您想连接多个 string,最好使用 string.Format() 方法或 StringBuilder 类进行连接。

string str = "k";
str += "u";
str += "n";
str += "a";
str += "l";
 
Console.WriteLine(str); 

对于 string.Format(),它不会创建多个对象,而是创建一个单一对象。StringBuilder 作为不可变对象,不会为每个操作创建单独的内存。因此,您的应用程序内存管理将不会处于危险状态。因此,执行此类操作最好使用 string.Format()StringBuilder

上面的代码会在每次添加新 string 时创建一个新的 string 对象。使用 string.Format(),您可以编写以下代码:

string str = string.Format("{0}{1}{2}{3}{4}", "k", "u", "n", "a", "l");

如果您想使用 StringBuilder,这里是代码:

StringBuilder sb = new StringBuilder();
 
sb.Append("k");
sb.Append("u");
sb.Append("n");
sb.Append("a");
sb.Append("l");
 
string str = sb.ToString();

上述两个示例每次都将使用相同的 string 实例。

在需要时使用条件属性

当您只想为调试版本执行某些操作时,条件属性非常有用。我知道您可能会想到 #if/#endif 块。是的,您可以使用 #if/#endif 块来执行特定于调试版本的操作,例如跟踪或将内容打印到输出屏幕。那么,为什么我们不使用它们呢?当然,您可以这样做。但是,有一些陷阱。假设您编写了以下代码:

private void PrintFirstName() 
{ 
    string firstName = string.Empty; 
    #if DEBUG 
    firstName = GetFirstName(); 
    #endif 
    Console.WriteLine(firstName); 
}

在这种情况下,它在调试模式下可以正常工作。但是,看看另一方面。当它转到生产环境时,GetFirstName() 将永远不会被调用,因此它将打印一个空的 string。这将是您代码中的一个 Bug。那么,该怎么办?我们可以使用条件属性来克服这个问题。将您的代码编写在一个方法中,并将类型为 Conditional 的属性设置为 DEBUG 字符串。当您的应用程序在调试模式下运行时,它将调用该方法;而在其他情况下,它将不调用。让我们看看这里的代码:

[Conditional(“DEBUG”)]
private void PrintFirstName()
{
    string firstName = GetFirstName();
    Console.WriteLine(firstName);
}

上面的代码实际上是通过 Conditional 属性隔离了函数。如果您的应用程序在调试版本下运行,它将在第一次调用 PrintFirstName() 时将其加载到内存中。但当它进入生产版本(即发布版本)时,该方法将永远不会被调用,因此不会加载到内存中。编译器会处理一旦将条件属性应用到方法后该做什么。因此,始终建议使用条件属性而不是 #if 编译指示块。

[Conditional(“DEBUG”)]
[Conditional(“TRACE”)]
private void PrintFirstName()
{
    string firstName = GetFirstName();
    Console.WriteLine(firstName);
}

此外,如果您想设置多个条件属性,可以通过添加多个属性来实现,如上所示。

在枚举值类型中,始终使用“0”(零)作为默认值

当我们在编写 enum 定义时,有时会忘记为其设置 DEFAULT 值。在这种情况下,它将自动设置为 0(零)。

public enum PersonType 
{ 
    Customer = 1 
}
 
class Program 
{ 
        static void Main(string[] args) 
        { 
            PersonType personType = new PersonType(); 
            Console.WriteLine(personType); 
    } 
}

例如,上面的代码将始终输出 0(零),您必须显式为其设置默认值。但是,如果我们稍作修改并添加默认值,它将始终输出默认值而不是 0(零)。

public enum PersonType 
{ 
    None = 0, 
    Customer = 1 
}
 
class Program 
{ 
        static void Main(string[] args) 
        { 
            PersonType personType = new PersonType(); 
            Console.WriteLine(personType); 
    } 
}

在这种情况下,它将输出“None”。实际上,系统会将所有值类型的实例初始化为 0。没有办法阻止用户创建所有为 0(零)的值类型实例。因此,始终建议在 enum 类型中隐式设置默认值。

始终优先使用 foreach(…) 循环

foreach 语句是 dowhilefor 循环的变体。它实际上会为您拥有的任何集合生成最佳的迭代代码。当您使用集合时,始终优先使用 foreach 循环,因为 C# 编译器会为您的特定集合生成最佳的迭代代码。请看下面的实现。

foreach(var collectionValue in MyCollection)
{
    // your code
}

在这里,如果您的 MyCollectionArray 类型,C# 编译器将为您生成以下代码:

for(int index = 0; index < MyCollection.Length; index++)
{
    // your code
}

将来,如果您将集合类型更改为 ArrayList 而不是 Arrayforeach 循环仍将编译并正常工作。在这种情况下,它将生成以下代码:

for(int index = 0; index < MyCollection.Count; index++)
{
    // your code
}

检查 for 条件。您会看到一些小的差异。ArrayLength 属性,因此它生成调用 Length 的代码。但对于 ArrayList,它将 Length 替换为 Count,因为 ArrayListCount,它执行相同的功能。在其他情况下,如果您的集合类型实现了 IEnumerator,它将为您生成不同的代码。让我们看看代码。

IEnumerator enumerator = MyCollection.GetEnumerator();
 
while(enumerator.MoveNext())
{
    // your code
}

因此,您无需考虑使用哪种循环。编译器负责这一点,它将为您选择最佳循环。foreach 循环的另一个优点是遍历一维或多维数组。对于多维数组,您不必编写多行 for 语句。因此,它将减少您需要编写的代码量。编译器内部将生成该代码。从而提高您的生产力。

妥善利用 try/catch/finally 块

是的,妥善利用 try/catch/finally 块。如果您知道您编写的代码可能会抛出异常,请使用 try/catch 块来处理该代码段中的异常。如果您知道您代码的第 5 行可能会抛出异常,建议仅将该行代码用 try/catch 块包围。不必要地用 try/catch 包围代码的其余部分会减慢应用程序的速度。有时,我注意到人们用 try/catch 块包围每个方法,这真的很糟糕,并且会降低代码的性能。所以从现在开始,只在需要时使用它。使用 finally 块在调用后清理任何资源。如果您正在进行任何数据库调用,请在该块中关闭连接。无论您的代码是否正常执行,finally 块都会运行。因此,妥善利用它来清理资源。

只捕获您能处理的异常

这也是一个主要问题。人们总是使用通用的 Exception 类来捕获任何异常,这对于您的应用程序和系统性能都不利。只捕获您期望的异常,并按顺序排列它们。最后,如果需要,可以添加通用的 Exception 来捕获任何其他未知异常。这为您提供了一种妥善处理异常的方式。假设您的代码抛出 NullReferenceExceptionArgumentException。如果您直接使用 Exception 类,在您的应用程序中处理起来会非常困难。但通过正确捕获异常,您可以轻松处理问题。

使用 IDisposable 接口

使用 IDisposable 接口来释放内存中的所有资源。一旦您在类中实现了 IDisposable 接口,您将在其中获得一个 Dispose() 方法。在该方法中编写代码来释放资源。如果您实现了 IDisposable,您可以这样初始化您的类:

using (PersonDataSource personDataSource = DataSource.GetPerson())
{
    // write your code here
}

using() {} 块之后,它将自动调用 Dispose() 方法来释放类资源。您不必显式调用 Dispose() 来清理类。

将您的逻辑拆分成多个小型简单的方

如果方法过长,有时很难处理。始终最好根据其功能使用多个小型方法,而不是将它们全部放在一个方法中。如果您将它们分解成单独的方法,将来如果您需要调用其中一部分,调用将比复制代码更容易。此外,对小型代码块进行单元测试也更容易。因此,无论何时编写代码片段,首先思考您想做什么。根据 đó,将您的代码提取到小型简单的方法中,并在需要时调用它们。通常,一个方法不应超过 10-15 行。

结束语

希望这些要点能帮助您正确理解 C# 编码实践。还有一些我没有在这里涵盖的要点。我将很快在同一主题下发布它们。如果您有任何建议或意见,请告诉我。祝您一切顺利…… 微笑

© . All rights reserved.