在软件开发过程中,写出“能运行”的代码并不难,真正困难的是写出一段容易理解、容易维护、容易扩展,并且能够长期稳定运行的代码 。随着项目规模不断扩大,代码往往会经历这样的变化。
最初,一个类可能只有几十行。逻辑简单,修改起来非常直接;但随着需求不断增加,功能不断堆叠,代码开始出现越来越多的问题:
一个方法越来越长,承担了太多职责;
类之间的依赖越来越复杂,修改地方可能影响多个模块;
重复代码不断出现,导致维护成本增加;
异常处理、资源释放、性能优化等细节容易被忽略;
新成员接手代码时,需要花费大量时间理解历史逻辑。
这些问题并不是因为开发者能力不足,而是因为代码质量需要持续演进 。C# 作为一门成熟的面向对象语言,提供了大量语言特性帮助我们编写更优雅、更安全的代码,例如泛型、委托、Lambda、LINQ、异步编程、特性(Attribute)、模式匹配等。但如果只是掌握语法,而没有形成良好的编程习惯,很容易写出“看起来能工作,但长期维护困难”的代码。
《C# 高效编程:改进 C# 代码的 50 个有效办法 第2版》 这本书并不是单纯讲解 C# 语法,而是从工程实践角度,总结了大量能够改善代码质量的技巧和原则。
这些建议覆盖了多个方面:
如何正确设计类型和接口;
如何减少代码耦合;
如何利用 C# 特性提升表达能力;
如何处理异常、资源和状态;
如何编写更高性能、更可靠的程序。
接下来,我会结合书中的实践建议,并加入自己在项目开发中的理解,对这些技巧进行整理,希望能够帮助自己,也帮助更多 C# 开发者写出更加健壮、优雅的代码。
第1章 C#语言习惯
为何程序已经可以正常工作了,还要继续修改呢?答案是我们还能让程序变得更好 。如果我们总是墨守成规,那么将永远体会不到新技术带来的优化。本章将讨论那些在C#中应该改变的旧习惯,以及与其对应的推荐的新做法。
条目1 使用属性而不是可访问属性的数据成员
属性允许将数据成员作为公共接口的一部分暴露出去,同时仍提供面向对象环境下所需要的封装。属性这个语言元素可让我们像访问数据成员一样使用,但底层依旧使用方法实现。
在以后可能会产生新的需求或行为场景中,属性更容易修改。例如,很快就有想法,客户对象的名称不应该为空白。若使用了共有属性来封装Name,那么只要修改一处即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Customer
{
private string name ;
public string Name
{
get { return name ; }
set
{
if ( string . IsNullOrEmpty ( value ))
{
throw new ArgumentException ( "Name cannot be blank" , "Name" );
}
name = value ;
}
}
}
若是使用了公有的数据成员,那么就需要每一个设置客户名称的代码并逐一修复,将花费大量时间。由于属性是使用方法来实现的,所以添加多线程支持 也非常简单。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Customer
{
private readonly object syncHandle = new ();
private string name ;
public string Name
{
get {
lock ( syncHandle ) {
return name ;
}
}
set
{
if ( string . IsNullOrEmpty ( value ))
{
throw new ArgumentException ( "Name cannot be blank" , "Name" );
}
lock ( syncHandle )
{
name = value ;
}
}
}
}
属性用于方法的所有语言特性。例如,属性可以是虚的(virtual),允许子类重写(override)属性的读写逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Animal
{
public virtual string Name { get ; set ; }
}
public class Dog : Animal
{
public override string Name
{
get
{
return "Dog: " + base . Name ;
}
set
{
base . Name = value ;
}
}
}
属性为什么可以是虚的?因为属性本质上不是字段。
1
public string Name { get ; set ;}
编译后类似:
1
2
3
4
5
6
7
8
9
10
11
12
13
private string _name ;
public string get_Name ()
{
return _name ;
}
public void set_Name ( string value )
{
_name = value ;
}
所以animal.Name,调用的是animal.get_Name()。
还可以将属性声明为抽象的(abstract) 。意思是这个属性没有实现(没有getter/setter),必须由子类强制实现。
1
2
3
4
5
6
7
8
9
10
11
12
public abstract class Animal
{
public abstract string Name { get ; set ; }
}
public class Dog : Animal
{
private string _name ;
public override string Name { get => _name ; set => _name = value ; }
}
和 virtual 属性的区别:
特性
virtual 属性
abstract 属性
是否有默认实现
✔ 有
✘ 没有
子类是否必须重写
❌ 不必须
✔ 必须
是否能实例化基类
✔ 可以
❌ 不可以
设计意图
“可选覆盖”
“必须实现”
若类型需要包含并暴露出可索引的功能,那么可以使用索引器(indexer) 。它让对象像数组一样,通过参数访问内部数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyCollection
{
private string [] _data = new string [ 10 ];
public string this [ int index ]
{
get { return _data [ index ]; }
set { _data [ index ] = value ; }
}
}
// 使用
var c = new MyCollection ();
c [ 0 ] = "hello" ;
Console . WriteLine ( c [ 0 ]);
编译后的本质是:
1
2
get_Item ( int index )
set_Item ( int index , string value )
只是C#提供了语法糖:
索引器不仅仅是[int],可以是任意参数组合。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Matrix
{
private int [,] _data = new int [ 10 , 10 ];
public int this [ int x , int y ]
{
set { _data [ x , y ] = value ; }
get { return _data [ x , y ]; }
}
}
// 使用
var matrix = new Matrix ();
matrix [ 1 , 2 ] = 100 ;
Console . WriteLine ( matrix [ 1 , 2 ]);
支持不同类型参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ConfigStore
{
private Dictionary < string , string > _dict = new ();
public string this [ string key ]
{
get { return _dict [ key ]; }
set { _dict [ key ] = value ; }
}
}
// 使用
var config = new ConfigStore ();
config [ "ip" ] = "127.0.0.1" ;
Console . WriteLine ( config [ "ip" ]);
属性访问就像是访问一个数据字段,因此不要与访问数据有太过明显的性能差别。属性访问器不要执行长时间的计算,或进行跨应用程序的调用(例如数据库查询等),或是其他与调用者期待不符的耗时操作。
本条目作者想表达的是
无论何时需要在类型的公有或保护接口中暴露数据,都应该使用属性。也应该使用索引器暴露序列或字典。所有的数据成员都应该是私有的,没有例外。
条目8 推荐使用查询语法而不是循环
程序逻辑的表达由命令式(Imperative)转为声明式(Declarative)核心意思是:
从“告诉计算机一步一步怎么做”,变成“告诉计算机我想要什么结果,由系统决定怎么实现”。
命令式: 描述过程 (How)
命令式变成关注控制流程。
1
2
3
4
5
int [] foo = new int [ 100 ];
for ( int num = 0 ; num < foo . Length ; num ++)
{
foo [ num ] = num * num ;
}
控制了执行步骤。
声明式: 描述结果 (What)
1
int [] foo = [.. ( from n in Enumerable . Range ( 0 , 100 ) select n * n )];
表达的是目标状态,而不是实现过程。
本条目作者想表达的是
查询语法要比传统的命令式循环结构更加清晰地表达我们的意图。
第2章 .NET资源管理
第3章 使用C#表达设计
第4章 使用框架
第5章 C#中的动态编程
第6章 杂项
推荐
LINQ实战
参考
C#高效编程 改进C#代码的50个行之有效的办法(第2版)