[ASP.NET MVC 小牛之路]02 - C#知識點提要

Liam Wang發表於2013-07-25

特別提醒:本文編寫時間是 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》

相關文章