C# 程序员最常犯的 10 个错误
关于C#
C#是达成微软公共语言运行库(CLR)的少数语言中的一种。达成CLR的语言可以受益于其带来的特性,如跨语言集成、异常处理、安全性增强、部件组合的简易模型以及调试和分析服务。作为现代的CLR语言,C#是应用最为广泛的,其应用场景针对Windows桌面、移动手机以及服务器环境等复杂、专业的开发项目。
C#是种面向对象的强类型语言。C#在编译和运行时都有的强类型检查,使在大多数典型的编程错误能够被尽早地发现,而且位置定位相当精准。相比于那些不拘泥类型,在违规操作很久后才报出可追踪到莫名其妙错误的语言,这可以为程序员节省很多时间。然而,许多程序员有意或无意地抛弃了这个检测的有点,这导致本文中讨论的一些问题。
关于本文
本文描述了10个 C# 程序员常犯的错误,或应该避免的陷阱。
尽管本文讨论的大多数错误是针对 C# 的,有些错误与其他以 CLR 为目标的语言,或者用到了 Framework Class Library (FCL) 的语言也相关。
常见错误 #1: 把引用当做值来用,或者反过来
C++ 和其他很多语言的程序员,习惯了给变量赋值的时候,要么赋单纯的值,要么是现有对象的引用。然而,在C# 中,是值还是引用,是由写这个对象的程序员决定的,而不是实例化对象并赋值的程序员决定的。这往往会坑到 C# 的新手程序员。
如果你不知道你正在使用的对象是否是值类型或引用类型,你可能会遇到一些惊喜。例如:
Point point1 = new Point(20, 30); Point point2 = point1; point2.X = 50; Console.WriteLine(point1.X); // 20 (does this surprise you?) Console.WriteLine(point2.X); // 50 Pen pen1 = new Pen(Color.Black); Pen pen2 = pen1; pen2.Color = Color.Blue; Console.WriteLine(pen1.Color); // Blue (or does this surprise you?) Console.WriteLine(pen2.Color); // Blue
如你所见,尽管Point和Pen对象的创建方式相同,但是当一个新的X的坐标值被分配到point2时, point1的值保持不变 。而当一个新的color值被分配到pen2,pen1也随之改变。因此,我们可以推断point1和point2每个都包含自己的Point对象的副本,而pen1和pen2引用了同一个Pen对象 。如果没有这个测试,我们怎么能够知道这个原理?
一种办法是去看一下对象是如何定义的(在Visual Studio中,你可以把光标放在对象的名字上,并按下F12键)
public struct Point { … } // defines a “value” type public class Pen { … } // defines a “reference” type
如上所示,在C#中,struct关键字是用来定义一个值类型,而class关键字是用来定义引用类型的。 对于那些有C++编程背景人来说,如果被C++和C#之间某些类似的关键字搞混,可能会对以上这种行为感到很吃惊。
如果你想要依赖的行为会因值类型和引用类型而异,举例来说,如果你想把一个对象作为参数传给一个方法,并在这个方法中修改这个对象的状态。你一定要确保你在处理正确的类型对象。
常见的错误#2:误会未初始化变量的默认值
在C#中,值得类型不能为空。根据定义,值的类型值,甚至初始化变量的值类型必须有一个值。这就是所谓的该类型的默认值。这通常会导致以下,意想不到的结果时,检查一个变量是否未初始化:
class Program { static Point point1; static Pen pen1; static void Main(string[] args) { Console.WriteLine(pen1 == null); // True Console.WriteLine(point1 == null); // False (huh?) } }
为什么不是【point 1】空?答案是,点是一个值类型,和默认值点(0,0)一样,没有空值。未能认识到这是一个非常简单和常见的错误,在C#中
很多(但是不是全部)值类型有一个【IsEmpty】属性,你可以看看它等于默认值:
Console.WriteLine(point1.IsEmpty); // True
当你检查一个变量是否已经初始化,确保你知道值未初始化是变量的类型,将会在默认情况下,不为空值。
常见错误 #3: 使用不恰当或未指定的方法比较字符串
在C#中有很多方法来比较字符串。
虽然有不少程序员使用==操作符来比较字符串,但是这种方法实际上是最不推荐使用的。主要原因是由于这种方法没有在代码中显示的指定使用哪种类型去比较字符串。
相反,在C#中判断字符串是否相等最好使用Equals方法:
public bool Equals(string value); public bool Equals(string value, StringComparison comparisonType);
第一个Equals方法(没有comparisonType这参数)和使用==操作符的结果是一样的,但好处是,它显式的指明了比较类型。它会按顺序逐字节的去比较字符串。在很多情况下,这正是你所期望的比较类型,尤其是当比较一些通过编程设置的字符串,像文件名,环境变量,属性等。在这些情况下,只要按顺序逐字节的比较就可以了。使用不带comparisonType参数的Equals方法进行比较的唯一一点不好的地方在于那些读你程序代码的人可能不知道你的比较类型是什么。
使用带comparisonType的Equals方法去比较字符串,不仅会使你的代码更清晰,还会使你去考虑清楚要用哪种类型去比较字符串。这种方法非常值得你去使用,因为尽管在英语中,按顺序进行的比较和按语言区域进行的比较之间并没有太多的区别,但是在其他的一些语种可能会有很大的不同。如果你忽略了这种可能性,无疑是为你自己在未来的道路上挖了很多“坑”。举例来说:
string s = "strasse"; // outputs False: Console.WriteLine(s == "straße"); Console.WriteLine(s.Equals("straße")); Console.WriteLine(s.Equals("straße", StringComparison.Ordinal)); Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCulture)); Console.WriteLine(s.Equals("straße", StringComparison.OrdinalIgnoreCase)); // outputs True: Console.WriteLine(s.Equals("straße", StringComparison.CurrentCulture)); Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCultureIgnoreCase));
最安全的实践是总是为Equals方法提供一个comparisonType的参数。
下面是一些基本的指导原则:
当比较用户输入的字符串或者将字符串比较结果展示给用户时,使用本地化的比较(CurrentCulture 或者CurrentCultureIgnoreCase)。
当用于程序设计的比较字符串时,使用原始的比较(Ordinal 或者 OrdinalIgnoreCase)
InvariantCulture和InvariantCultureIgnoreCase一般并不使用,除非在受限的情境之下,因为原始的比较通常效率更高。如果与本地文化相关的比较是必不可少的,它应该被执行成基于当前的文化或者另一种特殊文化的比较。
此外,对Equals 方法来说,字符串也通常提供了Compare方法,可以提供字符串的相对顺序信息而不仅仅中测试是否相等。这个方法可以很好适用于<, <=, >和>= 运算符,对上述讨论同样适用。
常见误区 #4: 使用迭代式 (而不是声明式)的语句去操作集合
在C# 3.0中,LINQ的引入改变了我们以往对集合对象的查询和修改操作。从这以后,你应该用LINQ去操作集合,而不是通过迭代的方式。
一些C#的程序员甚至都不知道LINQ的存在,好在不知道的人正在逐步减少。但是还有些人误以为LINQ只用在数据库查询中,因为LINQ的关键字和SQL语句实在是太像了。
虽然数据库的查询操作是LINQ的一个非常典型的应用,但是它同样可以应用于各种可枚举的集合对象。(如:任何实现了IEnumerable接口的对象)。举例来说,如果你有一个Account类型的数组,不要写成下面这样:
decimal total = 0; foreach (Account account in myAccounts) { if (account.Status == "active") { total += account.Balance; } }
你只要这样写:
decimal total = (from account in myAccounts where account.Status == "active" select account.Balance).Sum();
虽然这是一个很简单的例子,在有些情况下,一个单一的LINQ语句可以轻易地替换掉你代码中一个迭代循环(或嵌套循环)里的几十条语句。更少的代码通常意味着产生Bug的机会也会更少地被引入。然而,记住,在性能方面可能要权衡一下。在性能很关键的场景,尤其是你的迭代代码能够对你的集合进行假设时,LINQ做不到,所以一定要在这两种方法之间比较一下性能。