在我們寫代碼的時候可以使用一些小的技巧來使代碼變得更加簡潔,易於維護和高效。下面介紹幾種在C#/Net中非常有用的一些編程技巧。
1 空操作符(??)
在程序中經常會遇到對字符串或是對象判斷null的操作,如果為null則給空值或是一個指定的值。通常我們會這樣來處理
string name = value; if (name == null) { name = string.Empty; }
可以使用三元操作符(?:)對上面對嗎進行優化
string name = value == null ? string.Empty : value;
這樣使代碼簡潔了不少,但這還不是最簡潔的,我們還可以使用??操作符來進行進一步優化,??操作符意思是如果為null取操作符左邊的值,否則取右邊的值。
string name = value ?? string.Empty;
我們甚至可以寫一個擴展方法來過濾掉null和空格,使返回的結果可以更好的使用??操作符
public static class StringUtility { public static string TrimToNull(string source) { return string.IsNullOrWhiteSpace(source) ? null : source.Trim(); } }
使用代碼如下:
string name = string.TrimToNull(value) ?? "None Specified";
2 使用As轉換類型
在C#中進行類型轉換有很多種方式比如可以進行強制類型轉換,通常在轉換前會使用Is進行類型的判斷,所以您可能經常寫過或見過類似下面的代碼
if (employee is SalariedEmployee) { var salEmp = (SalariedEmployee)employee; pay = salEmp.WeeklySalary; // ... }
上面的代碼不會報異常,但在整個過程中做了兩次轉換操作,這樣會降低性能。我們可以使用as操作符來進行類型的轉換,同樣也不會報異常,如果類型不兼容則返回null,而是用as進行轉換整個過程只轉換一次。代碼如下:
var salEmployee = employee as SalariedEmployee; if (salEmployee != null) { pay = salEmployee.WeeklySalary; // ... }
3 自動屬性
自動屬性是C#3.0以上版本中的新功能,可以使代碼變得更簡潔,在以前定義屬性我們會寫如下代碼
public class Point { private int _x, _y; public int X { get { return _x; } set { _x = value; } } public int Y { get { return _y; } set { _y = value; } } }
使用自動屬性代碼就會簡潔了很多
public class Point { public int X { get; set; } public int Y { get; set; } }
在自動屬性中,我們可以給get或set訪問器設置訪問級別,使屬性變成只讀屬性或是只寫屬性
public class Asymetrical { public string ThisIsReadOnly { get; private set; } public double ThisIsWriteOnly { private get; set; } }
4 StopWatch類
在程序開發中有時會需要統計一個方法或是一個存儲過程執行了多長時間,比如在做一些方法的性能測試時就需要用到這用的時間統計功能,很自然想到的方法是在處理的方法前后各記錄一個時間,然后計算時間差,如下
DateTime start = DateTime.Now; SomeCodeToTime(); DateTime end = DateTime.Now; Console.WriteLine("Method took {0} ms", (end - start).TotalMilliseconds);
盡管使用DateTime的時間差可以達到目的,但DateTime統計出來的時間差並不是很精確,想要精確我們可以使用Win32 API調用PInvoke,但是做法非常麻煩而且容易出錯。
這時我們就需要使用StopWatch類了,使用這個類必須引用命名空間 System.Diagnostics
var timer = Stopwatch.StartNew(); SomeCodeToTime(); timer.Stop(); Console.WriteLine("Method took {0} ms", timer.ElapsedMilliseconds);
5 使用TimeSpan的靜態方法
當我們需要在程序中設置一個時間間隔或是等待多長時間后再做下一步操作時,往往會寫如下的代碼:
Thread.Sleep(50);
上面代碼中的參數50很明確是指50毫秒,這個是在定義方法的時候就定義好的類型,並不是很靈活,而且我們經常會使用int類型來定義傳入的參數,類型下面的代碼
void PerformRemoteWork(int timeout) { ... }
上面代碼中的timeout是指秒、毫秒還是小時或天,這個就需要開發者自己去定義了,這樣的代碼在調用時就非常不明確,我們可以使用TimeSpan來解決這個問題
void PerformRemoteWork(TimeSpan timeout) { ... }
調用的代碼
PerformRemoteWork(new TimeSpan(0, 0, 0, 0, 50));
這樣的代碼也讓人看着很頭疼,因為TimeSpan有5個構造函數的重載,如下
TimeSpan(); TimeSpan(long ticks); TimeSpan(int hours, int minutes, int seconds); TimeSpan(int days, int hours, int minutes, int seconds); TimeSpan(int days, int hours, int minutes, int seconds, int milliseconds);
由於這些重載在調用時很容易讓人混淆,而且寫出的代碼可讀性也不是很好,像上面的50如果不是很熟悉TimeSpan構造函數的並不能一眼就看出是50毫秒。更好的方法是使用TimeSpan的靜態方法
private static readonly TimeSpan _defaultTimeout=TimeSpan.FromSeconds(30);
上面的代碼的調用也可以寫成
PerformRemoteWork(TimeSpan.FromMilliseconds(50));
這樣無論是在寫程序時還是在閱讀代碼時都會感覺非常清楚明了。
1 string.IsNullOrEmpty() and string.IsNullOrWhiteSpace()
在Net2.0中String類型有一個靜態方法IsNullOrEmpty,到了Net4.0中String類又增加了一個新的靜態方法 IsNullOrWhiteSpace。這兩個方法看名稱也可以知道IsNullOrEmpty是判斷空引用和空字符串,而 IsNullOrWhiteSpace是判斷空引用和字符串中的每一個字符是否是空格。
在有這兩個方法之前,我們要進行這樣的判斷,需要些如下代碼
public string GetFileName(string fullPathFileName) { if (fullPathFileName == null || fullPathFileName.Length == 0) { throw new ArgumentNullException(fullPathFileName); } //... }
使用IsNullOrEmpty
public string GetFileName(string fullPathFileName) { if (string.IsNullOrEmpty(fullPathFileName)) { throw new ArgumentNullException(fullPathFileName); } //... }
下面又了新的需求,需要將三個名字連接在一起,並且希望中間名字不為空字符串和不出現多余的空格,我們會寫出下面的代碼
public string GetFullName(string firstName, string middleName, string lastName) { if (middleName == null || middleName.Trim().Length == 0) { return string.Format("{0} {1}", firstName, lastName); } return string.Format("{0} {1} {2}", firstName, middleName, lastName); }
上面的代碼中使用了Trim來去掉空格然后判斷其長度是否為0,代碼也非常的清晰簡潔,但是會產生額外的String對象以至於影響性能,這時就應該使用Net4.0中的IsNullOrWhiteSpace方法
public string GetFullName(string firstName, string middleName, string lastName) { if (string.IsNullOrWhiteSpace(middleName)) { return string.Format("{0} {1}", firstName, lastName); } return string.Format("{0} {1} {2}", firstName, middleName, lastName); }
上面的代碼非常簡潔,而且也不用擔心會產生額外的String對象沒有及時的進行垃圾回收而影響性能。
2 string.Equals()
string.Equals方法有很多的重載供我們使用,但是其中有些常常會被我們忽視掉。通常我們比較字符串會使用下面的方法
public Order CreateOrder(string orderType, string product, int quantity, double price) { if (orderType.Equals("equity")) { } // ... }
如果orderType為null會拋出NullReferenceException異常,所以為了不拋出異常,在判斷之前先要進行null的判斷,如下:
if (orderType != null && orderType.Equals("equity"))
相當於每次都要做兩次判斷,很麻煩而且有時還有可能遺忘,如果使用string.Equals就可以解決這個問題,代碼如下:
if (string.Equals(orderType, "equity"))
上面的代碼當orderType為null時不會拋出異常而是直接返回false。
判斷字符串相等的時候有時會需要區分大小寫,很多人的習慣做法是先轉換成大小或是小些在進行比較(建議轉換成大寫,因為編譯器做了優化可以提高性 能),但是當轉換成大寫或是小寫時又會創建的的字符串,使性能降低。這時應該使用 StringComparison.InvariantCultureIgnoreCase,代碼如下
if (orderType.Equals("equity", StringComparison.InvariantCultureIgnoreCase))
如果要考慮到null的情況,還是應該使用string.Equal
if (string.Equals(orderType, "equity", StringComparison.InvariantCultureIgnoreCase))
3 using語句
我們都知道using最常用的地方就是在類中引用命名空間。除此之外還可以用作設置別名和應用在一些實現了IDisposable 借口的對象實例上,可以使這些對象在using的作用范圍內自動釋放資源。下面的代碼示例是沒有使用using的情況:
public IEnumerable<Order> GetOrders() { var orders = new List<Order>(); var con = new SqlConnection("some connection string"); var cmd = new SqlCommand("select * from orders", con); var rs = cmd.ExecuteReader(); while (rs.Read()) { // ... } rs.Dispose(); cmd.Dispose(); con.Dispose(); return orders; }
上面的代碼不怎么好看,而且也沒有對異常的處理,如果在代碼執行過程中出現了異常將會導致有些資源不能及時釋放,盡管最終還是會被垃圾回收,但還是會影響性能呢。下面的代碼添加了異常處理
public IEnumerable<Order> GetOrders() { SqlConnection con = null; SqlCommand cmd = null; SqlDataReader rs = null; var orders = new List<Order>(); try { con = new SqlConnection("some connection string"); cmd = new SqlCommand("select * from orders", con); rs = cmd.ExecuteReader(); while (rs.Read()) { // ... } } finally { rs.Dispose(); cmd.Dispose(); con.Dispose(); } return orders; }
上面的代碼仍然存在不足,如果SqlCommand對象創建失敗或是拋出了異常,rs就會為null,那么最后在執行rs.Dispose()時就會拋出異常,會導致con.Dispose不能被調用,所以我們應該避免這種情況的發生
public IEnumerable<Order> GetOrders() { var orders = new List<Order>(); using (var con = new SqlConnection("some connection string")) { using (var cmd = new SqlCommand("select * from orders", con)) { using (var rs = cmd.ExecuteReader()) { while (rs.Read()) { // ... } } } } return orders; }
上面的代碼中的using嵌套了好幾層,看起來很繁瑣,而且可讀性也不是很好,我們可以像下面這樣改進
public IEnumerable<Order> GetOrders() { var orders = new List<Order>(); using (var con = new SqlConnection("some connection string")) using (var cmd = new SqlCommand("select * from orders", con)) using (var rs = cmd.ExecuteReader()) { while (rs.Read()) { // ... } } return orders; }
4 靜態類(Static)
很多人在創建類的時候沒有使用過Static修飾符,可能他們並不知道Static修飾符的作用,Static修飾符所做的一些限制可以在其他開發 人員使用我們代碼的時候使我們的代碼變得更加安全。比如我們現在寫一個XmlUtility類,這個類的作用是實現XML的序列化,代碼如下:
public class XmlUtility { public string ToXml(object input) { var xs = new XmlSerializer(input.GetType()); using (var memoryStream = new MemoryStream()) using (var xmlTextWriter = new XmlTextWriter(memoryStream, new UTF8Encoding())) { xs.Serialize(xmlTextWriter, input); return Encoding.UTF8.GetString(memoryStream.ToArray()); } } }
上面的是很典型的XML序列化代碼,但是我們在使用時需要先實例化這個類的對象,然后用對象來調用方法
var xmlUtil = new XmlUtility(); string result = xmlUtil.ToXml(someObject);
這樣顯然很麻煩,不過我們可以給方法加上static修飾符,然后給類加上私有的構造函數防止類實例化來使類的使用變得簡單
public class XmlUtility { private XmlUtility() { } public static string ToXml(object input) { var xs = new XmlSerializer(input.GetType()); using (var memoryStream = new MemoryStream()) using (var xmlTextWriter = new XmlTextWriter(memoryStream, new UTF8Encoding())) { xs.Serialize(xmlTextWriter, input); return Encoding.UTF8.GetString(memoryStream.ToArray()); } } }
上面的代碼可以實現類直接調用方法,但是給類設置私有構造函數的做法不是很好,當我們給類誤添加了非靜態方法時,類不能實例化,添加的非靜態方法就形同虛設了
public T FromXml<T>(string xml) { ... }
所以我們需要將類設置成靜態的,這樣當類中有非靜態方法時編譯時就會拋出異常,告訴我們類中只能包含靜態成員
public static class XmlUtility { public static string ToXml(object input) { var xs = new XmlSerializer(input.GetType()); using (var memoryStream = new MemoryStream()) using (var xmlTextWriter = new XmlTextWriter(memoryStream, new UTF8Encoding())) { xs.Serialize(xmlTextWriter, input); return Encoding.UTF8.GetString(memoryStream.ToArray()); } } }
給類添加Static修飾符,該類就只能包含靜態成員,並且不能被實例化,我們也不可能隨便就給添加了一個非靜態的成員,否則是不能編譯通過的。
5 對象和集合初始化器
在C#3.0及以上版本中增加了對象和集合初始化器的新特性,會使代碼看起來更加簡潔,還有可能帶來更高的性能。初始化器其實就是一個語法糖。看下面的例子,給出的是一個結構
public struct Point { public int X { get; set; } public int Y { get; set; } }
普通初始化如下
var startingPoint = new Point(); startingPoint.X = 5; startingPoint.Y = 13;
使用初始化器初始化
var startingPoint = new Point() { X = 5, Y = 13 };
代碼的確精簡了不少,從三行減到了一行,可以讓我們少敲很多字。
下面再來看看集合的初始化,假設我們在一個集合List中添加5個整數
var list = new List<int>(); list.Add(1); list.Add(7); list.Add(13); list.Add(42);
使用集合初始化器,代碼如下
var list = new List<int> { 1, 7, 13, 42 };
如果我們事先知道要加載的數量,可以給List設置默認的容量值,如下
var list = new List<int>(4) { 1, 7, 13, 42 };
下面來看一個通常情況下對象和集合一起使用的例子
var list = new List<Point>(); var point = new Point(); point.X = 5; point.Y = 13; list.Add(point); point = new Point(); point.X = 42; point.Y = 111; list.Add(point); point = new Point(); point.X = 7; point.Y = 9; list.Add(point);
下面為使用了初始化器的代碼,可以對比一下區別
var list = new List<Point> { new Point { X = 5, Y = 13 }, new Point { X = 42, Y = 111 }, new Point { X = 7, Y = 9 } };
使用對象或集合初始化器給我們帶來了非常簡潔的代碼,盡管有時一條語句會占用多行,但是可讀性是非常好的。
有些時候在性能上也會帶來提升,看下面兩個類
public class BeforeFieldInit { public static List<int> ThisList = new List<int>() { 1, 2, 3, 4, 5 }; } public class NotBeforeFieldInit { public static List<int> ThisList; static NotBeforeFieldInit() { ThisList = new List<int>(); ThisList.Add(1); ThisList.Add(2); ThisList.Add(3); ThisList.Add(4); ThisList.Add(5); } }
這兩個類都是做同樣的事情,都是創建一個靜態的List字段,然后添加了1到5五個整數。不同的是第一個類在生成的IL代碼中類上會添加beforefieldinit標記,對比兩個類生成的IL代碼
.class public auto ansi beforefieldinit BeforeFieldInit extends [mscorlib]System.Object { } // end of class BeforeFieldInit .class public auto ansi NotBeforeFieldInit extends [mscorlib]System.Object { } // end of class NotBeforeFieldInit
有關靜態構造函數的性能問題可以參考CLR Via C# 學習筆記(5) 靜態構造函數的性能
1 隱式類型
首先了解一下概念,隱式類型並不是動態類型,隱式類型是用關鍵字var來定義,var定義的類型仍然是強類型。
很多人認為使用隱式類型是懶惰的表現,剛開始我也是這么認為的,但是想想我使用STL中迭代指針的開發經理,我就明白了。看下面代碼:
for (list<int>::const_iterator it = myList.begin(); it != myList.end(); ++it) { // ... }
很多時候我們會寫出下面這樣的代碼
// pretty obvious ActiveOrdersDataAccessObject obj = new ActiveOrdersDataAccessObject(); // still obvious but even more typing Dictionary<string,List<Product>> productsByCategory = new Dictionary<string,List<Product>>();
上面的代碼的類型定義很明顯,是什么類型就用什么類型來定義,下面嘗試用var關鍵字來定義
// nicer! var obj = new ActiveOrdersDataAccessObject(); // Ah, so much nicer! var productsByCategory = new Dictionary<string,List<Product>>();
用var關鍵字后代碼變得簡潔多了,編譯器會在編譯時去推斷是什么類型,var關鍵字只相當於是一個占位符。
而且使用var關鍵字在我們使用泛型或是Linq表達式時會提供更好的可讀性,比較下面兩行代碼:
// 隱式類型 var results1 = from p in products where p.Value > 100 group p by p.Category; // 顯示類型 IEnumerable<IGrouping<string, Product>> results2 = from p in products where p.Value > 100 group p by p.Category;
2 Linq 擴展方法
在以前的編碼中,很多時候我們需要去寫一些自己的函數庫,如排序、分組、查找或是其他的一些算法。並且我們要花很多的時間來為這些函數寫單元測試,很多時候困擾我們的一些bug正是在這些方法中出現的。
隨着Linq擴展方法的推出,你可以使用現成的這些標准的算法,而不需要自己再去寫一遍,提供了極大的方便。需要排序可以使用OrderBy(), 當需要查詢條件時可以使用Where(),當需要選擇一些類的屬性時可以使用Select(),當需要分組查詢時可以使用GroupBy(),這些 Linq中的擴展方法經過了全面的測試,不需要我們來為他寫單元測試代碼,也不會出現讓人困擾的bug。
看下面的例子,假設有一個集合List<Product>,集合里裝載的是Product對象,Product有Value和Category兩個屬性,現在要按類別來查找Value值大於$100的數據,以前我們可能會像下面這樣寫
var results = new Dictionary<string, List<Product>>(); foreach (var p in products) { if (p.Value > 100) { List<Product> productsByGroup; if (!results.TryGetValue(p.Category, out productsByGroup)) { productsByGroup = new List<Product>(); results.Add(p.Category, productsByGroup); } productsByGroup.Add(p); } }
使用Linq擴展方法
var results = products
.Where(p => p.Value > 100)
.GroupBy(p => p.Category);
也可以像下面這樣寫
var results = from p in products where p.Value > 100 group p by p.Category;
3 擴展方法
擴展方法可以讓我們自己對一些類型進行方法的擴展,像上面講到的Linq的一些擴展方法。擴展方法是一個靜態方法,而且必須在一個靜態類中。看下面這個例子,編寫一個擴展方法將所以對象轉換成XML。
public static class ObjectExtensions { public static string ToXml(this object input, bool shouldPrettyPrint) { if (input == null) throw new ArgumentNullException("input"); var xs = new XmlSerializer(input.GetType()); using (var memoryStream = new MemoryStream()) using (var xmlTextWriter = new XmlTextWriter(memoryStream, new UTF8Encoding())) { xs.Serialize(xmlTextWriter, input); return Encoding.UTF8.GetString(memoryStream.ToArray()); } } }
需要注意的是,包含擴展方法的類必須為靜態類;擴展方法必須為靜態方法;方法的第一個參數必須在類型前面使用this關鍵字。下面看看怎樣調用該擴展方法
// can convert primatives to xml string intXml = 5.ToXml(); // can convert complex types to xml string objXml = employee.ToXml(); // can even call as static method if you choose: objXml = ObjectExtensions.ToXml(employee);
其實擴展方法只是一個語法糖,他可以使我們在類型上添加一些自己的方法。適當的使用擴展方法可以給我們的編碼帶來方便,但過度使用會適得其反,會使代碼易讀性變差,而且我們的只能提示項也會變得非常龐大。
4 System.IO.Path
Net中的System.IO.Path方法有很多的靜態方法來處理文件和路徑。很多時候我們嘗試手動的將路徑和文件名結合在一起而導致產生的文件路徑不可用,因為我們往往忽視了路徑后面可能有一個結尾符號‘\’。現在使用Path.Combine()方法可以避免這種錯誤
string fullPath = Path.Combine(workingDirectory, fileName);
假設現在有一個帶文件名的完整的路徑名,我們需要取其中的路徑、文件名或是文件的擴展名。Path類的很多靜態方法可以滿足我們的需要,如下
string fullPath = "c:\\Downloads\\output\\t0.html"; // gets "c:\" string pathPart = Path.GetPathRoot(fullPath); // gets "t0.html" string filePart = Path.GetFileName(fullPath); // gets ".html" string extPart = Path.GetExtension(fullPath); // gets "c:\downloads\output" string dirPart = Path.GetDirectoryName(fullPath);
所以當我們遇到這種需要對文件路徑進行操作時,我們可以去使用Path類的靜態方法。
5 泛型委托
如果你寫過或使用過帶事件的類,或是用過Linq的一些擴展方法,那么您很多可能直接或間接的使用過委托。委托可以以一種強有力的方式類創建一個類型,用來描述一個方法的簽名。在運行時來使用和調用這個方法。這個有點類似於C++中的函數指針。
委托最偉大的是比類的繼承有更好的重用性,假設你要設計一個緩存類,該類有一些方法供用戶調用,但是取決於緩存項是否過期或是被刪除了。你向用戶提供一個抽象方法,讓用戶去繼承類並重載該方法,這意味着增加了很多額外的工作。
如果使用委托,可以在被調用是在指定的方法中進行緩存項的過期檢查,可以傳遞或設置委托方法,匿名委托或是lambda表達式進行調用,這樣沒有必須創建子類,我們可以將類設置成密封的以防止任何意外的發生,這樣使類更加安全和有更好的可重用性。
那么這些和泛型委托有什么關系呢?現在有三個委托的基本“類型”反復的出現,而又不想去反復寫這些委托類型。就要使用泛型委托了,泛型委托還可以提高我們代碼的可讀性。下面是三個Net提供的泛型委托類型
關於上面三個泛型委托類型的詳細解釋和用法,可以點擊鏈接看MSDN
再回到剛才說到的緩存的例子,你希望該緩存接受一個緩存策略,並且有一個委托,委托的返回值來表示緩存是否過期,代碼如下:
public sealed class CacheItem<T> { public DateTime Created { get; set; } public DateTime LastAccess { get; set; } public T Value { get; set; } } public sealed class Cache<T> { private ConcurrentDictionary<string, CacheItem<T>> _cache; private Predicate<CacheItem<T>> _expirationStrategy; public Cache(Predicate<CacheItem<T>> expirationStrategy) { // set the delegate _expirationStrategy = expirationStrategy; } // ... private void CheckForExpired() { foreach (var item in _cache) { // call the delegate if (_expirationStrategy(item.Value)) { // remove the item... } } } }
現在就可以創建和使用緩存類了
var cache =
new Cache<int>(item => DateTime.Now - item.LastAccess > TimeSpan.FromSeconds(30));
事實上我們可以發揮我們的想象對緩存創建很多的過期策略,但不要去使用繼承。了解和使用泛型委托他會增加我們類的可重用性。
總結
本文是參考老外系列博客的寫的,並不是直譯,原文見下面鏈接。希望本文對大家有所幫助。