C# 程序员最常犯的 10 个错误

关于C#

  C#是达成微软公共语言运行库(CLR)的少数语言中的一种。达成CLR的语言可以受益于其带来的特性,如跨语言集成、异常处理、安全性增强、部件组合的简易模型以及调试和分析服务。作为现代的CLR语言,C#是应用最为广泛的,其应用场景针对Windows桌面、移动手机以及服务器环境等复杂、专业的开发项目。

  C#是种面向对象的强类型语言。C#在编译和运行时都有的强类型检查,使在大多数典型的编程错误能够被尽早地发现,而且位置定位相当精准。相比于那些不拘泥类型,在违规操作很久后才报出可追踪到莫名其妙错误的语言,这可以为程序员节省很多时间。然而,许多程序员有意或无意地抛弃了这个检测的有点,这导致本文中讨论的一些问题。

 关于本文

  本文介绍了10种最常见的编程错误,或是C#程序员要避免的陷阱。

  尽管本文中讨论的错误是C#环境下的,但对其他达成CLR或使用框架类库(FCL)的语言也相关(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做不到,所以一定要在这两种方法之间比较一下性能。

  #5常见错误:在LINQ语句之中没有考虑底层对象

  对于处理抽象操纵集合任务,LINQ无疑是庞大的。无论他们是在内存的对象,数据库表,或者XML文档。在如此一个完美世界之中,你不需要知道底层对象。然而在这儿的错误是假设我们生活在一个完美世界之中。事实上,相同的LINQ语句能返回不同的结果,当在精确的相同数据上执行时,如果该数据碰巧在一个不同的格式之中。

  例如,请考虑下面的语句:

decimal total=(from accout in myaccouts
where accout.status==‘active”
select accout .Balance).sum();
  想象一下,该对象之一的账号会发生什么。状态等于“有效的”(注意大写A)?

  好吧,如果myaccout是Dbset的对象。(默认设置了不同区分大小写的配置),where表达式仍会匹配该元素。然而,如果myaccout是在内存阵列之中,那么它将不匹配,因此将产生不同的总的结果。

  等一会,在我们之前讨论过的字符串比较中, 我们看见 == 操作符扮演的角色就是简单的比较. 所以,为什么在这个条件下, == 表现出的是另外的一个形式呢 ?

  答案是,当在LINQ语句中的基础对象都引用到SQL表中的数据(如与在这个例子中,在实体框架为DbSet的对象的情况下),该语句被转换成一个T-SQL语句。然后遵循的T-SQL的规则,而不是C#的规则,所以在上述情况下的比较结束是不区分大小写的。

  一般情况下,即使LINQ是一个有益的和一致的方式来查询对象的集合,在现实中你还需要知道你的语句是否会被翻译成什么比C#的引擎或者是其他表达,来确保您的代码的行为将如预期在运行时。

  常见错误 #6:对扩展方法感到困惑或者被它的形式欺骗

  如同先前提到的,LINQ状态依赖于IEnumerable接口的实现对象,比如,下面的简单函数会合计帐户集合中的帐户余额:

public decimal SumAccounts(IEnumerable myAccounts) { return myAccounts.Sum(a => a.Balance);
}
  在上面的代码中,myAccounts参数的类型被声明为IEnumerable,myAccounts引用了一个Sum 方法 (C# 使用类似的 “dot notation” 引用方法或者接口中的类),我们期望在IEnumerable接口中定义一个Sum()方法。但是,IEnumerable没有为Sum方法提供任何引用并且只有如下所示的简洁定义:

public interface IEnumerable : IEnumerable {
IEnumerator GetEnumerator();
}
  但是Sum方法应该定义到何处?C#是强类型的语言,因此如果Sum方法的引用是无效的,C#编译器会对其报错。我们知道它必须存在,但是应该在哪里呢?此外,LINQ提供的供查询和聚集结果所有方法在哪里定义呢?

  答案是Sum并不在IEnumerable接口内定义,而是一个

  定义在System.Linq.Enumerable类中的static方法(叫做“extension method”)

namespace System.Linq {
public static class Enumerable { …
// the reference here to “this IEnumerable source” is
// the magic sauce that provides access to the extension method Sum
public static decimal Sum(this IEnumerable source,
Func

猜你喜欢

转载自blog.csdn.net/yulongguiziyao/article/details/51698147