C# 정보
C#은 Microsoft CLR(공용 언어 런타임)을 구현하는 몇 안 되는 언어 중 하나입니다. CLR을 구현하는 언어는 언어 간 통합, 예외 처리, 보안 강화, 구성 요소 구성을 위한 손쉬운 모델, 디버깅 및 분석 서비스 등 CLR이 제공하는 기능의 이점을 누릴 수 있습니다. 최신 CLR 언어인 C#은 가장 널리 사용되며 해당 응용 프로그램 시나리오는 Windows 데스크톱, 휴대폰, 서버 환경과 같은 복잡하고 전문적인 개발 프로젝트를 대상으로 합니다.
C#은 객체 지향적이고 강력한 형식의 언어입니다. 컴파일 및 런타임 시 C#의 강력한 형식 검사를 통해 가장 일반적인 프로그래밍 오류를 가능한 한 빨리 발견할 수 있으며 위치가 매우 정확합니다. 이는 유형에 집착하지 않고 위반이 발생한 후 오랜 시간이 지나서 추적 가능한 설명할 수 없는 오류만 보고하는 언어에 비해 프로그래머의 시간을 많이 절약할 수 있습니다. 그러나 많은 프로그래머는 의도적으로 또는 의도하지 않게 이 검색의 이점을 무시하고 이로 인해 이 기사에서 논의된 일부 문제가 발생합니다.
이 기사 정보
이 기사에서는 가장 일반적인 10가지 프로그래밍 오류 또는 C# 프로그래머가 피해야 할 함정을 소개합니다.
이 글에서 다룬 오류는 C# 환경에만 국한된 것이지만, CLR을 구현하거나 FCL(Framework Class Library)을 사용하는 다른 언어에도 관련이 있습니다.
일반적인 실수 #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의 값은 변경되지 않습니다. 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#에서는 값 유형이 null일 수 없습니다. 정의에 따르면 값의 유형 값은 초기화된 변수의 값 유형이라도 값을 가져야 합니다. 이를 유형의 기본값이라고 합니다. 이로 인해 변수가 초기화되지 않았는지 확인할 때 다음과 같은 예상치 못한 결과가 발생하는 경우가 많습니다.
class Program { static Point point1; static Pen pen1; static void Main(string[] args) { Console.WriteLine(pen1 == null); // True Console.WriteLine(point1 == null); // False (huh?) } }
왜 [포인트 1]이 비어 있지 않습니까? 대답은 포인트가 값 유형이고 기본 값 포인트(0,0)와 마찬가지로 null 값이 없다는 것입니다. 이것이 C#
많은(전부는 아님) 값 유형에 [IsEmpty] 속성이 있는 매우 간단하고 일반적인 실수라는 점을 인식하지 못함:
Console.WriteLine(point1.IsEmpty); // True
변수가 초기화되었는지 확인할 때 초기화되지 않은 값이 기본적으로 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方法,可以提供字符串的相对顺序信息而不仅仅中测试是否相等。这个方法可以很好适用于<, <=, >和>= 运算符,对上述讨论同样适用。
常见误区 #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参数的类型被声明为IEnumerable
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 외에도 확장 메서드는 Microsoft에서 널리 사용되는 두 가지 다른 클래스 라이브러리인 Unity Application Block 및 Web API 프레임워크에서도 사용되며 그 외 많은 것들이 있습니다. 프레임워크가 최신일수록 확장 메서드를 사용할 가능성이 높아집니다.
물론 자신만의 확장 메서드를 작성할 수도 있습니다. 그러나 확장 메서드가 다른 인스턴스 메서드처럼 호출되는 것처럼 보이지만 실제로는 착각이라는 점을 깨달아야 합니다. 실제로 확장 메서드는 확장 클래스의 비공개 및 보호 멤버에 액세스할 수 없으므로 기존 상속을 대체하는 방법으로 사용할 수 없습니다.
일반적인 실수 #7: 현재 작업에 잘못된 컬렉션 유형 사용
C#은 수많은 컬렉션 유형 개체를 제공하지만 아래에는 그 중 몇 가지만 나열되어 있습니다.
Array,ArrayList,BitArray,BitVector32,Dictionary
하지만 어떤 경우에는 선택 사항이 너무 많으면 선택 사항이 부족한 것만큼이나 나쁘고 컬렉션 유형도 마찬가지입니다. 다양한 옵션을 통해 작업을 원활하게 진행할 수 있습니다. 하지만 필요에 가장 적합한 컬렉션을 선택할 수 있도록 미리 컬렉션 유형을 검색하고 학습하는 데 시간을 투자하는 것이 좋습니다. 이는 궁극적으로 프로그램의 성능을 향상시키고 오류 가능성을 줄여줍니다.
작업 중인 것과 동일한 요소 유형(예: 문자열 또는 비트)을 지정하는 컬렉션이 있는 경우 먼저 사용하는 것이 좋습니다. 이러한 컬렉션은 해당 요소 유형을 지정하면 더 효율적입니다.
C#에서 형식 안전성을 활용하려면 제네릭이 아닌 인터페이스를 사용하는 것보다 제네릭 인터페이스를 사용하는 것이 더 좋습니다. 일반 인터페이스의 요소 유형은 객체를 선언할 때 지정하는 유형인 반면, 제네릭이 아닌 인터페이스의 요소는 객체 유형입니다. 제네릭이 아닌 인터페이스를 사용하는 경우 C# 컴파일러는 코드 형식을 확인할 수 없습니다. 마찬가지로, 네이티브 형식 컬렉션으로 작업할 때 제네릭이 아닌 인터페이스를 사용하면 C#에서 이러한 형식에 대해 박싱 및 박싱 해제 작업을 자주 수행하게 됩니다. 이는 적절한 유형을 지정하는 일반 컬렉션을 사용하는 것에 비해 성능에 상당한 영향을 미칩니다.
또 다른 일반적인 함정은 컬렉션 유형을 직접 구현하는 것입니다. 절대 그렇게 하지 말라는 뜻은 아닙니다. 바퀴를 다시 만드는 대신 .NET에서 제공하는 널리 사용되는 일부 컬렉션 유형을 사용하거나 확장하면 많은 시간을 절약할 수 있습니다. 특히 C#의 C5 일반 컬렉션 라이브러리 및 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#的一些关键的细微之处,像本文中所提到的那些(但不限于这些),可以帮助我们更好的去使用语言,从而避免一些常见的陷阱。