Home >Backend Development >C#.Net Tutorial >The 10 most common mistakes C# programmers make

The 10 most common mistakes C# programmers make

伊谢尔伦
伊谢尔伦Original
2016-11-24 13:15:001133browse

About C#

 C# is one of the few languages ​​​​that has reached the Microsoft Common Language Runtime (CLR). Languages ​​that implement the CLR can benefit from the features it brings, such as cross-language integration, exception handling, security enhancements, easy models for component composition, and debugging and analysis services. As a modern CLR language, C# is the most widely used, and its application scenarios are targeted at complex and professional development projects such as Windows desktops, mobile phones, and server environments.

  C# is an object-oriented, strongly typed language. C#'s strong type checking at compile and run time enables most typical programming errors to be discovered as early as possible, and the location is quite precise. This can save programmers a lot of time compared to languages ​​that don't stick to types and only report traceable inexplicable errors long after the violation occurs. However, many programmers intentionally or unintentionally discard the advantages of this detection, which leads to some of the problems discussed in this article.

About this article

This article introduces the 10 most common programming errors, or traps that C# programmers should avoid.

Although the error discussed in this article is specific to the C# environment, it is also relevant to other languages ​​that implement the CLR or use the Framework Class Library (FCL).

Common Mistake #1: Using References Like Values ​​or Over

Programmers in C++ and many other languages ​​are accustomed to controlling whether the value they assign to a variable is a simple value or a reference to an existing object. In C#, this would be determined by the programmer who wrote the object, not by the programmer who instantiated the object and assigned variables to it. This is a common "problem" among novice C# programmers.

  If you don’t know whether the object you are working with is a value type or a reference type, you may encounter some surprises. For example:

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

As you can see, although the Point and Pen objects are created in the same way, when a new X coordinate value is assigned to point2, the value of point1 remains unchanged. When a new color value is assigned to pen2, pen1 also changes accordingly. Therefore, we can infer that point1 and point2 each contain their own copy of the Point object, while pen1 and pen2 refer to the same Pen object. Without this test, how can we know this principle?

One way is to look at how the object is defined (in Visual Studio, you can put the cursor on the name of the object and press the F12 key)

 public struct Point { … }     // defines a “value” type
  public class Pen { … }        // defines a “reference” type

As shown above, in C#, the struct keyword is used to define a value type, and the class keyword is used to define a reference type. For those with a C++ programming background, this behavior may be surprising if they are confused by some of the similar keywords between C++ and C#.

  If you want to rely on behavior that differs between value types and reference types, for example, if you want to pass an object as a parameter to a method and modify the state of the object in this method. You must make sure you are dealing with the correct type of object.

 Common mistake #2: Misunderstanding the default value of uninitialized variables

 In C#, value types cannot be null. By definition, a value's type value, even the value type of an initialized variable, must have a value. This is called the default value of the type. This often leads to the following, unexpected results when checking whether a variable is uninitialized:

class Program {
      static Point point1;      static Pen pen1;      static void Main(string[] args) {
          Console.WriteLine(pen1 == null);      // True
          Console.WriteLine(point1 == null);    // False (huh?)
      }
  }

Why is [point 1] not empty? The answer is that point is a value type, and like the default value point (0,0), there is no null value. Failure to realize that this is a very simple and common mistake in C#

Many (but not all) value types have an [IsEmpty] property, you can see if it is equal to the default value:

Console.WriteLine(point1.IsEmpty);        // True

When you check an Whether the variable has been initialized, make sure you know that the value uninitialized is the type of the variable and will be, by default, not null.

  常见错误 #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方法,可以提供字符串的相对顺序信息而不仅仅中测试是否相等。这个方法可以很好适用于be218b54e7c452811b5d72906a4b8a5d和>= 运算符,对上述讨论同样适用。

  常见误区 #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<Account> myAccounts) {      return myAccounts.Sum(a => a.Balance);
  }

 在上面的代码中,myAccounts参数的类型被声明为IEnumerable5788b468e1dd6be073fc218202eafec6,myAccounts引用了一个Sum 方法 (C# 使用类似的 “dot notation” 引用方法或者接口中的类),我们期望在IEnumerable8742468051c85b06f0a0af9e3e506b5c接口中定义一个Sum()方法。但是,IEnumerable8742468051c85b06f0a0af9e3e506b5c没有为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修饰符。这就是编译器知道它是一个扩展方法的“奥妙”。它所修饰的参数的类型(这个例子中的IEnumerable767cb18821eb42d0be2dbae71b5cc905)说明这个类或者接口将显得实现了这个方法。

  (另外需要指出的是,定义扩展方法的IEnumerable接口和Enumerable类的名字间的相似性没什么奇怪的。这种相似性只是随意的风格选择。)

  理解了这一点,我们可以看到上面介绍的sumAccounts方法能以下面的方式实现:

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

 事实上我们可能已经这样实现了这个方法,而不是问什么要有扩展方法。扩展方法本身只是C#的一个方便你无需继承、重新编译或者修改原始代码就可以给已存的在类型“添加”方法的方式。

  扩展方法通过在文件开头添加using [namespace];引入到作用域。你需要知道你要找的扩展方法所在的名字空间。如果你知道你要找的是什么,这点很容易。

  当C#编译器碰到一个对象的实例调用了一个方法,并且它在这个对象的类中找不到那个方法,它就会尝试在作用域中所有的扩展方法里找一个匹配所要求的类和方法签名的。如果找到了,它就把实例的引用当做第一个参数传给那个扩展方法,然后如果有其它参数的话,再把它们依次传入扩展方法。(如果C#编译器没有在作用域中找到相应的扩展方法,它会抛措。)

For the C# compiler, extension methods are "syntax sugar" that allows us to write code more clearly and easier to maintain (in most cases). Obviously, this assumes you know how to use it, otherwise it can be confusing, especially at first.

Applying extension methods does have advantages, but it can also cause headaches and waste time for developers who don’t understand it or have incorrect understanding of it. Especially when looking at online sample code, or other already written code. When such code generates a compilation error (because it calls methods that are obviously not defined in the type being called), the general tendency is to consider whether the code applies to other versions of the referenced library, or even different libraries. A lot of time will be spent looking for new versions, or libraries that are considered "lost".

Even developers who are familiar with extension methods occasionally make the above mistake when the name of the extension method is the same as the name of the method defined in the class, with only slight differences in the method signature. A lot of time is spent looking for misspellings that “do not exist”.

In C#, using extension methods is becoming more and more popular. In addition to LINQ, extension methods are also used in two other widely used class libraries from Microsoft, Unity Application Block and Web API framework, and there are many others. The newer the framework, the more likely it is to use extension methods.

Of course, you can also write your own extension methods. But you must realize that although extension methods appear to be called like other instance methods, this is actually an illusion. In fact, extension methods cannot access private and protected members of the extended class, so they cannot be used as a replacement for traditional inheritance.

Common Mistake #7: Using the Wrong Collection Type for the Task at Hand

C# provides a large number of collection type objects, only a few of them are listed below:

Array, ArrayList, BitArray, BitVector32, Dictionary5e92d5e6312a40dda277adb46254ebf0,HashTable,HybridDictionary,List8742468051c85b06f0a0af9e3e506b5c,NameValueCollection,OrderedDictionary,Queue, Queue8742468051c85b06f0a0af9e3e506b5c,SortedList,Stack, Stack8742468051c85b06f0a0af9e3e506b5c,StringCollection,StringDictionary.

 But in some cases, there are too many Choice is just as bad as not having enough choices, and the same goes for collection types. The vast number of options is sure to keep your job running smoothly. But you're better off spending some time searching and learning about collection types ahead of time so you can choose one that best suits your needs. This ultimately makes your program perform better and make errors less likely.

If there is a collection that specifies the element type (such as string or bit) that is the same as the one you are operating on, you'd better choose to use it first. Such collections are more efficient when the corresponding element types are specified.

To take advantage of type safety in C#, you are better off using a generic interface rather than using a non-generic interface. The element type in a generic interface is the type you specify when declaring the object, while the elements in a non-generic interface are of object type. When using a non-generic interface, the C# compiler cannot type-check your code. Likewise, when you are working with collections of native types, using non-generic interfaces will cause C# to perform frequent boxing and unboxing operations on these types. This has a significant performance impact compared to using a generic collection that specifies the appropriate type.

Another common pitfall is implementing a collection type yourself. That's not to say never do it, you can save a lot of time by using or extending some of the widely used collection types provided by .NET, rather than reinventing the wheel. In particular, C#'s C5 Generic Collection Library and CLI provide many additional collection types, like persistent tree data structures, heap-based priority queues, hash-indexed array lists, linked lists, etc. and many more.

 Common Mistake #8: Missing Resource Release

 The CLR managed environment plays the role of a garbage collector, so you do not need to explicitly release the memory occupied by created objects. In fact, you can't release it explicitly either. There is no operator in C# that corresponds to C++ delete or a method that corresponds to the free() function in C language. But that doesn't mean you can ignore all used objects. Many object types encapsulate many other types of system resources (for example, disk files, data connections, network ports, etc.). Keeping these resources in use can dramatically drain system resources, impair performance, and ultimately cause program errors.

  尽管所有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#的一些关键的细微之处,像本文中所提到的那些(但不限于这些),可以帮助我们更好的去使用语言,从而避免一些常见的陷阱。


Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn