C# 개발자가 알아야 할 13가지
프로그램 버그와 결함은 개발 과정에서 자주 나타납니다. 도구를 잘 활용하면 프로그램을 출시하기 전에 문제를 발견하거나 방지하는 데 도움이 될 수 있습니다.
표준화된 코드 작성을 통해 코드를 더 쉽게 유지 관리할 수 있으며, 특히 여러 개발자나 팀이 코드를 개발하고 유지 관리하는 경우 이러한 장점은 더욱 두드러집니다. 코드 표준화를 강제하는 일반적인 도구에는 FxCop, StyleCop 및 ReSharper가 있습니다.
개발자 메모: 오류를 덮기 전에 신중하게 생각하고 결과를 분석하세요. 결과가 실제와 크게 다를 수 있으므로 코드에서 오류를 찾기 위해 이러한 도구에 의존하지 마십시오.
코드 검토 및 파트너 프로그래밍은 개발자가 다른 사람이 작성한 코드를 의도적으로 검토하는 일반적인 연습입니다. 다른 사람들은 코딩 오류나 실행 오류와 같은 코드 개발자 측의 버그를 찾고 싶어합니다.
코드 검토는 수동 작업에 의존하기 때문에 정량화하기 어렵고 정확성이 만족스럽지 못한 귀중한 작업입니다.
정적 분석에서는 코드의 일부 불규칙성이나 결함 존재를 찾기 위해 테스트 케이스를 작성할 필요가 없습니다. 이는 문제를 찾는 매우 효과적인 방법이지만 오탐지가 너무 많지 않은 도구가 필요합니다. C#에 일반적으로 사용되는 정적 분석 도구로는 Coverity, CAT, NET 및 Visual Studio Code Analysis가 있습니다.
동적 분석 도구는 코드를 실행할 때 보안 취약점, 성능 및 동시성 문제 등의 오류를 찾는 데 도움이 됩니다. 이 접근 방식은 실행 시간 컨텍스트에서 분석을 수행하므로 코드 복잡성으로 인해 효율성이 제한됩니다. Visual Studio는 동시성 시각화 도우미, IntelliTrace 및 프로파일링 도구를 비롯한 다양한 동적 분석 도구를 제공합니다.
관리자/팀장 명언: 개발 관행은 일반적인 함정을 피하는 가장 좋은 방법입니다. 또한 테스트 도구가 귀하의 요구 사항을 충족하는지 주의 깊게 살펴보세요. 팀의 코드 진단 수준을 통제하도록 노력하세요.
테스트 방법에는 단위 테스트, 시스템 통합 테스트, 성능 테스트, 침투 테스트 등 여러 가지가 있습니다. 개발 단계에서는 프로그램이 요구 사항을 충족할 수 있도록 대부분의 테스트 사례가 개발자나 테스터에 의해 작성됩니다.
테스트는 올바른 코드를 실행하는 경우에만 효과적입니다. 기능 테스트를 수행할 때 개발자의 개발 및 유지 관리 속도에 도전하는 데에도 사용할 수 있습니다.
도구 선택에 더 많은 시간을 투자하고, 올바른 도구를 사용하여 관심 있는 문제를 해결하고, 개발자에게 추가 작업을 추가하지 마세요. 문제를 찾기 위해 분석 도구와 테스트가 자동으로 원활하게 실행되도록 하되 코드에 대한 아이디어가 개발자의 마음 속에 명확하게 남아 있는지 확인하십시오.
진단된 문제의 위치를 가능한 한 빨리 찾으십시오(컴파일 경고, 표준 위반, 문제 감지 등과 같은 정적 분석 또는 테스트를 통해 얻은 오류인지 여부). 방금 발생한 문제를 "신경 쓰지 않는다"고 무시하고 나중에 발견하기 어려워지면 코드 검토 작업자에게 많은 작업량을 추가하게 되며, 이로 인해 짜증을 내지 않도록 기도해야 합니다. .
코드의 품질, 보안 및 유지 관리 가능성을 향상시키는 동시에 개발자의 R&D 역량, 조정 기능 및 릴리스된 코드의 예측 가능성을 향상시키기 위해 이러한 유용한 제안을 수락하십시오.
目标 | 工具 | 影响 |
一致性,可维护性 | 标准化代码书写,静态分析,代码审查 | 间距一致,命名标准,良好的可读格式,都会让开发者更易编写与维护代码。 |
准确性 | 代码审查,静态分析,动态分析,测试 | 代码不只是需要语法正确,还需要以开发者的思想来满足软件需求。 |
功能性 | 测试 | 测试可以验证大多数的需求是否得到满足:正确性,可拓展性,鲁棒性以及安全性。 |
安全性 | 标准化代码书写,代码审查,静态分析,动态分析,测试 | 安全性是一个复杂的问题,任何一个小的漏洞都是潜在的威胁。 |
开发者研发能力 | 标准化代码书写,静态分析,测试 | 开发者在工具的帮助下会很快速地更正错误。 |
发布可预测性 | 标准化代码书写,代码审查,静态分析,动态分析,测试 | 流线型后期阶段的活动、最小化错误定位循环,都可以让问题发现的更早。 |
C#의 주요 장점 중 하나는 유연한 유형 시스템이며, 안전한 유형은 오류를 더 일찍 찾는 데 도움이 됩니다. 엄격한 유형 규칙을 적용함으로써 컴파일러는 좋은 코딩 습관을 유지하는 데 도움을 줄 수 있습니다. 이와 관련하여 C# 언어와 .NET 프레임워크는 대부분의 요구 사항을 충족할 수 있는 다양한 유형을 제공합니다. 많은 개발자가 일반적인 유형을 잘 이해하고 사용자 요구 사항을 알고 있지만 일부 오해와 오용은 여전히 존재합니다.
.NTE 프레임워크 클래스 라이브러리에 대한 자세한 내용은 MSDN 라이브러리를 참조하세요.
특정 인터페이스에는 일반적인 C# 기능이 포함됩니다. 예를 들어 IDiposable을 사용하면 "using"이라는 키워드와 같은 공통 리소스 관리 언어를 사용할 수 있습니다. 인터페이스를 잘 이해하면 C# 코드를 원활하게 작성하고 유지 관리가 더 쉬워집니다.
ICloneable 인터페이스를 사용하지 마세요. 개발자는 복사된 개체가 전체 복사본인지 얕은 복사본인지 알 수 없습니다. 객체를 복사하는 작업이 올바른지 판단할 수 있는 표준적인 방법이 아직 없기 때문에 인터페이스를 계약으로 의미 있게 사용할 수 있는 방법은 없습니다.
혼란을 방지하기 위해 구조체에 쓰는 것을 피하고 불변 객체로 취급하십시오. 멀티스레딩과 같은 시나리오에서 메모리 공유가 더욱 안전해집니다. 구조에 대해 취하는 접근 방식은 구조가 생성될 때 구조를 초기화하는 것입니다. 데이터를 변경해야 하는 경우 새 엔터티를 생성하는 것이 좋습니다.
어떤 표준 유형/메서드가 변경 불가능하고 새 값(예: 문자열, 날짜)을 반환할 수 있는지 올바르게 이해하고 이를 사용하여 변경 가능한 개체(예: List.Enumerator)를 대체합니다.
문자열의 값은 비어 있을 수 있으므로 적절한 경우 몇 가지 편리한 기능을 사용할 수 있습니다. 값 판단(s.Length==0) 시 NullReferenceException 오류가 발생할 수 있지만 String.IsNullOrEmpty(s) 및 String.IsNullOrWhitespace(s)는 null과 잘 작동할 수 있습니다.
열거형과 상수를 사용하면 코드를 더 쉽게 읽을 수 있으며, 매직 넘버를 식별자로 대체하여 값의 의미를 표현할 수 있습니다.
많은 수의 열거형 유형을 생성해야 하는 경우 태그가 지정된 열거형 유형이 더 간단한 옵션입니다.
[Flag] public enum Tag { None =0x0, Tip =0x1, Example=0x2 }
이 방법을 사용하면 한 조각에 여러 태그를 사용할 수 있습니다:
snippet.Tag = Tag.Tip | Tag.Example
이 방법은 데이터 캡슐화에 도움이 되므로 Tag 속성 getter를 사용할 때 내부 컬렉션 정보 유출에 대해 걱정할 필요가 없습니다.
평등에는 두 가지 유형이 있습니다.
1. 참조 동일성, 즉 두 참조가 모두 동일한 객체를 가리킵니다.
2. 수치적 동등성, 즉 서로 다른 두 참조 개체가 동일한 것으로 간주될 수 있습니다.
또한 C#에서는 다양한 동등성 테스트 방법도 제공합니다. 가장 일반적인 방법은 다음과 같습니다.
== 및 != 연산
객체별 가상 상속 등가 방법
정적 객체.동등 메서드
IEquatable
정적 Object.ReferenceEquals 메서드
때로는 참조 또는 값 동등성을 사용하는 목적을 파악하기 어려울 수 있습니다. 이에 대해 자세히 알아보고 작업을 개선하려면 다음을 참조하세요.
MSDNhttp://msdn.microsoft.com/en-us/library/dd183752.aspx
무언가를 덮어쓰려면 IEquatable
유형이 지정되지 않은 컨테이너가 오버로드에 미치는 영향에 주의하고 "myArrayList[0] == myString" 메서드 사용을 고려하세요. 배열 요소는 컴파일 타임 유형의 "객체"이므로 참조 동일성이 작동합니다. C#에서는 이러한 잠재적인 오류에 대해 경고하지만 컴파일 프로세스 중에 예상치 못한 참조 동일성은 경우에 따라 경고되지 않습니다.
클래스는 데이터를 제대로 관리하는 데 큰 역할을 합니다. 성능상의 이유로 클래스는 항상 부분적인 결과를 캐시하거나 내부 데이터의 일관성에 대해 몇 가지 가정을 합니다. 데이터 권한을 공개하면 어느 정도 캐시하거나 가정해야 하며, 이러한 작업은 성능, 보안 및 동시성에 대한 잠재적인 영향을 통해 나타납니다. 예를 들어 일반 컬렉션 및 배열과 같은 변경 가능한 멤버를 노출하면 사용자가 건너뛰고 구조를 직접 수정할 수 있습니다.
액세스 한정자를 통해 개체를 제어하는 것 외에도 속성을 사용하면 사용자가 개체와 상호 작용하는 방식을 매우 정확하게 제어할 수 있습니다. 특히, 속성은 읽기 및 쓰기의 특정 조건을 알려줄 수도 있습니다.
속성은 스토리지 논리를 통해 데이터를 getter 및 setter로 재정의하거나 데이터 바인딩 리소스를 제공할 때 안정적인 API를 구축하는 데 도움이 됩니다.
속성 getter에서 예외를 발생시키지 말고 객체 상태를 수정하지 마세요. 이는 속성에 대한 getter가 아닌 메서드에 대한 요구 사항입니다.
更多有关属性的信息,请参阅MSDN:
http://msdn.microsoft.com/en-us/library/ms229006(v=vs.120).aspx
同时也要注意getter的一些副作用。开发者也习惯于将成员体的存取视为一种常见的操作,因此他们在代码审查的时候也常常忽视那些副作用。
你可以为一个新创建的对象根据它创建的表达形式赋予属性。例如为Foo与Bar属性创建一个新的具有给定值的C类对象:
new C {Foo=blah, Bar=blam}
你也可以生成一个具有特定属性名称的匿名类型的实体:
var myAwesomeObject = new {Name=”Foo”, Size=10};
初始化过程在构造函数体之前运行,因此需要保证在输入至构造函数之前,将这一域给初始化。由于构造函数还没有运行,所以目标域的初始化可能不管怎样都不涉及“this”。
为了使一些特殊方法更加容易控制,最好在你使用的方法当中使用最少的特定类型。比如在一种方法中使用 List
public void Foo(List<Bar> bars) { foreach(var b in bars) { // do something with the bar... } }
对于其他IEnumerable
泛型是一种在定义独立类型结构体与设计算法上一种十分有力的工具,它可以强制类型变得安全。
用像List
在使用泛型时,我们可以用关键词“default”来为类型获取缺省值(这些缺省值不可以硬编码写进implementation)。特别要指出的是,数字类型的缺省值是o,引用类型与空类型的缺省值为null。
T t = default(T);
类型转换有两种模式。其一显式转换必须由开发者调用,另一隐式转换是基于环境下应用于编译器的。
常量o可由隐式转换至枚举型数据。当你尝试调用含有数字的方法时,可以将这些数据转换成枚举类型。
类型转换 | 描述 |
Tree tree = (Tree)obj; | 这种方法可以在对象是树类型时使用;如果对象不是树,可能会出现InvalidCast异常。 |
Tree tree = obj as Tree; | 这种方法你可以在预测对象是否为树时使用。如果对象不是树,那么会给树赋值null。你可以用“as”的转换,然后找到null值的返回处,再进行处理。由于它需要有条件处理的返回值,因此记住只在需要的时候才去用这种转换。这种额外的代码可能会造成一些bug,还可能会降低代码的可读性。 |
转换通常意味着以下两件事之一:
1.RuntimeType的表现可比编译器所表现出来的特殊的多,Cast转换命令编译器将这种表达视为一种更特殊的类型。如果你的设想不正确的话,那么编译器会向你输出一个异常。例如:将对象转换成串。
2.有一种完全不同的类型的值,与Expression的值有关。Cast命令编译器生成代码去与该值相关联,或者是在没有值的情况下报出一个异常。例如:将double类型转换成int类型。
以上两种类型的Cast都有着风险。第一种Cast向我们提出了一个问题:“为什么开发者能很清楚地知道问题,而编译器为什么不能?”如果你处于这个情况当中,你可以去尝试改变程序让编译器能够顺利地推理出正确的类型。如果你认为一个对象的runtime type是比compile time type还要特殊的类型,你就可以用“as”或者“is”操作。
第二种cast也提出了一个问题:“为什么不在第一步就对目标数据类型进行操作?”如果你需要int类型的结果,那么用int会比double更有意义一些。
获取额外的信息请参阅:
http://blogs.msdn.com/b/ericlippert/archive/tags/cast+operator/
在某些情况下显式转换是一种正确的选择,它可以提高代码可阅读性与debug能力,还可以在采用合适的操作的情况下提高测试能力。
异常不应该常出现在程序流程中。它们代表着开发者所不愿看到的运行环境,而这些很可能无法修复。如果你期望得到一个可控制的环境,那么主动去检查环境会比等待问题的出现要好得多。
利用TryParse()方法可以很方便地将格式化的串转换成数字。不论是否解析成功,它都会返回一个布尔型结果,这要比单纯返回异常要好很多。
写代码时注意catch与finally块的使用。由于这些不希望得到的异常,控制可能进入这些块中。那些你期望的已执行的代码可能会由于异常而跳过。如:
Frobber originalFrobber = null; try { originalFrobber = this.GetCurrentFrobber(); this.UseTemporaryFrobber(); this.frobSomeBlobs(); } finally { this.ResetFrobber(originalFrobber); }
如果GetCurrentFrobber()报出了一个异常,那么当finally blocks被执行时originalFrobber的值仍然为空。如果GetCurrentFrobber不能被扔掉,那么为什么其内部是一个try block?
要注意有针对性地处理你的目标异常,并且只去处理目标代码当中的异常部分。尽量不要去处理所有异常,或者是根类异常,除非你的目的是记录并重新处理这些异常。某些异常会使应用处于一种接近崩溃的状态,但这也比无法修复要好得多。有些试图修复代码的操作可能会误使情况变得更糟糕。
关于致命的异常都有一些细微的差异,特别是注重finally blocks的执行,可以影响到异常的安全与调试。更多信息请参阅:
http://incrediblejourneysintotheknown.blogspot.com/2009/02/fatal-exceptions-and-why-vbnet-has.html
使用一款顶级的异常处理器去安全地处理异常情况,并且会将debug的一些问题信息暴露出来。使用catch块会比较安全地定位那些特殊的情况,从而安全地解决这些问题,再将一些问题留给顶级的异常处理器去解决。
如果你发现了一个异常,请做些什么去解决它,而不要去将这个问题搁置。搁置只会使问题更加复杂,更难以解决。
将异常包含至一个自定义异常中,对面向公共API的代码特别有用。异常是可视界面方法的一部分,它也被参数与返回值所控制。但这种扩散了很多异常的方法对于代码的鲁棒性与可维护性的解决来说十分麻烦。
如果你希望在更高层次上解决caught异常,那么就维持原异常状态,并且栈就是一个很好的debug方法。但需要注意维持好debug与安全考虑的平衡。
好的选择包括简单地将异常继续抛出:
Throw;
或者将异常视为内部异常重新抛出:
抛出一个新CustomException;
不要显式重新抛出类似于这样的caught异常:
Throw e;
这样的话会将异常的处理恢复至初始状态,并且阻碍debug。
有些异常发生于你代码的运行环境之外。与其使用caught块,你可能更需要向目标当中添加如ThreadException或UnhandledException之类的处理器。例如,Windows窗体异常并不是出现于窗体处理线程环境当中的。
千万不要让异常影响到你数据模型的完整性。你需要保证你的对象处于比较稳定的状态当中——这样一来任何由类的执行的操作都不会出现违例。否则,通过“恢复”这一手段会使你的代码变得更加让人不解,也容易造成进一步的损坏。
考虑几种修改私有域顺序的方法。如果在修改顺序的过程当中出现了异常,那么你的对象可能并不处于非法状态下。尝试在实际更新域之前去得到新的值,这样你就可以在异常安全管理下,正常地更新你的域。
对特定类型的值——包括布尔型,32bit或者更小的数据类型与引用型——进行可变量的分配,确保可以是原子型。没有什么保障是给一些大型数据(double,long,decimal)使用的。可以多考虑这个:在共享多线程的变量时,多使用lock statements。
事件与委托共同提供了一种关于类的方法,这种方法在有特殊的事情发生时向用户进行提醒。委托事件的值在事件发生时应被调用。事件就像是委托类型的域,当对象生成时,其自动初始化为null。
事件也像值为“组播”的域。这也就是说,一种委托可以依次调用其他委托。你可以将一个委托分配给一个事件,你也可以通过类似-=于+=这样的操作来控制事件。
如果一个事件被多个线程所共享,另一个线程就有可能在你检查是否为null之后,在调用其之前而清除所有的用户信息——并抛出一个NullReferenceException。
对于此类问题的标准解决方法是创建一个该事件的副本,用于测试与调用。你仍然需要注意的是,如果委托没有被正确调用的话,那么在其他线程里被移除的用户仍然可以继续操作。你也可以用某种方法将操作按顺序锁定,以避免一些问题。
public event EventHandler SomethingHappened; private void OnSomethingHappened() { // The event is null until somebody hooks up to it // Create our own copy of the event to protect against another thread removing our subscribers EventHandler handler = SomethingHappened; if (handler != null) handler(this,new EventArgs()); }
更多关于事件与竞争的信息请参阅:
http://blogs.msdn.com/b/ericlippert/archive/2009/04/29/events-and-races.aspx
使用一种事件处理器为事件资源生成一个由处理器的资源对象到接收对象的引用,可以保护接收端的garbage collection。
适当的unhook处理器可以确保你不必因委托不再工作而去调用它浪费时间,也不会使内存存储无用委托与不可引用的对象。
属性提供了一种向程序集、类与其信息属性中注入元数据的方法。它们经常用来提供信息给代码的消费者——比如debugger、框架测试、应用——通过反射这一方式。你也可以向你的用户定义属性,或是使用预定义属性,详见下表:
属性 | 使用对象 | 目的 |
DebuggerDisplay | Debugger | Debugger display 格式 |
InternalsVisibleTo | Member access | 使用特定类来暴露内部成员去指定其他的类。基于此方法,测试方法可以用来保护成员,并且persistence层可以用一些特殊的隐蔽方法。 |
DefaultValue | Properties | 为属性指定一个缺省值 |
一定要对DebuggerStepThrough多重视几分——否则它会在这个方法应用的地方让寻找bug变得十分困难,你也会因此而跳过某步或是推倒而重做它。
Debug是在开发过程中必不可少的部分。除了使运行环境不透明的部分变得可视化之外,debugger也可以侵入运行环境,并且如果不使用debugger的话会导致应用程序变现有所不同。
为了观察当前框架异常状态,你可以将“$exception”这一表达添加进Visual Studio Watch窗口。这种变量包含了当前异常状态,类似于你在catch block中所看见的,但其中不包含在debugger中看见的不是代码中的真正存在的异常。
如果你的属性有副作用,那么考虑你是否应使用特性或者是debugger设置去避免debugger自动地调用getter。例如,你的类可能有这样一个属性:
private int remainingAccesses = 10; private string meteredData; public string MeteredData { get { if (remainingAccesses-- > 0) return meteredData; return null; } }
你第一次在debugger中看见这个对象时,remainingAccesses会获得一个值为10的整型变量,并且MeteredData为null。然而如果你hover结束了remainingAccesses,你会发现它的值会变成9.这样一来debugger的属性值表现改变了你的对象的状态。
早做计划,不断监测,后做优化
在设计阶段,制定切实可行的目标。在开发阶段,专注于代码的正确性要比去做微调整有意义的多。对于你的目标,你要在开发过程中多进行监测。只需要在你没有达到预期的目标的时候,你才应该去花时间对程序做一个调整。
请记住用合适的工具来确保性能的经验性测量,并且使测试处于这样一种环境当中:可反复多次测试,并且测试过程尽量与现实当中用户的使用习惯一致。
当你对性能进行测试的时候,一定要注意你真正所关心的测试目标是什么。在进行某一项功能的测试时,你的测试有没有包含这项功能的调用或者是回路构造的开销?
我们都听说过很多比别人做得快很多的项目神话,不要盲目相信这些,试验与测试才是实在的东西。
由于CLR优化的原因,有时候看起来效率不高的代码可能会比看起来效率高的代码运行的更快。例如,CLR优化循环覆盖了一个完整的数组,以避免在不可见的per-element范围里的检查。开发者经常在循环一个数组之前先计算一下它的长度:
int[] a_val = int[4000]; int len = a_val.Length; for (int i = 0; i < len; i++) a_val[i] = i;
通过将长度存储进一个变量当中,CLR会不去识别这一部分,并且跳过优化。但是有时手动优化会反人类地导致更糟糕的性能表现。
如果你打算将大量的字符串进行连接,可以使用System.Text.StringBuilder来避免生成大量的临时字符串。
如果你打算生成并填满集合中已知的大量数据,由于再分配的存在,可以用保留空间来解决生成集合的性能与资源问题。你可以用AddRange方法来进一步对性能进行优化,如下在List
Persons.AddRange(listBox.Items);
垃圾收集器(garbage collector)可以自动地清理内存。即使这样,一切被抛弃的资源也需要适当的处理——特别是那些垃圾收集器不能管理的资源。
资源管理问题的常见来源 | |
内存碎片 | 如果没有足够大的连续的虚拟地址存储空间,可能会导致分配失败 |
进程限制 | 进程通常都可以读取内存的所有子集,以及系统可用的资源。 |
资源泄露 | 垃圾收集器只管理内存,其他资源需要由应用程序正确管理。 |
不稳定资源 | 那些依赖于垃圾收集器与终结器(finalizers)的资源在很久没用过的时候,不可被立即调用。实际上它们可能永远不可能被调用。 |
利用try/finally block来确保资源已被合理释放,或是让你的类使用IDisposable,以及更方便更安全的声明方式。
using (StreamReader reader=new StreamReader(file)) { //your code here
除了用调用GC.Collect()干扰garbage collector之外,也可以考虑适当地释放或是抛弃资源。在进行性能测试时,如果你可以承担这种影响带来的后果,你再去使用garbage collector。
与当前一些流传的谣言不同的是,你的类不需要Finalizers,而这只是因为IDisposable的存在!你可以让IDisposable赋予你的类在任何已拥有的组合实例中调用Dispose的能力,但是finalizers只能在拥有未管理的资源类中使用。
Finalizers主要对交互式Win32位句柄API有很大作用,并且SafeHandle句柄是很容易利用的。
不要总是设想你的finalizers(总是在finalizer线程上运行的)会很好地与其他对象进行交互。那些其他的对象可能在该进程之前就被终止掉了。
处理并发性与多线程编程是件复杂的、困难的事情。在将并发性添加进你的程序之前,请确保你已经明确了解你的做的是什么——因为这里面有太多门道了!
多线程软件的情况很难进行预测,比如很容易产生如竞争条件与死锁的问题,而这些问题并不是仅仅影响单线程应用。基于这些风险,你应该将多线程视为最后一种手段。如果不得不使用多线程,尽量缩减多线程同时使用内存的需求。如果必须使线程同步,请尽可能地使用最高等级的同步机制。在最高等级的前提下,包括了这些机制:
Async-await/Task Parallel Library/Lazy
Lock/monitor/AutoResetEvent
Interlocked/Semaphore
可变域与显式barrier
以上的这些很难解释清楚C#/.NET的复杂之处。如果你想开发一个正常的并发应用,可以去参阅O’Reilly的《Concurrency in C# Cookboo》。
将一个域标记为“volatile”是一种高级特性,而这种设置也经常被专家所误解。C#的编译器会保证目标域可以被获取与释放语义,但是被lock的域就不适用于这种情况。如果你不知道获取什么,不知道释放什么语义,以及它们是怎样影响CPU层次的优化,那么久避免使用volatile域。取而代之的可以用更高层次的工具,比如Task Parallel Library或是CancellationToken。
标准库类型常提供使对象线程安全更容易的方法。例如Dictionary.TryGetValue()。使用此类方法一般可以使你的代码变得更加清爽,并且你也不必担心像TOCTOU(time-of-check-time-of-use竞争危害的一种)这样的数据竞争。
不要锁住“this”、字符串,或是其他普通public的对象
当使用在多线程环境下的一些类时,多注意lock的使用。锁住字符串常量,或是其他公共对象,会阻止你锁状态下的封装,还可能会导致死锁。你需要阻止其他代码锁定在同一使用的对象上,当然你最好的选择是使用private对象成员项。
滥用null是一种常见的导致程序错误的来源,这种非正常操作可能会使程序崩溃或是其他的异常。如果你试图获取一个null的引用,就好像它是某对象的有效引用值(例如通过获取一个属性或是方法),那么在运行时就会抛出一个NullReferenceException。
静态与动态分析工具可以在你发布代码之前为你检查出潜在的NullReferenceException。在C#当中,引用型为null通常是由于变量没有引用到某个对象而造成的。对于值可为空的类型与引用型来说,是可以使用null的。例如:Nullable
每个null引用异常都是一个bug。相比于找到NullReferenceException这个问题来说,不如尝试在你使用该对象之前去为null进行测试。这样一来可以使代码更易于最小化的try/catch block读取。
当从数据库表中读取数据时,注意缺失值可以表示为DBNull 对象,而不是作为空引用。不要期望它们表现得像潜在的空引用一样。
Float与double都可以表示十进制实数,但不能表示二进制实数,并且在存储十进制值的时候可以在必要时用二进制的近似值存储。从十进制的角度来看,这些二进制的近似值通常都有不同的精度与取舍,有时在算数操作当中会导致一些不期望的结果。由于浮点型运算通常在硬件当中执行,因此硬件条件的不可预测会使这些差异更加复杂。
在十进制精度很重要的时候,就要使用十进制了——比如经济方面的计算。
有一种常见的错误就是忘记了结构是值类型,意即其复制与通过值传递。例如你可能见过这样的代码:
struct P { public int x; public int y; } void M() { P p = whatever; … p.x = something; … N(p);
忽然某一天,代码维护人员决定将代码重构成这样:
void M() { P p = whatever; Helper(p); N(p); } void Helper(P p) { … p.x = something;
现在当N(p)在M()中被调用,p就有了一个错误的值。调用Helper(p)传递p的副本,并不是引用p,于是在Helper()中的突变便丢失掉了。如果被正常调用,那么Helper应该传递的是调整过的p的副本。
C#编译器可以保护在运算过程中的常量溢出,但不一定是计算值。使用“checked”与“unchecked”两个关键词来标记你想对变量进行什么操作。
与结构体不同的是,类是引用类型,并且可以适当地修改引用对象。然而并不是所有的对象方法都可以实际修改引用对象,有一些返回的是一个新的对象。当开发者调用后者时,他们需要记住将返回值分配给一个变量,这样才可以使用修改过的对象。在代码审查阶段,这些问题的类型通常会逃过审查而不被发现。像字符串之类的对象,它们是不可变的,因此永远不可能修改这些对象。即便如此,开发者还是很容易忘记这些问题。
例如,看如下 string.Replace()代码:
string label = “My name is Aloysius”; label.Replace(“Aloysius”, “secret”);
这两行代码运行之后会打印出“My name is Aloysius” ,这是因为Raeplace方法并没改变该字符串的值。
注意不要在遍历时去修改集合
List<Int> myItems = new List<Int>{20,25,9,14,50}; foreach(int item in myItems) { if (item < 10) { myItems.Remove(item); // iterator is now invalid! // you’ll get an exception on the next iteration
如果你运行了这个代码,那么它一在下一项的集合中进行循环,你就会得到一个异常。
正确的处理方法是使用第二个list去保存你想删除的这一项,然后在你想删除的时候再遍历这个list:
List<Int> myItems = new List<Int>{20,25,9,14,50}; List<Int> toRemove = new List<Int>(); foreach(int item in myItems) { if (item < 10) { toRemove.Add(item); } } foreach(int item in toRemove) {
如果你用的是C#3.0或更高版本,可以尝试List
myInts.RemoveAll(item => (item < 10));
在实现属性时,要注意属性的名称和在类当中用的成员项的名字有很大差别。很容易在不知情的情况下使用了相同的名称,并且在属性被获取的时候还会触发死循环。
// The following code will trigger infinite recursion private string name; public string Name { get { return Name; // should reference “name” instead.
在重命名间接属性时同样要小心。例如:在WPF中绑定的数据将属性名称指定为字符串。有时无意的改变属性名称,可能会不小心造成编译器无法解决的问题。
英文原文:13 Things Every C# Developer Should Know 翻译:码农网
위 내용은 C# 개발자가 알아야 할 13가지의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!