寫好C#程式碼的技巧
寫好C#程式碼的技巧
編者導語
本文來自https://www.pluralsight.com,作者Afzaal Ahmad Zeeshan。
原文包含以下三篇文章:
《編寫更好的C#程式碼簡介》https://www.pluralsight.com/guides/introduction-to-writing-better-csharp-code
《編寫更好的C#程式碼的技巧》https://www.pluralsight.com/guides/tips-for-writing-better-c-code
《有關編寫更好的C#程式碼的更多技巧》https://www.pluralsight.com/guides/more-tips-for-writing-better-csharp-code
雖然本文僅介紹了C#6.0語言特性,而現在最新的C#已經到了9.0,但這些內容已經仍然常讀常新。
一、簡介
C#已從C#5更改為C#6,為使專案更具可讀性,基於最佳標準的實踐也得到了發展。
本指南系列的目的是幫助您為在團隊環境中執行的C#專案和.NET Framework應用程式編寫更簡潔的程式碼。在團隊環境下,編寫好的程式碼對開發人員可能更容易,因為編寫的程式碼將由團隊中其他開發人員使用,管理和更新,而程式碼質量往往取決於您個人團隊的“哲學”和開發人員的編碼實踐。
在這種情況下,最好的方法是遵循編碼團隊的準則,併為應用程式專案中的C#程式新增設計和風格,以使它們對讀者更好。請注意,C#編譯器並不關心您放入程式碼中的風格。但我們以一種使C#應用程式對讀者來說看起來更簡單,更清潔,將更容易的方式更深入地進行程式設計,同時保持程式碼開發的效能和效率。
在閱讀本指南之前,您應該瞭解以下幾點:
1.第6版對C#的改進2..NET框架中的LINQ3.Task
C#中的非同步程式設計和物件4.使用C#進行的不安全程式設計,使您無法正常的使用記憶體管理
不專注於效能
應該注意的是,我不會談論改變程式效能,提高效率或減少程式執行所花費的時間。通過編寫簡潔的C#程式碼,您可以在幾秒鐘內提高程式效能,但是以下技巧並不能保證您的程式碼效能更好。
為什麼要編寫整潔的程式碼?
您編寫程式碼,編譯器編譯時沒有警告也沒有錯誤,程式碼很好。但是,如果其他人想讀出該程式碼怎麼辦?如果有人後來需要為您或您所在的公司升級程式碼,該怎麼辦?看下面的程式碼:
public static void Main(string[] args) {
int x = 0;
x = Console.Read();
Console.WriteLine(x * 1.5);
}
該程式執行良好,系統中沒有錯誤,應用程式也可以正常工作。但是您能告訴我該程式在現實生活中做什麼嗎?以下是可以做出的一些假設:
1.它只是乘以價值2.就像獎金一樣,它正在增加價值3.是個人銀行存款總額的利率4.等等。
哪一個是真實的?沒有人會知道。在這種情況下,最好編寫出良好的程式碼,並記住遵循程式設計的基礎。看下面的程式碼:
public static void Main(string[] args) {
int salary = 0;
salary = Console.Read();
Console.WriteLine(salary * 1.5);
}
這比以前的程式碼有意義嗎?我們可以很容易地說這個程式碼將增加薪水的價值。請注意,僅通過改進程式碼,我們就能確保其他人可以比以前更快地理解它。
在本指南中,我不會向您展示如何遵循最佳原則。相反,我將以您已有的知識為基礎,並教您如何充分利用C#程式。我將重點介紹如何在應用程式中編寫良好的C#邏輯,因此您將看到通過以這種方式和結構編寫程式,可以從應用程式中獲得很多好處。
因此,讓我們開始吧。
物件初始化
C#是一種面向物件的程式語言。如果物件本身沒有分塊,那麼寫一組提示有什麼好處?本節將重點介紹在前進並new Object()
在應用程式中編寫程式碼之前應考慮的事項。您必須瞭解如何建立C#類以及事物如何協作以在系統中啟動一個小程式。
例如,看下面的程式碼:
class Person {
public int ID { get; set; }
public string Name { get; set; }
public DateTime DateOfBirth { get; set; }
public bool Gender { get; set; }
}
您可能想要建立預設情況下設定值的程式,或者讓它們來自模型或諸如此原始碼的任何其他面向資料庫的資料來源,這些程式簡化了在物件時輸入預設值的方式正在建立。
var person = new Person { ID = 1, Name = "Afzaal Ahmad Zeeshan", DateOfBirth = new DateTime(1995, 08, 29), Gender = true };
相反,請嘗試通過以下方式編寫相同的程式碼:
var person = new Person();
person.ID = 1;
person.Name = "Afzaal Ahmad Zeeshan";
// So on.
這裡的程式碼沒有明顯的效能改進,但是可以真正提高程式碼的可讀性。如果您喜歡縮排,請在這裡檢視:
var person = new Person
{
ID = 1,
Name = "Afzaal Ahmad Zeeshan",
DateOfBirth = new DateTime(1995, 08, 29),
Gender = true
};
這也有縮排,但是它為您的C#程式碼的可讀性添加了更多的說明。儘管前面的程式碼可以實現相同的功能,但是建議的程式碼可以使程式碼更易讀和簡潔。
二、技巧
空檢查
NullReferenceException
當缺少初始化的物件再次丟擲異常時,您是否曾經對感到惱火?在程式中進行空檢查有很多好處,不僅可以提高可讀性,而且可以確保程式不會由於記憶體問題而終止(例如,記憶體中不存在變數時)。這些可能與程式的安全性以及團隊具有的良好UI和UX準則相牴觸。大多數情況下,由於以下原因會引發空異常:
string name = null;
Console.WriteLine(name);
在大多數情況下,除非您解決此問題,否則編譯器本身不會繼續執行,但是如果您設法以某種方式誘使編譯器認為變數具有值,但在執行時沒有變數,則會出現空引用異常。為了克服這個問題,您可以執行以下操作:
string name = null;
// Try to enter the value, from somewhere
if(name != null) {
Console.WriteLine(name);
}
此安全檢查將確保在呼叫此變數時該值可用。否則,它將影響您程式碼的路徑。但是,在C#6中,還有另一種方法可以克服此錯誤。考慮以下情形:建立資料庫,建立資料表,找到您的人員但找不到他們的就業詳細資訊。你能找到他們工作的公司嗎?
var company = DbHelper.PeopleTable.Find(x => x.id == id).FirstOrDefault().EmploymentHistory.CompanyName; // Error
如果您這樣做,將會出現錯誤,因為我們只能在這些值的列表中進行簡單幾步的物件篩選。然後我們將碰到一個空值,一切都丟失了。C#6提出了一種克服這些情況的新方法,方法是在值和欄位可以為null的後面使用安全的導航運算子。?.
。像這樣:
var company = DbHelper?.PeopleTable?.Find(x => x.id == id)?.FirstOrDefault()?.EmploymentHistory?.CompanyName; // Works
如果前一個不為null,則此程式碼僅檢查下一個值。如果先前的值為null,它將返回null並將null儲存為的值company
,而不是引發錯誤。將檢查留給框架本身可以很方便,但是,儘管如此,您仍然必須在最後檢查其餘值是否為null。
var company = DbHelper.PeopleTable?.Find(x => x.id == id)?.FirstOrDefault()?.EmploymentHistory?.CompanyName;
if(company != null) {
// Final process
}
但是您明白了這一點,而不是編寫程式碼並檢查所有內容是否為空,而是可以執行簡單的檢查並執行程式中想要的操作和邏輯。否則,將需要try...catch
包裝器或多個if...else
塊來控制程式在系統中的導航方式。
非同步程式設計模式
如果您正在使用C#5進行程式設計,那麼您已經在使用async / await關鍵字為您的應用程式帶來改進。如果不是這種情況,那麼我建議您在應用程式的原始碼中使用非同步程式設計模式。這不僅可以提高對程式的響應速度,還可以提高應用程式的可讀性。在原始碼中具有非同步模式的一些好處是:
1.程式碼路徑開始變得更加有意義。如果有一個程序在後臺開始執行,那麼程式設計師可以瞭解程式應該在哪裡。2.應用程式掛起問題將消失。大多數與應用程式阻塞相關的問題直接來自程式碼。當UI執行緒無法更新UI時,使用者會認為該應用程式正在掛起並且沒有響應,而事實並非如此。非同步方法確實可以幫上大忙。3.基於Windows執行時的應用程式完全基於此方法。您將(並且必須是!)在您的Windows Runtime應用程式中使用這種方法來解決諸如掛起應用程式或不良的程式設計習慣之類的問題。
自從執行緒化以來,程式碼執行的並行化就已經存在。非同步已經成為程式和應用程式的重要組成部分,因此您更應該考慮使用它。
C#字串構建
字串是當今應用程式的重要組成部分,構建字串可能會花費很多時間,並且還會導致應用程式效能下降。您可以通過多種方式在C#程式中構建字串。以下是其中幾種方式:
string str = ""; // Setting it to null would cause additional problems.
// Way 1
str = "Name: " + name + ", Age: " + age;
// Way 2
str = string.Format("Name: {0}, Age: {1}", name, age);
// Way 3
var builder = new StringBuilder();
builder.Append("Name: ");
builder.Append(name);
builder.Append(", Age: ");
builder.Append(age);
str = builder.ToString();
請注意,C#中的字串是不可變的。這意味著,如果您嘗試更新它們的值,則會重新建立它們,並從記憶體中刪除以前的控制代碼。這就是為什麼方式1看起來是最好的方式,但經過進一步思考,事實並非如此。最好的方法是方法3,它使您可以構建字串而不必在記憶體中重新建立物件。同時,C#6引入了一種全新的方式在C#中構建字串,該方式比您以前想象的要好得多。新的
字串插值運算子$
為您提供了以最佳方式執行字串構建的功能。字串插值如下所示:
static void Main(string[] args)
{
// Just arbitrary variables
string name = "";
int age = 0;
// Our interest
string str = $"Name: {name}, Age: {age}";
}
只需一行程式碼,編譯器就會自動將其轉換為string.Format()
版本。為了證明這一點,將詳細說明此C#程式已生成的位元組碼,並向您展示如何自動更改語法以讀取字串格式。
IL_0000: nop
IL_0001: ldstr ""
IL_0006: stloc.0 // name
IL_0007: ldc.i4.0
IL_0008: stloc.1 // age
IL_0009: ldstr "Name: {0}, Age: {1}"
IL_000E: ldloc.0 // name
IL_000F: ldloc.1 // age
IL_0010: box System.Int32
IL_0015: call System.String.Format
IL_001A: stloc.2 // str
IL_001B: ret
可以看出,這顯示瞭如何將語法更改回我們已經看到的語法。有關IL_0009
更多資訊,請參見。當其他人正在讀取程式時,這可以使您的程式外觀更簡潔,並且如果要構建的字串較小,則可以提高效能。如果字串較大,請使用StringBuilder
。
三、更多技巧
遍歷資料
如果不對一組資料進行迴圈和迭代,那麼應用程式有什麼用?在這種情況下,有時您將不得不查詢值,查詢節點,查詢記錄或對集合進行任何其他遍歷。在這種情況下,您確實需要確保編寫乾淨的程式碼,因為這是效能和可讀性都非常重要且相互關聯的領域。
有了一些經驗,我就克服了編寫用於讀取和遍歷資料的錯誤程式碼的方式。這正是LINQ應該加入的地方,LINQ允許您編寫使用最佳.NET框架為使用者和客戶提供最佳編碼體驗和最佳體驗的程式。
以前,您可能已經做過以下一些事情:
6
// A function to search for people
Person FindPerson(int id) {
var people = DbContext.GetPeople(); // Returns List<Person>
foreach (var person in people) {
if(person.ID == id) {
return person;
}
}
// No person found.
return null;
}
// Then do this
var person = FindPerson(123);
對於任何想接手您程式碼的人來說,這都是一段易讀的程式碼。但是,使用C#中的LINQ查詢可以使程式碼更加簡單和整潔。您可以通過兩種方式執行此操作。一個有點像SQL,另一個是通過Where
在集合上使用該函式並傳遞我們的要求。
// A function to search for people
Person FindPerson(int id) {
var people = DbContext.GetPeople(); // Returns List<Person>
return (from person in people
where person.ID == id
select person).ToList().FirstOrDefault();
}
// Then do this
var person = FindPerson(123);
該程式碼看起來有點像SQL,可以增強程式碼的可讀性和效能。該函式相似,但是,該Where
函式的讀取效果更好,並使所有迭代都針對.NET框架本身,而.NET框架將為應用程式提供最佳效能。
現在,讓我們看看用相同的C#程式碼編寫此查詢的另一種方式:
// A function to search for people
Person FindPerson(int id) {
var people = DbContext.GetPeople(); // Returns List<Person>
return people.FirstOrDefault(x => x.ID == id);
}
// Then do this
var person = FindPerson(123);
請注意,null
如果沒有找到匹配項,則返回第一個程式碼。這段程式碼也做同樣的事情。唯一的第一個程式碼更糟糕的是它必須對集合本身執行迭代。
該本地變數return person;
將允許程式返回控制元件,但是如果資料位於最後一個位置會發生什麼呢?此資料搜尋演算法的複雜度仍為O(n)。
避免unsafe上下文
在您必須親自處理記憶體時,C#還支援手動記憶體管理。C#中的不安全上下文允許您操作記憶體,執行指標算術,在可能無法訪問的記憶體位置讀取和寫入資料,等等。但是,.NET框架可以做很多事情來克服記憶體問題,延遲和磁碟上其他問題。這也使.NET框架完全無需實際執行任何記憶體管理,.NET框架將為您做到這一點。
使用不安全的上下文有很多好處,例如,當您要圍繞本機C ++庫編寫包裝器時。Emgu CV就是這樣一個示例,您將在其中編寫一些程式碼來處理如何管理本機程式碼,並以更簡單的方式來處理記憶體中的錯誤。在這種情況下,您可以:
1.使用指標管理和指標算術。您不能在此上下文之外的任何地址上執行任何操作,這是.NET規則所處的位置。2.使用記憶體管理來操作記憶體中的物件。3.使用C ++風格的程式設計,這正是C#設計的目的。
這幾乎沒有好處,如果您應該在應用程式中考慮這一點,請明智地考慮。
關於Unsafe
純屬個人觀點
我還想指出,關於“不安全”的利弊,我所說的一切都是我個人的看法。我不經常在程式中使用unsafe
上下文,因為沒有理由考慮在應用程式中使用上下文。但是,如果您的應用程式需要本機記憶體管理,則可以使用此上下文。
儘可能使用Lambda表示式
Lambda來自函數語言程式設計領域,在C#中已廣泛使用,從行內函數一直到C#6中的getter only屬性。我將展示C#中的兩種用法,它們構成的程式,不僅看起來更清爽,而且效能指標也更高。
為此,我將向您顯示該C#程式碼的IL。我個人喜歡在許多領域使用lambda,尤其是當我不得不用C#編寫行內函數時。自從可以使用此概念編寫僅用於getter的屬性以來,我一直在使用它們,並且我個人認為它比以前做同一件事的方法更好。
1.將Lambda用於行內函數
您應該知道一些C#程式設計的示例,使用這種寫法的程式碼很多。
例如在應用程式中進行事件處理的情況下,對於事件處理,您可以像下面這樣編寫當前函式:
// Without lamdbas
myBtn.Click += Btn_Click;
public void Btn_Click (object sender, EventArgs e) {
// Code to handle the event
}
// With the help of lambdas
myBtn.Click += (sender, e) =>
{
// Code to handle the event.
}
請注意,編譯器將自動將物件對映到其型別。這在許多方面都很方便,因為它允許您用C#編寫僅與物件一起保留的行內函數,除非您也想在其他任何地方使用它們。但是,這種處理事件的方法有一個缺點:一旦附加了事件處理程式,便無法刪除它。在C#中可以,-+
。
但是由於我們沒有刪除事件的參考,因此只能使用單獨的函式。但是,如果不必刪除處理程式,則應始終考慮在程式中使用這種事件處理方式。
2.將Lambda用於僅Getter的屬性
在C#中,有一個使用屬性而不是欄位的概念。您可以控制如何設定值以及如何從欄位中捕獲值。將其視為Java程式語言的getter和setter方法的替代方法(或類似方法)。唯一的區別是您不必在某個地方分別編寫它們,它們直接寫在欄位本身的前面。然後,C#程式編譯器將建立自己的後備欄位,用於儲存值。
基本上,您必須編寫如下這樣的屬性:
public string Name { get; }
請注意,這些屬性是恆定的,設定後就無法更改。它們是在建構函式中設定的,或者(從C#6開始)在它們的前面設定。像這樣:
public string Name { get; } = "Afzaal Ahmad Zeeshan";
但是,由於我們已經知道這是一個常量欄位,您不能修改它,那麼為什麼不建立一個簡單的常量屬性呢?事情變得有些棘手。甚至一個屬性也必須由欄位來備份。在這種情況下,這將為我們解決問題:
public string Name => "Afzaal Ahmad Zeeshan";
這等效於編寫以下內容:
public string Name { get { return "Afzaal Ahmad Zeeshan"; } }
但是由於編譯期將getter欄位轉換為常量欄位,並且在必須呼叫此屬性的時候才會在程式中使用該欄位,因此效能要好得多。
最後的話
本指南系列的目的是使您瞭解一些使程式更易於閱讀和更好執行的方法。C#編譯器本身會盡最大努力提高程式碼的質量和效率,而這程式設計師帶來便利,同時也將使程式更好地工作。
除了上面提到的方法,還有許多其他提高可讀性的方法,其中許多方法適合公司團隊協作的形式編寫程式,因為大多數團隊往往都要求程式設計師遵循自己的程式設計方法和方式。