C#開發者必須知道的13件事情
程式的Bug與瑕疵往往出現於開發流程當中。只要對工具善加利用,就有助於在你發布程式之前便將問題發現,或避開這些問題。
標準化程式碼書寫可以使程式碼更加易於維護,尤其是在程式碼由多個開發者或團隊進行開發與維護時,這一優點更加突出。常見的強製程式碼規範化的工具有:FxCop、StyleCop和ReSharper。
開發者語:在掩蓋錯誤之前請仔細思考這些錯誤,並且去分析結果。不要指望依靠這些工具來在程式碼中尋找錯誤,因為結果可能和你的與其相去甚遠。
審查程式碼與搭檔程式設計都是很常見的練習,例如開發者刻意去審查他人書寫的程式碼。而其他人則希望發現程式碼開發者的一些bug,例如編碼錯誤或執行錯誤。
審查程式碼是一種很有價值的練習,由於很依賴人工操作,因此很難被量化,準確度也不夠令人滿意。
靜態分析不需要你去執行程式碼,你不必寫測試案例就可以找出一些程式碼不規範的地方,或是一些瑕疵的存在。這是一種非常有效地尋找問題的方式,但是你需要有一個不會有太多誤報問題的工具。 C#常用的靜態分析工具有Coverity,CAT,NET,Visual Studio Code Analysis。
在你執行程式碼的時候,動態分析工具可以幫你找出這些錯誤:安全漏洞,效能與並發性問題。這種方法是在執行時期的環境下進行分析,正因如此,其有效性便受制於程式碼複雜度。 Visual Studio提供了大量包含Concurrency Visualizer, IntelliTrace, and Profiling Tools的動態分析工具。
管理者/團隊領導:開發實務是練習規避常見陷阱的最佳方法。同時也要注意測試工具是否符合你的需求。盡量讓你團隊的程式碼診斷等級處於可控的範圍內。
測試的方式多種多樣:單元測試,系統整合測試,效能測試,滲透測試等等。在開發階段,絕大多數的測試案例是由開發者或測試人員來完成編寫,使程式可以滿足需求。
測試只在運行正確的程式碼時才會有效。在進行功能測試的時候,它還可以用來挑戰開發者的研發與維護速度。
工具的選擇多花點時間,用正確的工具去解決你關心的問題,不要為開發者增添額外的工作。讓分析工具與測試自動流暢地運行起來去尋找問題,但是要確保程式碼的想法仍然清晰地留在開發者的頭腦當中。
盡可能快速定位診斷出來的問題所在位置(不論是透過靜態分析或測試得到的錯誤,例如編譯警告,標準違例,問題檢測等)。如果剛出來的問題因為「不關心」而去忽略它,導致該問題後來很難找到,那麼就會給代碼審閱工作者增加很大的工作量,並且還要祈禱他們不會因此煩躁。
請接受這些有用的建議,讓自己程式碼的質量,安全性,可維護性得到提升,同時也提升開發者們的研發能力、協調能力,以及提升發布程式碼的可預測性。
目標 | 工具 | 影響 |
# 一致性,可維護性 | 標準化程式碼書寫,靜態分析,程式碼審查 | 間距一致,命名標準,良好的可讀格式,都會讓開發者更容易編寫與維護程式碼。 |
# 準確性 | 程式碼審查,靜態分析,動態分析,測試 | 程式碼不只是需要語法正確,還需要以開發者的想法來滿足軟體需求。 |
# 功能性 | 測試 | 測試可以驗證大多數的需求是否得到滿足:正確性,可拓展性,穩健性以及安全性。 |
# 安全性 | 標準化程式碼書寫,程式碼審查,靜態分析,動態分析,測試 | 安全性是一個複雜的問題,任何一個小小的漏洞都是潛在的威脅。 |
# 開發者研發能力 | 標準化程式碼書寫,靜態分析,測驗 | 開發者在工具的幫助下會很快速地更正錯誤。 |
# 發布可預測性 | 標準化程式碼書寫,程式碼審查,靜態分析,動態分析,測試 | 流線型後期階段的活動、最小化錯誤定位循環,都可以讓問題發現的更早。 |
C#的一個主要的優點是其靈活的類型系統,而安全的類型可以幫助我們更早找到錯誤。透過強制執行嚴格的類型規則,編譯器能夠幫助你維持良好的程式碼書寫習慣。在這一方面,C#語言與.NET框架為我們提供了大量的類型,以適應絕大多數的需求。雖然許多開發者對一般的類型有著良好的理解,並且也知曉用戶的需求,但是一些誤解與誤用仍然存在。
更多關於.NTE框架類別庫的資訊請參閱MSDN library。
特定的介面涉及到常用的C#特徵。例如,IDiposable允許使用常見的資源管理語言,例如關鍵字「using」。很好地理解介面可以幫助你書寫通順的C#程式碼,並且更容易維護。
避免使用ICloneable介面-開發者從來沒搞清楚一個被複製的物件到底是深拷貝還是淺拷貝。由於仍沒有一種對複製物件操作是否正確的標準評判,於是也就沒辦法有意義地去將介面當作一個contract去使用。
盡量避免向結構體中進行寫入,將它們視為一種不變的物件以防止混亂。在像多執行緒這種場景下進行記憶體共享,會變得更安全。我們對結構體採用的方法是,在創建結構體時對其進行初始化操作,如果需要改變其數據,那麼建議產生一個新的實體。
正確理解哪些標準類型/方法是不可變,並且可傳回新的值(例如串,日期),用這些來取代那些易變物件(如List.Enumerator)。
字串的值可能為空,所以可以在適當的時候使用一些比較方便的功能。值判斷(s.Length==0)時可能會出現NullReferenceException錯誤,而String.IsNullOrEmpty(s)和String.IsNullOrWhitespace(s)可以很好地使用null。
枚舉類型與常數可以使程式碼更容易閱讀,透過利用標識符取代幻數,可以表現出數值的意義。
如果你需要產生大量的枚舉類型,那麼標記的枚舉類型是一種更簡單的選擇:
[Flag] public enum Tag { None =0x0, Tip =0x1, Example=0x2 }
下面這個方法可以讓你在一個snippet中使用多重標記:
snippet.Tag = Tag.Tip | Tag.Example
這種方法有利於資料的封裝,因此你也不必擔心在使用Tag property getter時有內部集合資訊外洩。
有以下兩種類型的相等性:
# 1.引用相等性,即兩種引用都指向同一個物件。
2.數值相等性,即兩個不同的引用物件可以視為相等的。
除此之外,C#也提供了許多相等性的測試方法。最常見的方法如下:
==與!=操作
# 由物件的虛繼承等值法
# 靜態Object.Equal法
#
IEquatable
# 靜態Object.ReferenceEquals法
# 有時候很難弄清楚使用引用或值相等性的目的。想進一步弄清楚這些,並且讓你的工作做得更好,請參閱:
# MSDNhttp://msdn.microsoft.com/en-us/library/dd183752.aspx
如果你想要覆蓋某個東西的時候,不要忘了MSDN上為我們提供的諸如IEquatable
注意無型別容器在重載方面的影響,可以考慮使用「myArrayList[0] == myString」這個方法。數組元素是編譯階段類型的“物件”,因此引用相等性可以使用。雖然C#會向你提醒這些潛在的錯誤,但是在編譯過程中,unexpected reference equality在某些情況下不會被提醒。
類別在適當的管理資料方面扮演很大角色。鑑於效能上的一些原因,類別總是快取部分結果,或者是在內部資料的一致性上做出一些假設。使資料權限公開的話會在一定程度上讓你去緩存,或者是做出假設,而這些操作是透過對效能、安全性、並發性的潛在影響表現出來的。例如揭露像是泛型集合、陣列之類的易變成員項,可以讓使用者跳過你而直接進行結構體的修改。
除了可以透過access modifiers控制物件之外,屬性還可以讓你精確地掌控使用者與你的物件之間進行了什麼互動。特別要指出的是,屬性還可以讓你了解讀寫的具體情況。
屬性能在透過儲存邏輯將資料覆寫進getters與setters的時候幫助你建立一個穩定的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中文網其他相關文章!