Pro ASP.Net Core MVC 6th 第四章

第四章 C# 關鍵特徵

在本章中,我描述了Web應用程序開發中使用的C#特徵,這些特徵還沒有被普遍理解或常常引發混淆。 這不是關於C#的書,可是,我僅爲每一個特徵提供一個簡單的例子,以便您能夠按照本書其他部分的示例,並在本身的項目中利用他們。 表4-1總結了本章的內容。html

表4-1 本章彙總程序員

問題
解決方案
代碼示例編程

避免訪問空引用屬性
使用空條件操做符
6-9json

簡化C# 屬性
使用自動實現的屬性
10-12數組

簡化字符串構造
使用字符串插值
13瀏覽器

在單個步驟內建立對象並賦值
使用對象或集合初始化器
14-17安全

給類添加功能而無需修改類
使用擴展方法
18-25服務器

單語句方法與簡化委託
使用lambda表達式
26-33app

使用隱含類型
使用var關鍵字
34異步

建立對象無需定義類型
使用匿名類型
35-36

簡化異步方法的使用
使用async和await關鍵字
37-40

取得類或屬性的名字無需定義靜態字符串
使用nameof 表達式
41-42

準備示例項目

在本章中,我使用ASP.NET Core Web應用程序(.NET Core)模板建立了一個名爲LanguageFeatures的新Visual Studio項目。 不要選擇「添加應用程序洞察到項目」選項,而後單擊肯定按鈕,如圖4-1所示。

fig.4-1

圖4-1 選擇項目類型

當顯示不一樣的ASP.NET項目配置時,我選擇了空模板,如圖4-2所示,而後單擊肯定按鈕建立項目。

fig.4-2

圖4-2 選擇初始項目內容

啓用ASP.NET Core MVC

空項目模板建立一個包含最小ASP.NET Core配置而沒有任何MVC支持的項目。 這意味着Web應用程序模板添加的佔位符內容不存在,但這也意味着須要一些額外的步驟來啓用MVC,以便控件和視圖等功能能夠工做。 在本節中,我進行了修改,以在項目中添加啓用MVC設置,但我不會詳細介紹每一個步驟目前的功能。 第一步是添加.NET MVC程序集,它在project.json文件的依賴項部分完成,如代碼4-1所示。

Listing 4-1. 在文件project.json 中添加.NET MVC程序集

"dependencies": { "Microsoft.NETCore.App": { "version": "1.0.0", "type": "platform" }, "Microsoft.AspNetCore.Diagnostics": "1.0.0", "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", "Microsoft.Extensions.Logging.Console": "1.0.0", "Microsoft.AspNetCore.Mvc": "1.0.0" },

project.json文件的依賴關係部分列出了項目所需的程序集。 我添加了包含MVC類的Microsoft.AspNetCore.Mvc程序集。 請注意,在添加Microsoft.AspNetCore.Mvc程序集的上一行要添加行末尾的逗號。 JSON配置文件對正確的格式要求很嚴格的,若是忘記添加逗號,則會產生錯誤。

提示:使用每一個程序集都要指定版本號。 您必須確保您指定的全部程序集版本都能在一塊兒工做。 當您編輯project.json文件時,Visual Studio將提供可用的程序集版本列表,最簡單的方法是確保您爲Microsoft.AspNetCore.Mvc指定的版本與現有Visual Studio在建立項目時添加的依賴項部分的程序集的版本相同 。

下一步是告訴ASP.NET使用MVC,這是在Startup類中完成的,如代碼4-2所示。

Listing 4-2. Startup.cs 啓用ASP.NET MVC

using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace LanguageFeatures { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddMvc(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { app.UseMvcWithDefaultRoute(); } } }

我將在第14章中解釋如何配置ASP.NET Core MVC應用程序,但代碼4-2中添加的兩個語句提供了使用默認配置和約定的基本MVC設置。

建立 MVC 應用組件

如今MVC已經創建起來,我能夠添加我將用來演示重要C#語言特徵的MVC應用程序組件。

創建模型

我開始建立一個簡單的模型類,以便我可使用一些數據來處理。 我添加了一個名爲Models的文件夾,並在其中建立了一個名爲Product.cs的類文件,類定義如代碼4-3所示。

Listing 4-3. Product.cs

namespace LanguageFeatures.Models { public class Product { public string Name { get; set; } public decimal? Price { get; set; } public static Product[] GetProducts() { Product kayak = new Product { Name = "Kayak", Price = 275M }; Product lifejacket = new Product { Name = "Lifejacket", Price = 48.95M }; return new Product[] { kayak, lifejacket, null }; } } }

Products類定義名稱和價格屬性,而且有一個名爲GetProducts的靜態方法返回Products數組。 由GetProducts方法返回的數組中包含的元素之一設置爲null。

創建控制器和視圖

對於本章中的示例,我使用一個簡單的控制器來演示不一樣的語言特性。 我建立了一個Controllers文件夾,並添加了一個名爲HomeController.cs的類文件,其內容如代碼4-4所示。 當使用默認的MVC配置時,MVC將發送HTTP請求給Home控制器。

Listing 4-4. HomeController.cs

using Microsoft.AspNetCore.Mvc; namespace LanguageFeatures.Controllers { public class HomeController : Controller { public ViewResult Index() { return View(new string[] { "C#", "Language", "Features" }); } } }

Index 方法告訴MVC渲染默認視圖並傳遞一個字符串數組,轉成發送給客戶端的HTML。 要建立相應的視圖,我添加了一個Views/Home文件夾(經過建立一個Views文件夾,而後在其中添加一個Home文件夾),並添加了一個名爲Index.cshtml的視圖文件,其內容如代碼4-5所示。

Listing 4-5. Index.cshtml

@model IEnumerable<string> @{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Language Features</title> </head> <body> <ul> @foreach (string s in Model) { <li>@s</li> } </ul> </body> </html>

運行示例應用程序,您將看到如圖4-3所示的輸出。

fig.4-3

因爲本章中全部示例的輸出都是文本,我將顯示瀏覽器顯示的消息,以下所示:


C# Language Features

使null條件運算符

null條件運算符容許更優雅地檢測空值。 在肯定請求是否包含特定頭或值或模型是否包含特定數據項時,要對MVC開發中的空值進行大量判斷。 傳統上,處理null須要進行明確的檢查,當對象及其屬性必須被檢查時,這可能變得煩人。 null條件運算符使此過程更簡單,更簡潔,如代碼4-6所示。

Listing 4-6. 檢測空值

using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; namespace LanguageFeatures.Controllers { public class HomeController : Controller { public ViewResult Index() { List<string> results = new List<string>(); foreach (Product p in Product.GetProducts()) { string name = p?.Name; decimal? price = p?.Price; results.Add(string.Format("Name: {0}, Price: {1}", name, price)); } return View(results); } } }

Product類定義的靜態GetProducts方法返回我在控制器的Index操做方法中檢查的對象數組,以獲取名稱和價格值的列表。 問題是數組中的對象和屬性的值均可覺得null,這意味着我不能在foreach循環中簡單地引用p.Name或p.Price,這會引發NullReferenceException。 爲了不這種狀況,我使用null條件運算符,像這樣:

... string name = p?.Name ; decimal? price = p?.Price ; ...

空條件運算符是單個問號(?字符)。 若是p爲空,則名稱也將設置爲null。 若是p不爲空,那麼name將被設置爲Person.Name屬性的值。 價格屬性受同一測試。 請注意,使用null條件運算符時分配的變量必須可以被分配爲null,這就是爲何價格變量被聲明爲可空的decimal(decimal?)。

連接空條件運算符

空條件運算符能夠連接在一塊兒以導航對象的層次結構,這是真正成爲簡化代碼並容許安全導航的有效工具。 在代碼4-7中,我已經將一個屬性添加到產品類中,從而建立了一個更復雜的對象層次結構。

Listing 4-7. 增長Product中的屬性

namespace LanguageFeatures.Models { public class Product { public string Name { get; set; } public decimal? Price { get; set; } public Product Related { get; set; } public static Product[] GetProducts() { Product kayak = new Product { Name = "Kayak", Price = 275M }; Product lifejacket = new Product { Name = "Lifejacket", Price = 48.95M }; kayak.Related = lifejacket; return new Product[] { kayak, lifejacket, null }; } } }

每一個Product對象都有一個能夠引用另外一個Product對象的「相關」屬性。 在GetProducts方法中,我設置了表明皮划艇的Product對象的「相關」屬性。 代碼4-8顯示瞭如何將null條件運算符連接在一塊兒,以導航對象屬性而不引發異常。

Listing 4-8. 檢查嵌套的空值

using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; namespace LanguageFeatures.Controllers { public class HomeController : Controller { public ViewResult Index() { List<string> results = new List<string>(); foreach (Product p in Product.GetProducts()) { string name = p?.Name; decimal? price = p?.Price; string relatedName = p?.Related?.Name; results.Add(string.Format("Name: {0}, Price: {1}, Related: {2}", name, price, relatedName)); } return View(results); } } }

null條件運算符能夠應用於屬性鏈的每一個部分,以下所示:

... string relatedName = p?.Related? .Name; ...

結果是當p爲空或p.Related爲空時,relatedName變量爲空。 不然,將爲變量分配p.Related.Name屬性的值。 若是運行該示例,您將在瀏覽器窗口中看到如下輸出:

Name: Kayak, Price: 275, Related: Lifejacket Name: Lifejacket, Price: 48.95, Related: Name: , Price: , Related:

結合條件和合並運算符

將空條件運算符(單個問號)與空合併運算符(兩個問號)組合以設置回退值以顯示應用程序中使用的空值可能很是有用,如清單4-9所示。

Listing 4-9. 合併 Null運算符

using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; namespace LanguageFeatures.Controllers { public class HomeController : Controller { public ViewResult Index() { List<string> results = new List<string>(); foreach (Product p in Product.GetProducts()) { string name = p?.Name ?? "<No Name>"; decimal? price = p?.Price ?? 0; string relatedName = p?.Related?.Name ?? "<None>"; results.Add(string.Format("Name: {0}, Price: {1}, Related: {2}", name, price, relatedName)); } return View(results); } } }

空條件運算符確保在導航對象屬性時不會獲得NullReferenceException,而且空合併運算符確保在瀏覽器中顯示的結果中不包括空值。 若是運行該示例,您將在瀏覽器窗口中看到如下結果:

Name: Kayak, Price: 275, Related: Lifejacket Name: Lifejacket, Price: 48.95, Related: <None> Name: <No Name> , Price: 0 , Related: <None>

使用自動實現的屬性

C#支持自動實現的屬性,我在上一節中定義Person類的屬性時使用它們,以下所示:

namespace LanguageFeatures.Models { public class Product { public string Name { get; set; } public decimal? Price { get; set; } public Product Related { get; set; } public static Product[] GetProducts() { Product kayak = new Product { Name = "Kayak", Price = 275M }; Product lifejacket = new Product { Name = "Lifejacket", Price = 48.95M }; kayak.Related = lifejacket; return new Product[] { kayak, lifejacket, null }; } } }

此功能容許我定義屬性,而沒必要實現get和set主體。 使用自動實現的屬性功能定義以下屬性:

... public string Name { get; set; } ... is equivalent to the following code: ... public string Name { get { return name; } set { name = value; } } ...

這種類型的特徵叫作語法糖,它能使C#更加愉快地工做 - 在這種狀況下,它能消除最終被複制的冗餘代碼,而不會大大改變語言的行爲方式。 術語"糖"可能看起來很貶義,可是使代碼更容易編寫和維護的任何加強功能都是有益的,特別是在大型複雜項目中。

使用自動實現的屬性初始化器

自C#3.0以來一直支持自動實現的屬性。 最新版本的C#支持自動實現的屬性的初始化器,容許設置初始值,而沒必要使用構造函數,如代碼清單4-10所示。

Listing 4-10. 使用自動實現的屬性初始化器

namespace LanguageFeatures.Models { public class Product { public string Name { get; set; } public string Category { get; set; } = "Watersports"; public decimal? Price { get; set; } public Product Related { get; set; } public static Product[] GetProducts() { Product kayak = new Product { Name = "Kayak", Category = "Water Craft", Price = 275M }; Product lifejacket = new Product { Name = "Lifejacket", Price = 48.95M }; kayak.Related = lifejacket; return new Product[] { kayak, lifejacket, null }; } } }

將值分配給自動實現的屬性不會阻止setter更改屬性,而且只需對構造函數的簡單類型的代碼進行整理,以提供默認值。 在該示例中,初始化程序爲「類別」屬性分配了一個「Watersports」值。當我建立皮划艇對象時並須要指定一個值的水工藝時,我會作改變初始值。

建立只讀的自動實現的屬性

您能夠經過使用初始化程序建立只讀屬性,屬性中省略set關鍵字便可,如清單4-11所示。

Listing 4-11. 建立只讀屬性

namespace LanguageFeatures.Models { public class Product { public string Name { get; set; } public string Category { get; set; } = "Watersports"; public decimal? Price { get; set; } public Product Related { get; set; } public bool InStock { get; } = true; public static Product[] GetProducts() { Product kayak = new Product { Name = "Kayak", Category = "Water Craft", Price = 275M }; Product lifejacket = new Product { Name = "Lifejacket", Price = 48.95M }; kayak.Related = lifejacket; return new Product[] { kayak, lifejacket, null }; } } }

InStock屬性初始化爲true,不能更改; 可是,該值能夠在類型的構造函數中分配,如清單4-12所示。

Listing 4-12. 給只讀屬性賦值

namespace LanguageFeatures.Models { public class Product { public Product(bool stock = true) { InStock = stock; } public string Name { get; set; } public string Category { get; set; } = "Watersports"; public decimal? Price { get; set; } public Product Related { get; set; } public bool InStock { get; } public static Product[] GetProducts() { Product kayak = new Product { Name = "Kayak", Category = "Water Craft", Price = 275M }; Product lifejacket = new Product(false) { Name = "Lifejacket", Price = 48.95M }; kayak.Related = lifejacket; return new Product[] { kayak, lifejacket, null }; } } }

構造函數容許將只讀屬性的值指定爲參數,若是未提供值,則默認爲true。 由構造函數設置後,屬性值不能更改。

使用字符串插值

string.Format方法是用於編寫包含數據值的字符串的傳統C#工具。 下面是Home控制器中使用這種技術的一個例子:

... results.Add( string.Format("Name: {0}, Price: {1}, Related: {2}", name, price, relatedName) ); ...

C#6.0添加了對不一樣方法的支持,稱爲字符串插值,無需確保字符串模板中的{0}引用與指定爲參數的變量匹配。 相反,字符串插入直接使用變量名,如代碼4-13所示。

Listing 4-13. 使用字符串插值

using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; namespace LanguageFeatures.Controllers { public class HomeController : Controller { public ViewResult Index() { List<string> results = new List<string>(); foreach (Product p in Product.GetProducts()) { string name = p?.Name ?? "<No Name>"; decimal? price = p?.Price ?? 0; string relatedName = p?.Related?.Name ?? "<None>"; results.Add($"Name: {name}, Price: {price}, Related: {relatedName}"); } return View(results); } } }

內插字符串前綴爲$字符,幷包含空格,這些引用是{和}字符中包含的值的引用。 當字符串被求值時,這些位置用指定的變量或常量的當前值填充。 Visual Studio爲建立內插字符串提供了IntelliSense支持,並提供了{字符鍵入時可用成員的列表; 這有助於最小化打字錯誤,結果是更容易理解的字符串格式。

提示:字符串插值支持string.Format方法中可用的全部格式。 格式指定做爲佔位符(hole)的一部分包括在內,所以$「Price:{price:C2}」將格式化Price的值做爲具備兩位十進制數的貨幣值。

使用對象和集合初始化器

當我在Product類的靜態GetProducts方法中建立一個對象時,我使用一個對象初始化器,它容許我建立一個對象並同時指定它的屬性值,以下所示: Product kayak = new Product { Name = "Kayak", Category = "Water Craft", Price = 275M }; ...

這是另外一種使C#更容易使用的語法糖功能。 沒有這個功能,我必須調用Product構造函數,而後使用新建立的對象來設置每一個屬性,以下所示:

... Product kayak = new Product(); kayak.Name = "Kayak"; kayak.Category = "Water Craft"; kayak.Price = 275M; ...

與此相關的功能是集合初始化器,它容許建立在單一步驟中指定的集合及其內容。 沒有初始化程序,例如,建立一個字符串數組須要單獨指定數組和數組元素的大小,如清單4-14所示。

Listing 4-14. 初始化對象

using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; namespace LanguageFeatures.Controllers { public class HomeController : Controller { public ViewResult Index() { string[] names = new string[3]; names[0] = "Bob"; names[1] = "Joe"; names[2] = "Alice"; return View("Index", names); } } }

使用集合初始化程序容許將數組的內容指定爲構造的一部分,這將爲編譯器提供數組的大小,如清單4-15所示。

Listing 4-15. 使用集合初始化器

using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; namespace LanguageFeatures.Controllers { public class HomeController : Controller { public ViewResult Index() { return View("Index", new string[] { "Bob", "Joe", "Alice" }); } } }

數組元素在{和}字符之間指定,這容許對集合進行更簡潔的定義,並能夠在方法調用中內聯定義集合。 清單4-15中的代碼與清單4-14中的代碼具備相同的效果,若是運行示例應用程序,您將在瀏覽器窗口中看到如下輸出:

Bob Joe Alice

使用索引初始化器

C# 6 使用了一種更加整潔的新的集合初始化方式來建立帶有索引的集合(如字典)。 清單4-16顯示了使用C# 5 方法初始化字典來定義一個集合的重寫索引操做。

Listing 4-16. 初始化字典對象

using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; namespace LanguageFeatures.Controllers { public class HomeController : Controller { public ViewResult Index() { Dictionary<string, Product> products = new Dictionary<string, Product> { { "Kayak", new Product { Name = "Kayak", Price = 275M } }, { "Lifejacket", new Product{ Name = "Lifejacket", Price = 48.95M } } }; return View("Index", products.Keys); } } }

初始化這種類型的集合的語法對{和}字符有太多依賴,特別是當集合值正在使用對象初始化器建立時。 C#6編譯器支持一種更天然的方法來初始化索引集合,這與收集初始化後檢索或修改值的方式一致,如清單4-17所示。

Listing 4-17. 使用集合初始化器語法

using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; namespace LanguageFeatures.Controllers { public class HomeController : Controller { public ViewResult Index() { Dictionary<string, Product> products = new Dictionary<string, Product> { ["Kayak"] = new Product { Name = "Kayak", Price = 275M }, ["Lifejacket"] = new Product { Name = "Lifejacket", Price = 48.95M } }; return View("Index", products.Keys); } } }

效果是相同的 - 建立一個字典,其鍵是Kayak和Lifejacket,其值是Product對象,可是使用用於其餘集合操做的索引符號建立元素。 若是您運行應用程序,您將在瀏覽器中看到如下結果:

Kayak Lifejacket

使用擴展方法

擴展方法是將方法添加到您不擁有而且不能直接修改的類的方便方法。 清單4-18顯示了ShoppingCart類的定義,我將它添加到名爲ShoppingCart.cs文件的文件中的Models文件夾中,該文件表示Product對象的集合。

Listing 4-18. ShoppingCart.cs 的內容

using System.Collections.Generic; namespace LanguageFeatures.Models { public class ShoppingCart { public IEnumerable<Product> Products { get; set; } } }

這是一個簡單的類,用做Product對象的包裝(我只須要一個這個例子的基本類)。 假設我須要可以肯定ShoppingCart類中Product對象的總價值,可是我沒法修改類自己,也許是由於它來自第三方,我沒有源代碼。 我可使用擴展方法來添加我須要的功能。 清單4-19顯示了我添加到MyExtensionMethods.cs文件中的Models文件夾的MyExtensionMethods類。

Listing 4-19. MyExtensionMethods.cs 文件內容

namespace LanguageFeatures.Models { public static class MyExtensionMethods { public static decimal TotalPrices(this ShoppingCart cartParam) { decimal total = 0; foreach (Product prod in cartParam.Products) { total += prod?.Price ?? 0; } return total; } } }

第一個參數前面的這個關鍵字標記TotalPrices做爲擴展方法。 在這種狀況下,第一個參數告訴.NET哪一個類的擴展方法能夠應用於ShoppingCart。 我能夠經過使用cartParam參數來引用已經應用擴展方法的ShoppingCart實例。 個人方法枚舉ShoppingCart中的Product,並返回Product.Price屬性的總和。 清單4-20顯示瞭如何在Home控制器的Action方法中應用擴展方法。

提示: 擴展方法不會讓你破壞方法、字段和屬性定義的訪問規則。 您可使用擴展方法擴展類的功能,但只能使用您能夠訪問的類成員。

Listing 4-20. 應用擴展方法

using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; namespace LanguageFeatures.Controllers { public class HomeController : Controller { public ViewResult Index() { ShoppingCart cart = new ShoppingCart { Products = Product.GetProducts() }; decimal cartTotal = cart.TotalPrices(); return View("Index", new string[] { $"Total: {cartTotal:C2}" }); } } }

關鍵的語句是: ... decimal cartTotal = cart.TotalPrices(); ...

我將ShoppingCart對象的TotalPrices方法稱爲是ShoppingCart類的一部分,儘管它是由不一樣類徹底定義的擴展方法。 若是擴展類在當前類的範圍內,則會發現它們是同一個命名空間的一部分,也能夠是做爲using語句主題的命名空間。 若是您運行應用程序,您將在瀏覽器窗口中看到如下輸出:

Total: $323.95

在接口上應用擴展方法

我還能夠建立適用於接口的擴展方法,這樣能夠在實現該接口的全部類上調用擴展方法。 清單4-21顯示了更新以實現IEnumerable 接口的ShoppingCart類。

Listing 4-21. 實現一個接口

using System.Collections; using System.Collections.Generic; namespace LanguageFeatures.Models { public class ShoppingCart : IEnumerable<Product> { public IEnumerable<Product> Products { get; set; } public IEnumerator<Product> GetEnumerator() { return Products.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } }

如今我能夠更新擴展方法,以處理IEnumerable ,如清單4-22所示。

Listing 4-22. 更新擴展方法

using System.Collections.Generic; namespace LanguageFeatures.Models { public static class MyExtensionMethods { public static decimal TotalPrices(this IEnumerable<Product> products) { decimal total = 0; foreach (Product prod in products) { total += prod?.Price ?? 0; } return total; } } }

第一個參數類型已更改成IEnumerable,這意味着方法體中的foreach循環直接用於Product對象。 使用接口的更新意味着我能夠計算任何IEnumerable 枚舉的Product對象的總值,其中包含ShoppingCart的實例,也能夠計算Product對象的數組,如清單4-23所示。

Listing 4-23. 在數組上應用擴展方法

using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; namespace LanguageFeatures.Controllers { public class HomeController : Controller { public ViewResult Index() { ShoppingCart cart = new ShoppingCart { Products = Product.GetProducts() }; Product[] productArray = { new Product {Name = "Kayak", Price = 275M}, new Product {Name = "Lifejacket", Price = 48.95M} }; decimal cartTotal = cart.TotalPrices(); decimal arrayTotal = productArray.TotalPrices(); return View("Index", new string[] { $"Cart Total: {cartTotal:C2}", $"Array Total: {arrayTotal:C2}" }); } } }

若是您啓動該項目,您將看到如下結果,這些結果代表我從擴展方法得到相同的結果,而無論Product對象如何收集:

Cart Total: $323.95 Array Total: $323.95

建立過濾擴展方法

關於擴展方法的最後一件事是,它們能夠用來過濾對象的集合。在IEnumerable 上運行而且還返回IEnumerable 的擴展方法可使用yield關鍵字將選擇條件應用於源數據中的項,以產生一組過濾後的結果。 清單4-24演示了我已經添加到MyExtensionMethods類中的一種方法。

Listing 4-24. 增長過濾擴展方法

using System.Collections.Generic; namespace LanguageFeatures.Models { public static class MyExtensionMethods { public static decimal TotalPrices(this IEnumerable<Product> products) { decimal total = 0; foreach (Product prod in products) { total += prod?.Price ?? 0; } return total; } public static IEnumerable<Product> FilterByPrice( this IEnumerable<Product> productEnum, decimal minimumPrice) { foreach (Product prod in productEnum) { if ((prod?.Price ?? 0) >= minimumPrice) { yield return prod; } } } } }

這種擴展方法稱爲FilterByPrice,它使用一個附加參數,容許我過濾產品,以便在結果中返回Price屬性匹配或超過參數的Product對象。 清單4-25顯示了正在使用的方法。

Listing 4-25. 使用過濾擴展方法

using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; namespace LanguageFeatures.Controllers { public class HomeController : Controller { public ViewResult Index() { 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} }; decimal arrayTotal = productArray.FilterByPrice(20).TotalPrices(); return View("Index", new string[] { $"Array Total: {arrayTotal:C2}" }); } } }

當我在Product對象數組中調用FilterByPrice方法時,只有那些花費超過$ 20的那些方法被TotalPrices方法接收並用於計算總數。 若是您運行應用程序,您將在瀏覽器窗口中看到如下輸出:

Total: $358.90

使用Lambda表達式

Lambda表達式是一個能夠引發大量混亂的語言特徵,特別是由於它們簡化的功能也使人迷惑。 請參考我在上一節中定義的FilterByPrice擴展方法。 這個方法能夠按價格過濾產品對象,這意味着若是我想按名稱過濾,我必須建立另外一個方法,如代碼4-26所示。

Listing 4-26. 增長一個過濾方法

using System.Collections.Generic; namespace LanguageFeatures.Models { public static class MyExtensionMethods { public static decimal TotalPrices(this IEnumerable<Product> products) { decimal total = 0; foreach (Product prod in products) { total += prod?.Price ?? 0; } return total; } public static IEnumerable<Product> FilterByPrice( this IEnumerable<Product> productEnum, decimal minimumPrice) { foreach (Product prod in productEnum) { if ((prod?.Price ?? 0) >= minimumPrice) { yield return prod; } } } public static IEnumerable<Product> FilterByName( this IEnumerable<Product> productEnum, char firstLetter) { foreach (Product prod in productEnum) { if (prod?.Name?[0] == firstLetter) { yield return prod; } } } } }

代碼4-27顯示了使用控制器中應用的兩種過濾方法來建立兩個不一樣的總計。

Listing 4-27. 使用兩個方法

using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; namespace LanguageFeatures.Controllers { public class HomeController : Controller { public ViewResult Index() { 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} }; decimal priceFilterTotal = productArray.FilterByPrice(20).TotalPrices(); decimal nameFilterTotal = productArray.FilterByName('S').TotalPrices(); return View("Index", new string[] { $"Price Total: {priceFilterTotal:C2}", $"Name Total: {nameFilterTotal:C2}" }); } } }

第一個過濾器選擇價格大於$20的全部產品,第二個過濾名稱以字母S開頭的產品。 若是運行示例應用程序,您將在瀏覽器窗口中看到如下輸出:

Price Total: $358.90 Name Total: $19.50

定義函數

我能夠重複這個過程,併爲每一個屬性和屬性組合建立不一樣的過濾器方法。一個更優雅的方法是將處理枚舉的代碼與選擇標準分開。 經過容許函數做爲對象傳遞,C#很容易作到這一點。 清單4-28顯示了一個單獨的擴展方法,用於過濾Product對象的枚舉,可是將結果中包含哪些數據由單獨的函數來決定。

Listing 4-28. 建立一個通用的過濾方法

using System.Collections.Generic; using System; namespace LanguageFeatures.Models { public static class MyExtensionMethods { public static decimal TotalPrices(this IEnumerable<Product> products) { decimal total = 0; foreach (Product prod in products) { total += prod?.Price ?? 0; } return total; } public static IEnumerable<Product> Filter( this IEnumerable<Product> productEnum, Func<Product, bool> selector) { foreach (Product prod in productEnum) { if (selector(prod)) { yield return prod; } } } } }

Filter方法的第二個參數是接受Product對象並返回bool值的函數。 Filter方法調用每一個Product對象的函數,若是該函數返回true,則將其包含在結果中。 要使用Filter方法,我能夠指定一個方法或建立一個獨立的函數,如清單4-29所示。

Listing 4-29. 使用過濾器函數

using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; using System; namespace LanguageFeatures.Controllers { public class HomeController : Controller { bool FilterByPrice(Product p) { return (p?.Price ?? 0) >= 20; } public ViewResult Index() { 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} }; Func<Product, bool> nameFilter = delegate (Product prod) { return prod?.Name?[0] == 'S'; }; decimal priceFilterTotal = productArray .Filter(FilterByPrice) .TotalPrices(); decimal nameFilterTotal = productArray .Filter(nameFilter) .TotalPrices(); return View("Index", new string[] { $"Price Total: {priceFilterTotal:C2}", $"Name Total: {nameFilterTotal:C2}" }); } } }

這兩種方法都不理想。 定義如FilterByPrice方法必須定義一個類。 建立Func <Product,bool>對象避免了這個問題,可是要使用難以閱讀和難以維護的語法。 爲解決這個問題,咱們能夠用lambda表達式以更優雅和表現力的方式定義函數,如清單4-30所示。

Listing 4-30. 使用Lambda表達式

using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; using System; namespace LanguageFeatures.Controllers { public class HomeController : Controller { public ViewResult Index() { 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} }; decimal priceFilterTotal = productArray .Filter(p => (p?.Price ?? 0) >= 20) .TotalPrices(); decimal nameFilterTotal = productArray .Filter(p => p?.Name?[0] == 'S') .TotalPrices(); return View("Index", new string[] { $"Price Total: {priceFilterTotal:C2}", $"Name Total: {nameFilterTotal:C2}" }); } } }

上面的lambda表達式以粗體顯示。這裏使用了無類型參數,編譯器將自動推斷類型。 =>字符朗讀爲「轉到」並將參數連接到lambda表達式的結果。 在個人例子中,一個名爲p的Product參數轉到bool結果,若是第一個表達式中Price屬性等於或大於20,或者Name屬性以第二個表達式中的S開頭,則該參數將爲true。 該代碼的工做方式與單獨的方法和函數委託相同,但更簡潔,對大多數人來講更容易閱讀。

我不須要在lambda表達式中表達個人委託的邏輯。 能夠輕鬆地調用一個方法,像這樣:

prod => EvaluateProduct(prod)

若是我須要一個具備多個參數的委託的lambda表達式,我必須用括號括起參數,以下所示:

(prod,count)=> prod.Price> 20 && count> 0

最後,若是我須要在lambda表達式中須要多個語句的邏輯,我能夠經過使用大括號({})並完成一個返回語句,以下所示:

(prod,count)=> { // ...多個代碼語句... return result; }

您不須要非使用lambda表達式不可,可是它能以可讀和清晰的方式讓複雜函數更加整潔。 我很是喜歡他們,你會看到,在這本書中我用了不少。

使用Lambda 表達式方法和屬性

C#6已經擴展了對lambda表達式的支持,以便它們能夠用於實現方法和屬性。 在MVC開發中,尤爲是在編寫控制器的時候,你會常用包含一個選擇要顯示的數據和要呈現的視圖的單個語句的方法。 在清單4-31中,我已經重寫了Index操做方法,使其遵循這個常見模式。

Listing 4-31. 建立通用的 Action 模式

using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; using System; using System.Linq; namespace LanguageFeatures.Controllers { public class HomeController : Controller { public ViewResult Index() { return View(Product.GetProducts().Select(p => p?.Name)); } } }

這個Action方法從靜態Product.GetProducts方法獲取Product對象的集合,並使用LINQ來投影Name屬性的值,而後將其用做默認視圖的視圖模型。 若是運行應用程序,您將在瀏覽器窗口中看到如下輸出:

Kayak Lifejacket

瀏覽器窗口中也會有一個空的列表項,由於GetProducts方法在其結果中包含一個空引用,但對本章的這一部分可有可無。當一個方法體由單個語句組成時,它能夠 被重寫爲lambda表達式如代碼4-32所示。

Listing 4-32. 使用Lambda表達式的Action 方法 using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; using System; using System.Linq; namespace LanguageFeatures.Controllers { public class HomeController : Controller { public ViewResult Index() => View(Product.GetProducts().Select(p => p?.Name)); } }

方法的Lambda表達式忽略return關鍵字,並使用=>(轉到)將方法簽名(包括其參數)與其實現相關聯。 清單4-32所示的Index方法的工做方式與清單4-31所示的方法相同,但更簡潔。也可使用相同的基本方法來定義屬性。 列表4-33顯示了添加一個使用lambda表達式給Product類的屬性。

Listing 4-33. Lambda 表達式屬性

namespace LanguageFeatures.Models { public class Product { public Product(bool stock = true) { InStock = stock; } public string Name { get; set; } public string Category { get; set; } = "Watersports"; public decimal? Price { get; set; } public Product Related { get; set; } public bool InStock { get; } public bool NameBeginsWithS => Name?[0] == 'S'; public static Product[] GetProducts() { Product kayak = new Product { Name = "Kayak", Category = "Water Craft", Price = 275M }; Product lifejacket = new Product(false) { Name = "Lifejacket", Price = 48.95M }; kayak.Related = lifejacket; return new Product[] { kayak, lifejacket, null }; } } }

使用類型推斷和匿名類型

C# var關鍵字容許您定義一個局部變量而不顯式指定變量類型,如清單4-34所示。 這稱爲類型推斷或隱式類型。

Listing 4-34. 使用類型推斷

using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; using System; using System.Linq; namespace LanguageFeatures.Controllers { public class HomeController : Controller { public ViewResult Index() { var names = new [] { "Kayak", "Lifejacket", "Soccer ball" }; return View(names); } } }

這不是名稱變量沒有類型; 相反,我要求編譯器推斷類型。 編譯器檢查數組聲明,並肯定它是一個字符串數組。 運行示例生成如下輸出:

Kayak Lifejacket Soccer ball

使用匿名類型

經過組合對象初始化器和類型推斷,我能夠建立簡單的視圖模型對象,這些對象在控制器和視圖之間傳輸數據很是有用,而沒必要定義類或結構體,如代碼清單4-35所示。

Listing 4-35. 建立匿名類型

using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; using System; using System.Linq; namespace LanguageFeatures.Controllers { public class HomeController : Controller { public ViewResult Index() { var products = new [] { new { Name = "Kayak", Price = 275M }, new { Name = "Lifejacket", Price = 48.95M }, new { Name = "Soccer ball", Price = 19.50M }, new { Name = "Corner flag", Price = 34.95M } }; return View(products.Select(p => p.Name)); } } }

product數組中的每一個對象都是一個匿名類型的對象。 這並不意味着它在JavaScript變量是動態的意義上是動態的。 這僅僅意味着類型定義將由編譯器自動建立。 強類型仍然執行。 例如,您能夠獲取並設置在初始化程序中定義的屬性。 若是運行該示例,您將在瀏覽器窗口中看到如下輸出:

Kayak Lifejacket Soccer ball Corner flag

C#編譯器根據初始化程序中參數的名稱和類型生成類。具備相同屬性名稱和類型的兩個匿名類型的對象將被分配給相同的自動生成的類。 這意味着產品數組中的全部對象將具備相同的類型,由於它們定義了相同的屬性。

提示: 我必須使用var關鍵字來定義匿名類型對象的數組,由於在編譯代碼以前不會建立類型,因此我不知道要使用的類型的名稱。 匿名類型對象數組中的元素必須都定義相同的屬性; 不然,編譯器沒法知道數組是什麼類型。

爲了演示這一問題,我已經從清單4-36中的示例更改了輸出,以便它顯示了類型名稱,而不是Name屬性的值。

Listing 4-36. 顯示匿名類型名

using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; using System; using System.Linq; namespace LanguageFeatures.Controllers { public class HomeController : Controller { public ViewResult Index() { var products = new [] { new { Name = "Kayak", Price = 275M }, new { Name = "Lifejacket", Price = 48.95M }, new { Name = "Soccer ball", Price = 19.50M }, new { Name = "Corner flag", Price = 34.95M } }; return View(products.Select(p => p.GetType().Name)); } } }

數組中的全部對象都已分配相同的類型,若是運行該示例,則能夠看到該對象。 類型名稱不是用戶友好的,但不須直接使用,您看到的可能與如下輸出中顯示的不一樣:

<>f__AnonymousType02 <>f__AnonymousType02 <>f__AnonymousType02 <>f__AnonymousType02

使用異步方法

C#中最新增長一個語言特性是改進異步方法的處理方式。 異步方法在後臺工做,並在完成後通知您,當執行後臺工做時,容許您的代碼照顧其餘業務。 異步方法是從代碼中消除瓶頸的重要工具,容許應用程序利用多個處理器和多處理器核心並行執行工做。在MVC中,可使用異步方法來提升應用程序的總體性能,它容許服務器在調度和執行請求的方式上具備更大的靈活性。 兩個C#關鍵字-async 和 await用於異步執行工做。 爲了準備這個部分,我須要在示例項目中添加一個新的.NET程序集,以便我能夠進行異步HTTP請求。 清單4-37顯示了我對project.json文件的依賴關係部分的添加。

Listing 4-37. 添加依賴的程序集

"dependencies": { "Microsoft.NETCore.App": { "version": "1.0.0", "type": "platform" }, "Microsoft.AspNetCore.Diagnostics": "1.0.0", "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", "Microsoft.Extensions.Logging.Console": "1.0.0", "Microsoft.AspNetCore.Mvc": "1.0.0", "System.Net.Http": "4.1.0" },

當保存project.json文件時,Visual Studio將下載System.Net.Http程序集並將其添加到項目中。 我在第6章更詳細地描述這個過程。

直接使用任務

C#和.NET對異步方法有很好的支持,但代碼每每是冗長的,而不習慣於並行編程的開發人員經常因不尋常的語法陷入僵局。 例如,清單4-38顯示了一個稱爲GetPageLength的異步方法,它在MyAsyncMethods類中定義,並添加到名爲MyAsyncMethods.cs的類文件中的Models文件夾中。

Listing 4-38. MyAsyncMethods.cs

using System.Net.Http; using System.Threading.Tasks; namespace LanguageFeatures.Models { public class MyAsyncMethods { public static Task<long?> GetPageLength() { HttpClient client = new HttpClient(); var httpTask = client.GetAsync("http://apress.com"); // we could do other things here while the HTTP request is performed return httpTask.ContinueWith((Task<HttpResponseMessage> antecedent) => { return antecedent.Result.Content.Headers.ContentLength; }); } } }

此方法使用System.Net.Http.HttpClient對象請求Apress主頁的內容頁面並返回其長度。 .NET表示將做爲Task異步執行的工做。 基於後臺工做產生的結果,任務對象是強類型的。 因此,當我調用HttpClient.GetAsync方法,我獲得的是一個Task 。 這告訴我,請求將在後臺執行,請求的結果將是一個HttpResponseMessage對象。

提示: 當我使用像background這樣的詞時跳過了不少細節,以便提出要點,這對於MVC的世界很重要。 對異步方法和並行編程的.NET支持通常是很是好的,若是要建立能夠利用多核和多處理器硬件的真正高性能應用程序,咱們鼓勵您更多地瞭解它。 當我介紹不一樣的功能時,您將看到MVC如何在本書中建立異步Web應用程序。

令大多數程序員感到困惑的部分是延續(Continuation),這指的是在後臺任務完成時指定要發生什麼。 在這個例子中,我使用了ContinueWith方法來處理從HttpClient獲取的HttpResponseMessage對象。 GetAsync方法,我使用一個lambda表達式返回一個屬性的值,該屬性包含從Apress Web服務器獲取的內容的長度。 這是Continuation代碼:

return httpTask.ContinueWith((Task<HttpResponseMessage> antecedent) => { return antecedent.Result.Content.Headers.ContentLength; });

請注意,我使用return關鍵字兩次。 這是致使混亂的部分。 第一次使用return關鍵字指定我正在返回一個Task 對象,當任務完成時,它將返回ContentLength頭的長度。 ContentLength標頭返回一個long?result(一個可空的long值),這表示個人GetPageLength方法的結果是Task <long?>,像這樣: ... public static Task<long?> GetPageLength() { ...

若是看不明白,不要擔憂,別人也和你差很少。 正是由於這個緣由 Microsoft將關鍵字添加到C#中以簡化異步方法。

##使用async 和await 關鍵字

Microsoft向C#引入了兩個關鍵字,它們專門用於簡化使用異步方法(如HttpClient.GetAsync)。 關鍵字是async、await,您能夠看到我如何使用它們來簡化清單4-39中的示例方法。

Listing 4-39. 使用async 和 await 關鍵字

using System.Net.Http; using System.Threading.Tasks; namespace LanguageFeatures.Models { public class MyAsyncMethods { public async static Task<long?> GetPageLength() { HttpClient client = new HttpClient(); var httpMessage = await client.GetAsync("http://apress.com"); return httpMessage.Content.Headers.ContentLength; } } }

調用異步方法時,我使用了await關鍵字。這告訴C#編譯器,我想等待GetAsync方法返回的任務的結果,而後以相同的方法繼續執行其餘語句。應用await關鍵字意味着我能夠將GetAsync方法的結果視爲一種常規方法,並將HttpResponseMessage對象分配給一個變量。並且,更好的是,我能夠用正常的方式使用return關鍵字來生成其餘方法的結果 - 在這裏是ContentLength屬性的值。這是很天然的表達方式,我沒必要擔憂ContinueWith方法和return關鍵字的屢次使用。當您使用await關鍵字時,還必須將async關鍵字添加到方法聲明中,如我在示例中所作的那樣。方法結果類型不會改變 - 個人示例GetPageLength方法仍然返回一個Task <long?>。這是由於使用一些聰明的編譯器技巧實現等待和異步,它們容許更天然的語法,可是使用它們時,並不會更改方法中的邏輯。調用GetPageLength方法的人還須要處理一個 Task <long?>結果是由於後臺還有一個能夠產生可空的long類型的任務,儘管這個程序員也能夠選擇使用await和async關鍵字。

這種模式貫穿到MVC控制器中,這使得編寫異步動做方法變得很容易,如清單4-40所示。

Listing 4-40. 定義異步Action方法

using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; using System; using System.Linq; using System.Threading.Tasks; namespace LanguageFeatures.Controllers { public class HomeController : Controller { public async Task<ViewResult> Index() { long? length = await MyAsyncMethods.GetPageLength(); return View(new string[] { $"Length: {length}" }); } } }

我將Index操做方法的結果更改成Task,它告訴MVC,action方法將返回一個任務,該任務會在完成後產生一個ViewResult對象,提供應該呈現的視圖的詳細信息和它須要的數據。 我已經將async關鍵字添加到方法的定義中,這容許我在調用MyAsyncMethods.GetPathLength方法時使用await關鍵字。 MVC和.NET負責處理這些Continuation,這些是易於編寫,易於閱讀和易於維護的異步代碼。 若是運行應用程序,您將看到相似於如下內容的輸出:

Length: 62164

獲取名字

Web應用程序開發中有許多任務須要引用參數,變量,方法或類的名稱。 常見的示例包括在處理來自用戶的輸入時拋出異常或建立驗證錯誤。 傳統的方法是使用名稱硬編碼的字符串值,如清單4-41所示。

Listing 4-41. 硬編碼名字

using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; using System; using System.Linq; namespace LanguageFeatures.Controllers { public class HomeController : Controller { public ViewResult Index() { var products = new [] { new { Name = "Kayak", Price = 275M }, new { Name = "Lifejacket", Price = 48.95M }, new { Name = "Soccer ball", Price = 19.50M }, new { Name = "Corner flag", Price = 34.95M } }; return View(products.Select(p => $"Name: {p.Name}, Price: {p.Price}")); } } }

對LINQ Select方法的調用生成一個字符串序列,每一個字符串都包含對Name和Price屬性的硬編碼引用。 運行應用程序在瀏覽器窗口中生成如下輸出:

Name: Kayak, Price: 275 Name: Lifejacket, Price: 48.95 Name: Soccer ball, Price: 19.50 Name: Corner flag, Price: 34.95

這種方法的問題是它容易出錯,由於名稱有可能拼寫錯誤或代碼重構以後字符串中的名稱未正確更新。 結果可能會致使誤導,向用戶顯示的消息可能會有問題。 C# 6引入了表達式的名稱,編譯器負責生成一個名稱字符串,如清單4-42所示。

Listing 4-42. 使用nameof 表達式

using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; using System; using System.Linq; namespace LanguageFeatures.Controllers { public class HomeController : Controller { public ViewResult Index() { var products = new [] { new { Name = "Kayak", Price = 275M }, new { Name = "Lifejacket", Price = 48.95M }, new { Name = "Soccer ball", Price = 19.50M }, new { Name = "Corner flag", Price = 34.95M } }; return View(products.Select(p => $"{nameof(p.Name)}: {p.Name}, {nameof(p.Price)}: {p.Price}")); } } }

編譯器處理諸如p.Name的引用,以便只有最後一部分包含在字符串中,產生與前面示例相同的輸出。 Visual Studio爲名稱表達式提供IntelliSense支持,所以將提示您選擇引用,並在重構代碼時正確更新表達式。 因爲編譯器負責處理nameof,所以使用無效的引用將致使編譯器錯誤,從而防止錯誤或過時的引用異常通知。

本章小結

本章概述了能讓程序員提升效率的MVC關鍵的C#語言特性。 C#是一種靈活的語言,一般有不一樣的方法來解決一個問題,但這裏所介紹的是您在Web應用程序開發過程當中最常遇到的功能,而且在本書的整個示例中均可以看到。 在下一章中,我將介紹Razor視圖引擎,並介紹如何在MVC Web應用程序中生成動態內容。

相關文章
相關標籤/搜索