设为首页收藏本站

LUPA开源社区

 找回密码
 注册
文章 帖子 博客
LUPA开源社区 首页 业界资讯 技术文摘 查看内容

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

2014-5-13 11:35| 发布者: 红黑魂| 查看: 4586| 评论: 0|来自: 开源中国编译

摘要: 本文描述了10个 C# 程序员常犯的错误,或应该避免的陷阱。尽管本文讨论的大多数错误是针对 C# 的,有些错误与其他以 CLR 为目标的语言,或者用到了 Framework Class Library (FCL) 的语言也相关。 ...

常见错误 #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
      Account(int id) {          this.myId = Id;     // OOPS!
      }
  
  }

再加上使用了编辑器的智能感知的功能,这种错误就很有可能发生。

现在,你的代码中有了一个严重的错误(但是编译器只是输出了一个警告,其原因已经解释过),这会浪费你大量的时间去查找这错误,具体情况由你的程序复杂程度决定。如果你一开始就注意到了这个警告,你只需要5秒钟就可以修改掉,从而避免这个问题。

记住,如果你仔细看的话,你会发现,C#编译器给了你很多关于你程序健壮性的有用的信息。不要忽略警告。你只需花几秒钟的时间就可以修复它们,当出现的时候就去修复它,这可以为你节省很多时间。试着为自己培养一种“洁癖”,让Visual Studio 的“错误窗口”一直显示“0错误, 0警告”,一旦出现警告就感觉不舒服,然后即刻把警告修复掉。


当然了,任何规则都有例外。所以,有些时候,虽然你的代码在编译器看来是有点问题的,但是这正是你想要的。在这种很少见的情况下,你最好使用 #pragma warning disable [warning id] 把引发警告的代码包裹起来,而且只包裹警告ID对应的代码。这会且只会压制对应的警告,所以当有新的警告产生的时候,你还是会知道的。.

总结

C#是一门强大的并且很灵活的语言,它有很多机制和语言规范来显著的提高你的生产力。和其他语言一样,如果对它能力的了解有限,这很可能会给你带来阻碍,而不是好处。正如一句谚语所说的那样“knowing enough to be dangerous”(译者注:意思是自以为已经了解足够了,可以做某事了,但其实不是)。

熟悉C#的一些关键的细微之处,像本文中所提到的那些(但不限于这些),可以帮助我们更好的去使用语言,从而避免一些常见的陷阱。


开源中国编译:http://www.oschina.net/translate/top-10-mistakes-that-c-sharp-programmers-make

英文原文:Top 10 Mistakes that C# Programmers Make



酷毙

雷人

鲜花

鸡蛋

漂亮
  • 快毕业了,没工作经验,
    找份工作好难啊?
    赶紧去人才芯片公司磨练吧!!

最新评论

关于LUPA|人才芯片工程|人才招聘|LUPA认证|LUPA教育|LUPA开源社区 ( 浙B2-20090187 浙公网安备 33010602006705号   

返回顶部