特別提醒:本文編寫時間是 2013 年,請根據目前 .NET 發展接收你所需的知識點。
本篇博文主要對asp.net mvc開發需要撐握的C#語言知識點進行簡單回顧,尤其是C# 3.0才有的一些C#語言特性。對於正在學asp.net mvc的童鞋,不防花個幾分鍾瀏覽一下。本文要回顧的C#知識點有:特性、自動屬性、對象集合初始化器、擴展方法、Lambda表達式和Linq查詢。C#資深“玩家”可路過。
1.特性(Attributes)
特性(Attributes),MSDN的定義是:公共語言運行時允許你添加類似關鍵字的描述聲明,叫做attributes, 它對程序中的元素進行標注,如類型、字段、方法和屬性等。Attributes和Microsoft .NET Framework文件的元數據保存在一起,可以用來向運行時描述你的代碼,或者在程序運行的時候影響應用程序的行為。
例如,在一個方法前標注[Obsolete]特性,則調用該方法時VS則會提示該方法已過期的警告,如下圖:
又如,在.Net Remoting的遠程對象中,如果要調用或傳遞某個對象,例如類,或者結構,則該類或結構則必須標注[Serializable]特性。還有,我們在構建XML Web服務時用得很多的一個特性就是[WebMegthod],它可讓通過HTTP請求的公開方法的返回值編碼成XML進行傳遞。
特性實際上就是一個類,[Obsolete]特性的實際類名是ObsoleteAttribute,但我們在標注的時候可以不帶Attribute后綴,系統在名稱轉換時會自動給我們加上。
上面說的都是些.NET系統定義的一些特性,當然還有很多。了解如何自定義特性,有利有我們更好的在ASP.NET MVC編程使用特性,比如給Model類的屬性標注特性來驗證表單輸入的合法性(以后進行介紹)。
下面我們來模擬一個ASP.NET MVC經常要用到的StringLenth特性,它用於判斷用戶輸入是否超出長度限制。我們現在來模擬它。先定義一個MyStringLenth特性:
// 用戶自定義的帶有可選命名參數的 MyStringLenthAttribute 特性類。 // 該特性通過AttributeUsage限制它只能用在屬性和字段上。 [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public sealed class MyStringLenthAttribute : Attribute { public MyStringLenthAttribute(string displayName, int maxLength) { this.MaxLength = maxLength; this.DisplayName = displayName; } //顯示的名稱,對外是只讀的,所以不能通過可選參數來賦值,必須在構造函數中對其初始化。 public string DisplayName { get; private set; } //長度最大值,對外是只讀的,所以不能通過可選參數來賦值,必須在構造函數中對其初始化。 public int MaxLength { get; private set; } //錯誤信息,標注時可作為可選命名參數來使用。 public string ErrorMessage { get; set; } //長度最小值,標注時可作為可選命名參數來使用。 public int MinLength { get; set; } }
上面若不加AttributeUsage限制,特性可以聲明在類型(如結構、類、枚舉、委托)和成員(如方法,字段,事件,屬性,索引)的前面。
然后我們把這個特性應用在下面的Order類之上:
// 應用自定義MyStringLenth特性於Order類的OrderID屬性之上。MinLength和ErrorMessage是命名參數。 public class Order { [MyStringLenth("訂單號", 6,MinLength = 3, ErrorMessage = "{0}的長度必須在{1}和{2}之間,請重新輸入!")] public string OrderID { get; set; } }
最后我們看看如何使用MyStringLenth特性驗證用戶輸入字符串的長度:
//檢查成員字符串長度是否越限。 private static bool IsMemberValid(int inputLength, MemberInfo member) { foreach (object attribute in member.GetCustomAttributes(true)) { if (attribute is MyStringLenthAttribute) { MyStringLenthAttribute attr=(MyStringLenthAttribute)attribute; string displayName = attr.DisplayName; int maxLength = attr.MaxLength; int minLength = attr.MinLength; string msg = attr.ErrorMessage; if (inputLength < minLength || inputLength > maxLength) { Console.WriteLine(msg, displayName, minLength, maxLength); return false; } else { return true; } } } return false; } //驗證輸入是否合法 private static bool IsValid(Order order) { if (order == null) return false; foreach (PropertyInfo p in typeof(Order).GetProperties()) { if (IsMemberValid(order.OrderID.Length, p)) return true; } return false; } public static void Main() { string input=string.Empty; Order order; do { Console.WriteLine("請輸入訂單號:"); input = Console.ReadLine(); order = new Order { OrderID = input }; } while (!IsValid(order)); Console.WriteLine("訂單號輸入正確,按任意鍵退出!"); Console.ReadKey(); }
輸出效果如下:
2.自動屬性
在 C# 3.0 和更高版本中,當屬性的訪問器中不需要其他邏輯時,自動實現的屬性可使屬性聲明更加簡潔。
下面示例演示了屬性的標准實現和自動實現:
class Program { class Person { //標准實現的屬性 int _age; public int Age { get { return _age; } set { if (value < 0 || value > 130) { Console.WriteLine("設置的年齡有誤!"); return; } _age = value; } } //自動實現的屬性 public string Name { get; set; } } static void Main(string[] args) { Person p = new Person(); p.Age = 180; p.Name = "小王"; Console.WriteLine("{0}今年{1}歲。",p.Name,p.Age); Console.ReadKey(); } }
自動屬性也可以有不同的訪問權限,如:
public string Name { get;private set; }
注意,自動屬性不能定義只讀或者只寫的屬性,必須同時提供get和set訪問器:
public string Name { get; }//編譯出錯 public string PetName { set; }//編譯出錯
3.對象和集合的初始化器
上面我們演示自動屬性的時候給對象的實例初始化時是一個一個屬性進行賦值的,有多少個屬性就需要多少句代碼。C# 3.0和更高版本中有了對象集合初始化器,有了它,只需一句代碼就可初始化一個對象或一個對象集合的所有屬性。這在里先創建一個“商品”類,用於后面的示例演示:
/// <summary> /// 商品類 /// </summary> public class Product { /// <summary> /// 商品編號 /// </summary> public int ProductID { get; set; } /// <summary> /// 商品名稱 /// </summary> public string Name { get; set; } /// <summary> /// 商品描述 /// </summary> public string Description { get; set; } /// <summary> /// 商品價格 /// </summary> public decimal Price { get; set; } /// <summary> /// 商品分類 /// </summary> public string Category { set; get; } }
基於上面定義好的商品類,下面代碼演示了如何通過初始化器來創建商品類的實例對象和集合:
static void Main(string[] args) { //對象初始化器的使用 (可只給部分字段賦值) Product product = new Product { ProductID = 1234, Name = "西瓜", Price = 2.3M };//創建並初始化一個實例 //集合初始化器的使用 List<Product> proList = new List<Product> { new Product { ProductID = 1234, Name = "西瓜", Price = 2.3M }, new Product { ProductID = 2345, Name = "蘋果", Price = 5.9M }, new Product { ProductID = 3456, Name = "櫻桃", Price = 4.6M } }; //打印 Console.WriteLine("對象初始化器:{0} {1} {2}", product.ProductID, product.Name, product.Price); foreach (Product p in proList) { Console.WriteLine("集合初始化器:{0} {1} {2}", p.ProductID, p.Name, p.Price); } Console.ReadKey(); }
另外還有一些其它類型也可以使用初始化器,如下:
//數組使用初始化器 string[] fruitArray = {"apple","orange","plum" }; //匿名類型使用初始化器 var books = new { Title = "ASP.NET MVC 入門", Author = "小王", Price = 20 }; //字典類型使用初始化器 Dictionary<string, int> fruitDic = new Dictionary<string, int>() { { "apple", 10 }, { "orange", 20 }, { "plum", 30 } };
4.擴展方法
擴展方法使您能夠向現有類型“添加”方法,而無需創建新的派生類型或修改原始類型。擴展方法是一種特殊的靜態方法,但可以像擴展類型上的實例方法一樣進行調用。例如,我們可以讓Random類的所有實例對象擁有一個返回隨機bool值的方法。我們不能對Random類本身進行修改,但可以對它進行擴展,如下代碼所示:
static class Program { /// <summary> /// 隨機返回 true 或 false /// </summary> /// <param name="random">this參數自動指定到Random的實例</param> /// <returns></returns> public static bool NextBool(this Random random) { return random.NextDouble() > 0.5; } static void Main(string[] args) { //調用擴展方法 Random rd = new Random(); bool bl = rd.NextBool(); Console.WriteLine(bl.ToString()); Console.ReadKey(); } }
注意,擴展方法必須在非泛型的靜態類中定義,上面的Program類如不加static修飾符則會報錯。
我們可以創建一個接口的擴展方法,這樣實現該接口的類都可以調用該擴展方法。看下面一個完整示例:
/// <summary> /// 購物車類 (實現 IEnumerable<Product> 接口) /// </summary> public class ShoppingCart : IEnumerable<Product> { public List<Product> Products { get; set; } public IEnumerator<Product> GetEnumerator() { return Products.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } /// <summary> /// 定義一個靜態類,用於實現擴展方法(注意:擴展方法必須定義在靜態類中) /// </summary> public static class MyExtensionMethods { /// <summary> /// 計算商品總價錢 /// </summary> public static decimal TotalPrices(this IEnumerable<Product> productEnum) { decimal total = 0; foreach (Product prod in productEnum) { total += prod.Price; } return total; } } class Program { static void Main(string[] args) { // 創建並初始化ShoppingCart實例,注入IEnumerable<Product> IEnumerable<Product> products = new ShoppingCart { Products = new List<Product> { new Product {Name = "Kayak", Price = 275}, new Product {Name = "Lifejacket", Price = 48.95M}, new Product {Name = "Soccer ball", Price = 19.50M}, new Product {Name = "Corner flag", Price = 34.95M} } }; // 創建並初始化一個普通的Product數組 Product[] productArray = { new Product {Name = "Kayak", Price = 275M}, new Product {Name = "Lifejacket", Price = 48.95M}, new Product {Name = "Soccer ball", Price = 19.50M}, new Product {Name = "Corner flag", Price = 34.95M} }; // 取得商品總價錢:用接口的方式調用TotalPrices擴展方法。 decimal cartTotal = products.TotalPrices(); // 取得商品總價錢:用普通數組的方式調用TotalPrices擴展方法。 decimal arrayTotal = productArray.TotalPrices(); Console.WriteLine("Cart Total: {0:c}", cartTotal); Console.WriteLine("Array Total: {0:c}", arrayTotal); Console.ReadKey(); } }
執行后輸出如下結果:
5.Lambda 表達式
Lambda 表達式和匿名函數其實是一件事情。不同是,他們語法表現形式不同,Lambda 表達式在語法上實際上就是匿名函數的簡寫。直接介紹匿名函數和Lambda表達式的用法沒什么意思,在這里,我要根據實際應用來講一個兩者用法的例子,這樣在介紹知識點的同時也能和大家分享一下解決問題的思想。
假如我們要實現一個功能強大的商品查詢方法,這個商品查詢方法如何查詢商品是可以由用戶自己來決定的,用戶可以根據價格來查詢商品,也可以根據分類來查詢商品等等,也就是說用戶可以把自己的查詢邏輯傳遞給這個查詢方法。要編寫這樣一個方法,我們很自然的會想到用一個委托來作為這個方法的參數,這個委托就是用戶處理商品查詢的邏輯。 我們不防把這個查詢方法稱為“商品查詢器”。我們可以用靜態的擴展方法來實現這個“商品查詢器“,這樣每個商品集合對象(如 IEnumerable<Product> products)可以直接調用該靜態方法返回查詢結果。解決問題的思想有了,接下來就是實現了。或許你對這一段描述有點蒙,結合代碼可能讓你更清晰。下面是這個“商品查詢器”-Filter方法的實現代碼:
/// <summary> /// 定義一個靜態類,用於實現擴展方法 /// </summary> public static class MyExtensionMethods { /// <summary> /// 商品查詢器 /// </summary> /// <param name="productEnum">擴展類型的實例引用</param> /// <param name="selectorParam">一個參數類型為Product,返回值為bool的委托</param> /// <returns>查詢結果</returns> public static IEnumerable<Product> Filter(this IEnumerable<Product> productEnum, Func<Product, bool> selectorParam) { foreach (Product prod in productEnum) { if (selectorParam(prod)) { yield return prod; } } } }
沒錯,我們就是用這么簡短的Filter方法來滿足各種需求的查詢。上面Product類使用的是前文定義的。這里也再一次見證了擴展方法的功效。為了演示Filter查詢方法的調用,我們先來造一批數據:
static void Main(string[] args) { // 創建商品集合 IEnumerable<Product> products = new ShoppingCart { Products = new List<Product> { new Product {Name = "西瓜", Category = "水果", Price = 2.3M}, new Product {Name = "蘋果", Category = "水果", Price = 4.9M}, new Product {Name = "ASP.NET MCV 入門", Category = "書籍", Price = 19.5M}, new Product {Name = "ASP.NET MCV 提高", Category = "書籍", Price = 34.9M} } }; }
接下來我們繼續在上面Main方法中來調用查詢方法Filter:
//用匿名函數定義一個具體的查詢需求 Func<Product, bool> fruitFilter = delegate(Product prod) { return prod.Category == "水果"; }; //調用Filter,查詢分類為“水果”的商品 IEnumerable<Product> filteredProducts = products.Filter(fruitFilter); //打印結果 foreach (Product prod in filteredProducts) { Console.WriteLine("商品名稱: {0}, 單價: {1:c}", prod.Name, prod.Price); } Console.ReadKey();
輸出結果為:
上面我們使用的是委托和匿名函數來處理用戶查詢邏輯,並把它傳遞給Filter方法,滿足了前面所說的需求。但若使用Lambda表達式代替上面的匿名函數能使上面的代碼看上去更簡潔更人性化,如下代碼所示:
Func<Product, bool> fruitFilter = prod => prod.Category == "水果"; IEnumerable<Product> filteredProducts = products.Filter(fruitFilter);
沒有了delegate關鍵字,沒有了大小括號,看上去更舒服。當然上面兩行代碼可以繼續簡化為一行:
IEnumerable<Product> filteredProducts = products.Filter(prod => prod.Category == "水果");
這三種方式輸出結果都是一樣的。然后,我們還可以通過Lambda表達式實現各種需求的查詢:
//查詢分類為“水果”或者單價大於30元的商品 IEnumerable<Product> filteredProducts = products.Filter(prod => prod.Category == "水果" || prod.Price > 30 );
通過這個示例,相信大家已經清晰的了解並撐握了Lambda表達式的簡單應用,而這就足夠了:)。
6.LINQ
最后簡單回顧一下LINQ。LINQ(Language Integrated Query語言集成查詢)是 VS 2008 和 .NET Framework 3.5 版中一項突破性的創新,它在對象領域和數據領域之間架起了一座橋梁。
上面講Lambda表達式時,用到的查詢結果集的方式未免還是有點麻煩(因為自定義了一個Filter擴展方法),而Linq本身就集合了很多擴展方法,我們可以直接使用,大大的簡化了編寫查詢代碼的工作。例如,對於這樣一個數據集合:
Product[] products = { new Product {Name = "西瓜", Category = "水果", Price = 2.3M}, new Product {Name = "蘋果", Category = "水果", Price = 4.9M}, new Product {Name = "空心菜", Category = "蔬菜", Price = 2.2M}, new Product {Name = "地瓜", Category = "蔬菜", Price = 1.9M} };
如果要查詢得到價錢最高的三個商品信息,如果不使用Linq,我們可能會先寫一個排序方法,對products根據價錢由高到低進行排序,排序時需要創建一個新的Product[]對象用於存儲排序好的數據。但用Linq可大大減少工作量,一兩句代碼就能搞定。如下代碼所示,查出價錢最高的三個商品:
var results = from product in products orderby product.Price descending select new { product.Name, product.Price }; //打印價錢最高的三個商品 int count = 0; foreach (var p in results) { Console.WriteLine("商品:{0},價錢:{1}", p.Name, p.Price); if (++count == 3) break; } Console.ReadKey();
輸出結果:
能熟練使用Linq是一件很爽的事情。上面的Linq語句和我們熟悉的SQL查詢語句類似,看上去非常整潔且易懂。但並不是每一種SQL查詢語句在C#都有對應的關鍵字,有時候我們需要使用另外一種Linq查詢方式,即“點號”方式的Linq查詢方式,這種方式中的Linq查詢方法都是擴展方法。如下面這段代碼和上面實現的效果是一樣的:
var results = products .OrderByDescending(e => e.Price) .Take(3) .Select(e => new { e.Name,e.Price}); foreach (var p in results) { Console.WriteLine("商品:{0},價錢:{1}", p.Name, p.Price); } Console.ReadKey();
雖然類SQL的Linq查詢方式比這種方式看上去更一目了然,但並不是每一種SQL查詢語句在C#都有對應的關鍵字,比如這里的Take擴展方法就是類SQL的Linq查詢語法沒有的功能。
注意,有些Linq擴展方法分為“延后查詢”(deferred)和“即時查詢”(immediate)。延后查詢意思是擁有“延后查詢”擴展方法的Linq語句只有當調用結果集對象的時候才開始真正執行查詢,即時查詢則是立即得到結果。比如上面的Linq語句的OrderByDescending擴展方法就是一個“延后查詢”方法,當程序執行到Linq語句定義部分時並沒有查詢出結果並放到results對象中,而是當程序執行到foreach循環時才真正執行Linq查詢語句得到查詢結果。我們可以做個測試,在Ling語句之后,我們再將products[1]對象重新賦值,如下代碼所示:
var results = products .OrderByDescending(e => e.Price) .Take(3) .Select(e => new { e.Name, e.Price }); //在Linq語句之后對products[1]重新賦值 products[1] = new Product { Name = "榴蓮", Category = "水果", Price = 22.6M }; //打印 foreach (var p in results) { Console.WriteLine("商品:{0},價錢:{1}", p.Name, p.Price); } Console.ReadKey();
輸出結果為:
我們發現results是重新賦值之后的結果。可想而知,查詢語句是在results被調用之后才真正執行的。
Linq非常強大也非常好用,這里只是把它當作一個學習ASP.NET MVC之前需掌握的知識點進行簡單回顧。要靈活熟練地使用Linq還需要經常使用才行。
參考:
《Pro ASP.NET MVC 3 Framework》