C# 程序员最常犯的 10 个谬误http://www.oschina.net/translate/top-10-mistakes-that-c-sharp-programmers-make

来源:http://www.oschina.net/translate/top-10-mistakes-that-c-sharp-programmers-make

关于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做不到,所以肯定要在那两种办法之间相比较一下属性。


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

对此拍卖抽象操纵集合职分,LINQ无疑是巨大的。无论他们是在内存的目的,数据库表,或者XML文档。在如此一个周到世界中间,你不须要了然底层对象。然则一旦我们生存在一个到家世界中间可能会推动错误。事实上,当在精确的一模一样数量上执行时,尽管该多少碰巧在一个不一的格式之中相同的,LINQ语句能回到不一样的结果

诸如,请考虑上边的言语:

decimal total=(from accout in myaccouts
    where accout.status=='active'
    select accout.Balance).sum();

设想一下,该目标之一的账号会发出如何。状态很是“Active”(注意大写A)?

好呢,要是myaccout是Dbset的靶子。(默许设置了分歧界别轻重缓急写的配置),where表达式仍会合作该因素。不过,要是myaccout是在内存阵列之中,那么它将不匹配,由此将生出不一致的总的结果。

等一会,在我们前边钻探过的字符串对比中, 大家看见 ==
操作符扮演的角色就是简约的可比. 所以,为啥在这几个规则下, ==
表现出的是其它的一个花样呢 ?

答案是,当在LINQ语句中的基础对象都引用到SQL表中的数据(如与在那个事例中,在实体框架为DbSet的靶子的情事下),该语句被转换成一个T-SQL语句。然后依据的T-SQL的规则,而不是C#的平整,所以在上述意况下的相比较截至是不区分轻重缓急写的。

诚如情形下,就算LINQ是一个有利的和同等的艺术来查询对象的成团,在具体中你还索要了解你的言语是还是不是会被翻译成什么,来确保您的代码的行为将如预期运行。


广大错误 #6:对扩充方法感到迷惑不解或者被它的款式诈骗

就像先前波及的,LINQ状态保护于IEnumerable接口的兑现目的,比如,上面的大概函数会协商帐户集合中的帐户余额:

public decimal SumAccounts(IEnumerable<Account> myAccounts) 
{      
   return myAccounts.Sum(a => a.Balance);
}

在上头的代码中,myAccounts参数的项目被声称为IEnumerable<Account>,myAccounts引用了一个Sum
方法 (C# 使用类似的 “dot notation”
引用方法或者接口中的类),大家期待在IEnumerable接口中定义一个Sum()方法。不过,IEnumerable没有为Sum方法提供任何引用并且唯有如下所示的精简定义:

 public interface IEnumerable<out T> : IEnumerable 
 {
      IEnumerator<T> 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<TSource> source” is
      // the magic sauce that provides access to the extension method Sum
      public static decimal Sum<TSource>(this IEnumerable<TSource> source,Func<TSource, decimal> selector);      ...
    }
 }

扩充方法的显明特点是首个形参前的this修饰符。那就是编译器知道它是一个恢弘方法的“奥妙”。它所修饰的参数的体系(那么些事例中的IEnumerable)表达那几个类依然接口将显示完毕了那些方法。

(其它需要指出的是,定义伸张方法的IEnumerable接口和Enumerable类的名字间的相似性没什么奇怪的。那种相似性只是随意的品格选拔。)

精晓了那或多或少,大家能够看来上面介绍的sumAccounts方法能以上边的法门贯彻:

  public decimal SumAccounts(IEnumerable<Account> myAccounts) 
  {
      return Enumerable.Sum(myAccounts, a => a.Balance);
  }

骨子里大家或许早就这么已毕了这一个方式,而不是问怎么要有伸张方法。扩张方法本身只是C#的一个方便你无需后续、重新编译或者涂改原始代码就足以给已存的在品种“添加”方法的法门。

增加方法通过在文书最先添加using
[namespace];引入到功用域。你需求了解您要找的扩大方法所在的名字空间。要是你通晓你要找的是哪些,那一点很简单。

当C#编译器碰着一个目的的实例调用了一个办法,并且它在那些目标的类中找不到那些格局,它就会尝试在功能域中具备的恢宏方法里找一个匹配所需求的类和格局签名的。若是找到了,它就把实例的引用当做第三个参数传给那些伸张方法,然后一旦有任何参数的话,再把它们依次传入扩大方法。(如若C#编译器没有在功能域中找到相应的壮大方法,它会抛措。)

对C#编译器来说,扩张方法是个“语法糖”,使大家能把代码写得更清楚,更易于维护(多数场地下)。显著,前提是你领会它的用法,否则,它会相比较便于令人迷惑,越发是一先导。

使用增加方法确实有优势,但也会让那么些对它不打听如故认识不正确的开发者胸闷,浪费时间。尤其是在看在线示例代码,或者别的已经写好的代码的时候。当这么些代码爆发编译错误(因为它调用了那么些明明没在被调用类型中定义的不二法门),一般的倾向是考虑代码是还是不是选用于所引述类库的任何版本,甚至是分歧的类库。很多年华会被花在找新本子,或者被认为“丢失”的类库上。

在扩张方法的名字和类中定义的不二法门的名字同样,只是在方式签名上有微小分歧的时候,甚至那一个熟识扩展方法的开发者也偶尔犯上面的一无所能。很多时间会被花在搜索“不存在”的拼写错误上。

在C#中,用增添方法变得越发流行。除了LINQ,在其它四个出自微软今日被大面积使用的类库Unity
Application Block和Web API
framework中,也应用了扩张方法,而且还有很多任何的。框架越新,用伸张方法的可能性越大。

理所当然,你也可以写你协调的扩展方法。不过必须意识到纵然伸张方法看起来和别的实例方法一致被调用,但那实质上只是幻觉。事实上,增加方法无法访问所扩充类的私房和维护成员,所以它不可以被用作传统接二连三的替代品。


科普错误 #7: 对手头上的职分选拔不当的聚集类型

C#提供了大量的聚众类型的对象,上面只列出了其中的一有的:

Array,ArrayList,BitArray,BitVector32,Dictionary<K,V>,HashTable,HybridDictionary,List<T>,NameValueCollection,OrderedDictionary,Queue, Queue<T>,SortedList,Stack, Stack<T>,StringCollection,StringDictionary.

但是在有点景况下,有太多的精选和没有丰裕的选取一样不好,集合类型也是那样。数量众多的抉择余地肯定可以确保是您的做事健康运作。不过你最好或者花一些年华提前查找并问询一下会见类型,以便拔取一个最适合你须要的集合类型。那最后会使你的次序品质更好,裁减失误的或许。

一经有一个会师指定的因素类型(如string或bit)和您正在操作的同样,你最好优先挑选使用它。当指定相应的因素类型时,这种集合的作用更高。

为了利用好C#中的类型安全,你最好选取使用一个泛型接口,而不是选拔非泛型的假说。泛型接口中的元素类型是您在在表明对象时指定的序列,而非泛型中的元素是object类型。当使用一个非泛型的接口时,C#的编译器不可以对您的代码举行项目检查。同样,当你在操作原生类型的汇集时,使用非泛型的接口会导致C#对这个品种进行很多次的装箱(boxing)和拆箱(unboxing)操作。和行使指定了适度类型的泛型集合比较,那会带来很鲜明的品质影响。

另一个广大的圈套是投机去落到实处一个汇聚类型。那并不是说永远不要这么做,你可以透过动用或扩大.NET提供的一对被大面积采纳的成团类型来节省大批量的小时,而不是去重新造轮子。
尤其是,C#的C5 Generic Collection Library
和CLI提供了好多额外的会聚类型,像持久化树形数据结构,基于堆的先期级队列,哈希索引的数组列表,链表等以及越来越多。


大面积错误#8:遗漏资源自由

CLR
托管环境扮演了垃圾堆回收器的角色,所以你不要求显式释放已创制对象所占用的内存。事实上,你也无法显式释放。C#中并未与C++
delete对应的运算符或者与C语言中free()函数对应的点子。但那并不代表你可以忽略所有的施用过的目的。许多目的类型封装了好多别样门类的系统资源(例如,磁盘文件,数据连接,网络端口等等)。保持这一个资源选用状态会猛烈耗尽系统的资源,削弱质量并且最终促成程序出错。

即使所有C#的类中都定义了析构方法,但是销毁对象(C#中也称为终结器)可能存在的标题是你不确定它们在时候被调用。他们在未来一个不确定的日子被垃圾回收器调用(一个异步的线程,此举可能引发额外的产出)。试图避免那种由垃圾回收器中GC.Collect()方法所施加的威迫限制并非一种好的编程实践,因为可能在垃圾堆回收线程试图回收适宜回收的对象时,在不足预言的小时内造成线程阻塞。

那并表示最好不要用终结器,显式释放资源并不会促成其中的其他一个结局。当您打开一个文件、网络端口或者数额连接时,当你不再动用那一个资源时,你应有尽快的显式释放这一个资源。

资源走漏大致在享有的条件中都会掀起关心。不过,C#提供了一种健康的机制使资源的选拔变得简单。倘使合理选用,可以大大裁减败露出现的机率。NET
framework定义了一个IDisposable接口,仅由一个Dispose()构成。任何完成IDisposable的接口的目的都会在目标生命周期停止调用Dispose()方法。调用结果肯定而且决定性的假释占用的资源。

假诺在一个代码段中开创并释放一个对象,却忘记调用Dispose()方法,那是不可原谅的,因而C#提供了using语句以管教无论代码以怎么着的主意退出,Dispose()方法都会被调用(不管是相当,return语句,或者不难的代码段截止)。那些using和事先涉嫌的在文件初阶用来引入名字空间的同一。它有其余一个众多C#开发者都不曾察觉的,完全不相干的目的,也就是承保代码退出时,对象的Dispose()方法被调用:

  using (FileStream myFile = File.OpenRead("foo.txt")) {
    myFile.Read(buffer, 0, 100);
  }

在上头示例中应用using语句,你就可以规定myFile.Dispose()方法会在文书使用完之后被随即调用,不管Read()方法有没有抛格外。


广泛错误 #9: 回避卓殊

C#在运转时也会强制进行项目检查。相对于像C++那样会给错误的类型转换赋一个随机值的言语来说,C#那能够使你更快的找到出错的任务。然则,程序员再一遍无视了C#的这一特点。由于C#提供了两体系型检查的方法,一种会抛出极度,而另一种则不会,那很可能会使他们掉进那么些“坑”里。有些程序员倾向于回避万分,并且认为不写
try/catch 语句可以节约一些代码。

例如,下边演示了C#中展开浮现类型转换的二种不相同的不二法门:

  // 方法 1:
  // 如果 account 不能转换成 SavingAccount 会抛出异常
  SavingsAccount savingsAccount = (SavingsAccount)account;

  // 方法 2:
  // 如果不能转换,则不会抛出异常,相反,它会返回 null
  SavingsAccount savingsAccount = account as SavingsAccount;

很强烈,即便不对方法2回来的结果进行判断的话,最后很可能会发出一个
NullReferenceException
的要命,这恐怕会并发在稍晚些的时候,那使得难点更难追踪。相比较的话,方法1会即时抛出一个
InvalidCastExceptionmaking,那样,难题的源于就很明朗了。

其它,即便你知道要对方法2的重临值进行判断,倘若你意识值为空,接下去你会怎么办?在这一个点子中告知错误合适呢?假若类型转换战败了您还有别的的措施去品味吧?如若没有的话,那么抛出那些那么些是绝无仅有正确的精选,并且足够的抛出点离其发出点越近越好。

上面的事例演示了其余一组广泛的主意,一种会抛出尤其,而另一种则不会:

  int.Parse();     // 如果参数无法解析会抛出异常
  int.TryParse();  // 返回bool值表示解析是否成功

  IEnumerable.First();           // 如果序列为空,则抛出异常
  IEnumerable.FirstOrDefault();  // 如果序列为空则返回 null 或默认值

稍稍程序员认为“很是有害”,所以她们听之任之的以为不抛出分外的程序显得越来越“高大上”。就算在一些意况下,那种意见是毋庸置疑的,可是那种观点并不适用于所有的景色。

举个实际的事例,某些景况下当非凡发生时,你有另一个可选的不二法门(如,默许值),那么,接纳不抛出特其余格局是一个相比好的挑三拣四。在那种景色下,你最接近上边那样写:

  if (int.TryParse(myString, out myInt)) 
  {
      // use myInt
  }
  else 
  {    
      // use default value
  }

而不是如此:

  try 
  {
    myInt = int.Parse(myString);    // use myInt
  }
  catch (FormatException) 
    // use default value
  }

但是,这并不表明 TryParse
方法更好。某些意况下适合,某些意况下则不切合。那就是干什么有三种方法供大家选择了。根据你的具体景况选取适合的方法,并记住,作为一个开发者,相当是一心可以变成您的情人的。


周边错误 #10: 累积编译器警告而不处理

以此颠倒是非并不是C#所特有的,然则在C#中那种场所却相比较多,越发是从C#编译器弃用了残暴的项目检查之后。

警示的产出是有来头的。所有C#的编译错误都标志你的代码有毛病,同样,一些警告也是那样。那两者之间的分裂在于,对于警告的话,编译器可以按照你代码的指令工作,可是,编译器发现你的代码有一点小标题,很有可能会使你的代码不可能依据你的预料运行。

一个广大的例证是,你改改了您的代码,并移除了对少数变量的行使,但是,你忘了移除该变量的表明。程序可以很好的运行,不过编译器会唤起有未利用的变量。程序可以很好的周转使得一些程序员不去修复警告。更有甚者,有些程序员很好的施用了Visual
Studio中“错误列表”窗口的隐藏警告的效率,很不难的就把警告过滤了,以便专注于错误。不用多久,就会积累一堆警告,这一个警告都被“惬意”的疏忽了(更糟的是,隐藏掉了)。

而是,如若您忽视掉这一类的告诫,类似于上面那个事例迟早会现出在您的代码中。

  class Account 
  {

      int myId;      
      int Id;   // 编译器已经警告过了,但是你不听

      // Constructor
      {          
         this.myId = Id;     // OOPS!
      }

  }

再加上使用了编辑器的智能感知的功效,那种张冠李戴就很有可能暴发。

现行,你的代码中有了一个严重的谬误(可是编译器只是出口了一个告诫,其原因已经表达过),那会浪费你大量的小时去探寻那错误,具体意况由你的次序复杂程度决定。若是您一初阶就留心到了这几个警示,你只必要5分钟就可以修改掉,从而幸免那个题材。

切记,要是您精心看的话,你会意识,C#编译器给了您不少关于你程序健壮性的管事的新闻。不要忽视警告。你只需花几秒钟的年华就足以修复它们,当出现的时候就去修补它,那可以为你节省不可枚举光阴。试着为祥和创设一种“洁癖”,让Visual
Studio 的“错误窗口”一贯突显“0荒谬,
0警告”,一旦出现警示就感觉到不佳受,然后随即把警告修复掉。

当然了,任何规则都有例外。所以,有些时候,就算你的代码在编译器看来是稍稍难题的,不过那正是你想要的。在那种很少见的事态下,你最好使用
#pragma warning disable [warning id]
把吸引警告的代码包裹起来,而且只包裹警告ID对应的代码。那会且只会幸免对应的警示,所以当有新的警示爆发的时候,你仍旧会明白的。.
总结

C#,C#是一门强大的同时很灵敏的言语,它有诸多机制和言语专业来明确的滋长你的生产力。和此外语言一样,就算对它能力的询问有限,那很可能会给您带来阻碍,而不是补益。正如一句谚语所说的那样“knowing
enough to be
dangerous”(译者注:意思是自以为已经掌握丰硕了,可以做某事了,但实际不是)。

熟悉C#的部分要害的细小之处,像本文中所提到的那几个(但不限于那几个),可以支持大家更好的去行使语言,从而防止有些大规模的圈套。

相关文章