Pro ASP.NET Core MVC2(第7版)翻译

第4章:必备的 C# 特性

作者:Adam Freeman 翻译:陈广 日期:2018-8-22


本章描述 web 应用程序开发所需要的 C# 特性,这些特征未被广泛理解或经常引起混淆。这不是一本关于 C# 的书籍,因此我只为每个特性提供了一个简短的示例,以便您可以在书的其余部分中遵循这些示例,并在您自己的项目中利用这些特性。表 4-1 为本章概述。

表 4-1:章节摘要

问题 解决方案 清单
避免访问空引用上的属性 使用空条件运算符 5-8
简化 C# 属性 使用自动实现的属性 9-11
简化字符串合成 使用字符串内插 12
创建对象并在一个步骤中设置它的属性 使用对象或集合初始化程序 13-16
测试对象的类型或特征 使用模式匹配 17-18
向无法修改的类添加功能 使用扩展方法 19-26
简化委托和单语句方法的使用 使用 lambda 表达式 27-34
使用隐式类型 使用 var 关键字 35
创建对象而不定义类型 使用匿名类型 36-37
简化异步方法的使用 使用 async 和 await 关键字 38-41
获取类方法或属性的名称,而不定义静态字符串 使用 nameof 表达式 42-43

准备示例项目

本章,我使用【ASP.NET Core Web 应用程序(.NET Core)】模板创建了一个名为 LanguageFeatures 的 Visual Studio 项目,如图4-1所示。

图4-1 选择项目类型

当出现不同的项目配置时,我选择了【空】模板,如图4-2所示。我从对话框顶部的列表中选择了 .NET Core 和 ASP.NET Core 2.0,确保身份验证选项被设置为【不进行份验证】,并且在单击【确定】按钮创建项目之前未选中【启用 Docker 支持】选项。

图4-2 选择项目模板

启用 ASP.NET Core MVC

【空】项目模板创建的项目包含最小 ASP.NET Core 配置,不支持任何 MVC。这意味着【web 应用程序(模型-视图-控制器)】模板所添加的占位符内容不会出现在这里,这意味着需要一些额外的步骤来启用 MVC,以便控制器和视图等能够工作。在本节中,我将进行必要的更改,以在项目中添加启用 MVC 设置,但我将暂时不详细介绍每个步骤背后的原理。

要启用 MVC 框架,如清单4-1所示,修改Startup类代码。

清单 4-1:LanguageFeatures 文件夹下的 Startup.cs 文件,启用 MVC

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace LanguageFeatures
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            //app.Run(async (context) =>
            //{
            //    await context.Response.WriteAsync("Hello World!");
            //});
            app.UseMvcWithDefaultRoute();
        }
    }
}

我在第14章中解释了如何配置 ASP.NET Core MVC 应用程序,但是清单4-1中添加的两个语句提供了使用默认配置和约定的基本 MVC 设置。

创建 MVC 应用程序组件

现在 MVC 已经建立,可以添加 MVC 应用程序组件,用于演示重要的 C# 语言特性。

创建模型

我首先创建了一个简单的模型类,以便能够使用一些数据。我在其中添加了一个名为 Models 的文件夹,并创建了一个名为 Product.cs 的类文件,用于定义清单4-2所示的类。

清单 4-2:Models 文件夹下的 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 };
        }
    }
}

Product类定义了NamePrice属性,还有一个叫GetProducts的静态方法返回一个Product数组。由GetProducts方法返回的数组中的一个元素被设置为null,它将被用于在本章后面演示一些有用的语言特性。

创建控制器和视图

对于本章中的示例,我使用一个简单的控制器来演示不同的语言特性。我创建了一个 Controllers 文件夹,并在其中添加了一个名为 HomeController.cs 的类文件,内容如清单4-3所示。当使用默认 MVC 配置时,MVC 将默认使用 Home 控制器来发送 HTTP 请求。

清单 4-3:Controllers 文件夹下的 HomeController.cs 文件的内容

using Microsoft.AspNetCore.Mvc;

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

Index action 方法告诉 MVC 云渲染默认视图,并通过返回客户端的 HTML 向其传递一个字符串数组。要创建相应的视图,我添加了一个 Views/Home 文件夹(在项目中创建一个 Views 文件夹,并在其中添加一个 Home 文件夹),并添加了一个名为 Index.cshtml 的视图文件,清单4-4列出了此文件的内容。

清单 4-4:Views/Home 文件夹下的 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 所示的输出。

图4-3 运行示例应用程序

由于本章所有示例的输出都是文本,我将以如下方式显示浏览器显示的消息:

C#
Language
Features

使用空条件运算符

空条件运算符允许更优雅地检测null值。在 MVC 开发中,计算出请求是否包含特定的标头或值,或模型是否包含特定的数据项时,可能会进行大量的空值检查。传统的空值处理需要进行显式检查,当必须检查对象及其属性时,这可能变得乏味和容易出错。空条件运算符使这个过程更为简单、简洁,如清单4-5所示。

清单 4-5:Controllers 文件夹下的 HomeController.cs 文件,检查空值

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类定义的静态方法GetPRoduccts返回一个数组对象,我在控制器的Index action 方法所做的检查是为了获取NamePrice值的列表。现在的问题是数组中的对象和属性值都可能为null,这意味着,我不能仅仅在foreach循环中引用p.Namep.Price而不引发NullReferenceException。为避免此事发生,我使用了空条件运算符,如下:

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

空条件运算符是单个问号?。如果p为空,name将被设置为null。如果p不为空,则name将被设置为Person.Name属性的值。Price属性也要接受同样的测试。注意,在使用空条件运算符时你所声明的变量必须能够被赋值为null,这就是price变量被声明为可空 decimal(decimal?)的原因。

链接空条件运算符

空条件运算符可以被链接以通过对象层次结构进行导航,这是它真正成为简化代码和允许安全导航的有效工具。在清单4-6中,我为嵌套引用的Product类添加了一个属性,从而创建了一个更复杂的对象层次结构。

清单 4-6:Models 文件夹下的 Product.cs 文件,添加一个属性

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对象的Related属性。在GetProducts方法中,我为Product对象kayak设置了Related属性。清单4-7演示了如何在不引发异常的情况下,链接空条件运算符以在对象属性中导航。

清单 4-7:Controllers 文件夹下的 HomeController.cs 文件,检测嵌套的空值

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 relateName = p?.Related?.Name;
                results.Add(string.Format("Name:{0}, Price:{1}, Related:{2}", name, price, relateName));
            }
            return View(results);
        }
    }
}

空条件运算符可以应用于属性链的每个部分,如下:

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

结果就是当pnullp.Relatednull时,relatedName变量将为null,否则会被赋予p.Related.Name属性的值。如果运行程序,将会看到浏览器输出如下结果:

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

组合条件和合并运算符

将空条件运算符(单个问号)与空合并运算符(两个问号)组合在一起,设置一个回退值以表示应用程序中使用的空值是有用的,如清单4-8所示。

清单 4-8:Controllers 文件夹下的 HomeController.cs 文件,组合空运算符

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 relateName = p?.Related?.Name ?? "<None>";
                results.Add(string.Format("Name:{0}, Price:{1}, Related:{2}", name, price, relateName));
            }
            return View(results);
        }
    }
}

空条件运算符确保在导航对象属性时不会收到NullReferenceException,而空合并运算符确保浏览器中显示的结果中不包含null值。运行示例,将会看到浏览器中显示以下结果:

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

使用自动实现属性

C# 支持自动实现属性,之前我使用它为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 };
        }
    }
}

此功能允许我在不实现getset访问器的情况下定义属性。以下代码使用自动实现属性功能定义了一个属性:

...
public string Name { get; set; }
...

等同于以下代码:

...
public string Name {
    get { return name; }
    set { name = value; }
}
...

这类功能被称为语法糖,它让 C# 变得更为令人愉悦 —— 本例中,在不改变语言行为方式的情况下,去掉每个属性都要重复的东西,从而消除冗余代码。“糖”这一术语看起来有些贬义,但是任何使代码更易于编写和维护的增强都是有益的,特别是在大型和复杂的项目中。

使用自动实现属性初始化器

自 C# 3.0 以来就已经支持自动实现属性。最新版的 C# 支持了自动实现属性初始化器,它允许在不使用构造方法的情况下给属性设置一个初始值,如清单4-9。

清单 4-9:Models 文件夹下的 Product.cs 文件,使用自动实现属性初始化器

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 };
        }
    }
}

将值分配给自动实现属性并不会阻止set访问器稍后用于更改属性,而只会使简单类型的代码变得简洁,这些类型最终会使用一个构造函数以包含提供默认值的属性赋值列表。本例中,初始化器将Watersports值赋予Category属性。初始值可以被改变,在我创建kayak对象时,指定了Water Craft值用于代替。

创建只读自动实现属性

您可以使用初始化器创建一个只读属性,这需要在拥有初始化器的自动实现属性中省略set关键字,如清单4-10所示。

清单 4-10:Models 文件夹下的 Product.cs 文件,创建一个只读属性

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-11所示。

清单 4-11:Models 文件夹下的 Product.cs 文件,给只读属性赋值

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# 还支持不同的方法,被称为字符串内插,这避免了需要确保字符串模板中的{0}引用与指定为参数的变量匹配。取而代之的是字符串内插直接使用变量名,如清单4-12所示。

清单 4-12 Controllers 文件夹下的 HomeController.cs 文件,使用字符串内插

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 relateName = p?.Related?.Name ?? "<None>";
                results.Add($"Name:{name}, Price:{price}, Related:{relateName}");
            }
            return View(results);
        }
    }
}

内插字符串以$字符作为前缀,并包含 空穴(holes),它是对{}字符中包含的值的引用。在计算字符串时,将用指定的变量或常量的当前值填充空穴。

Visual Studio 为内插字符串提供了智能感知支持,在键入{时会提供一个可用成员列表;这有助于减少打字错误,其结果是一种更容易理解的字符串格式。

提示:字符串内插支持string.Format方法中可用的所有格式说明符。格式说明符作为空穴的一部分包含在内,因此$"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-13所示。

清单 4-13:Controllers 文件夹下的 HomeController.cs 文件,初始化对象

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-14所示。

清单 4-14:Controllers 文件夹下的 HomeController.cs 文件,使用集合初始化器

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-14中的代码与清单4-13中的代码具有相同的效果,运行示例应用程序,将在浏览器窗口中看到以下输出:

Bob
Joe
Alice

使用索引初始化器

最近版本的 C# 简化了使用索引的集合(例如字典)的初始化方式。清单4-15重写了Index action方法,并使用传统的 C# 方法初始化 Dictionary 来定义集合。

清单 4-15:Controllers 文件夹下的 HomeController.cs 文件,初始化一个 Dictionary

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# 支持更为自然的方式来初始化索引集合,这与集合初始化后检索或修改值的方式一致,如清单4-16所示。

清单 4-16:Controllers 文件夹下的 HomeController.cs 文件,使用集合初始化器语法

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);
        }
    }
}

效果是一样的 —— 创建一个 Dictionary,键为KayakLifejacket,值为Product对象 —— 但是创建元素使用的是集合操作中的索引符号。运行程序,将会看到以下结果:

Kayak
Lifejacket

模式匹配

C# 最近添加的一个最有用的功能是支持模式匹配,它可用于测试对象是否具有指定类型或特征。这是另一种形式的语法糖,它可以极大地简化复杂的条件语句块。is关键字用于执行类型测试,如清单4-17所示。

清单 4-17:Controllers 文件夹下的 HomeController.cs 文件,执行类型测试

using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using LanguageFeatures.Models;

namespace LanguageFeatures.Controllers
{
    public class HomeController : Controller
    {
        public ViewResult Index()
        {
            object[] data = new object[] { 275M, 29.95M,
                "apple", "orange", 100, 10 };
            decimal total = 0;
            for (int i = 0; i < data.Length; i++)
            {
                if (data[i] is decimal d)
                {
                    total += d;
                }
            }
            return View("Index", new string[] { $"Total: {total:C2}" });
        }
    }
}

is关键字执行一个类型检查,当一个值为指定类型时,则将此值赋给一个新变量,如下:

...
if (data[i] is decimal d) {
...

如果data[i]是一个decimal类型,此表达式结果为truedata[i]的值将赋给变量d,并允许在不执行任何类型转换情况下,在后续语句中使用它。is关键字将只匹配指定类型,这意味着data数组中仅有两个值会被处理(数组中的其它项为stringint值)。如果运行程序,将会看到以下输出:

Total: $304.95

Switch 语句中的模式匹配

模式匹配也可用于switch语句,它支持由when关键字来限制值何时由case语句匹配,如清单4-18所示。

清单4-18:Controllers 文件夹下的 HomeController.cs 文件,模式匹配

using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using LanguageFeatures.Models;

namespace LanguageFeatures.Controllers
{
    public class HomeController : Controller
    {
        public ViewResult Index()
        {
            object[] data = new object[] { 275M, 29.95M,
                "apple", "orange", 100, 10 };
            decimal total = 0;
            for (int i = 0; i < data.Length; i++)
            {
                switch (data[i])
                {
                    case decimal decimalValue:
                        total += decimalValue;
                        break;
                    case int intValue when intValue > 50:
                        total += intValue;
                        break;
                }
            }
            return View("Index", new string[] { $"Total: {total:C2}" });
        }
    }
}

要匹配指定类型的任何值,请在case语句中使用类型和变量名,如下:

...
case decimal decimalValue:
...

case语句匹配任何decimal值,并将它赋给一个叫decimalValue的变量。如果需要更多选择,可包含when关键字,如下:

...
case int intValue when intValue > 50:
...

case语句匹配int值,并将它赋给一个叫intValue的变量,但仅在值大于50的情况下有效。如果运行程序,将输出如下结果:

Total: $404.95

使用扩展方法

在您不拥有及无法直接直接修改一个类的情况下,扩展方法是为此类添加方法的一个便利途径。清单4-19演示了ShoppingCart类的定义,我在 Models 文件夹下添加了一个名为 ShoppingCart.cs 的文件,它表示Product对象的集合。

using System.Collections.Generic;

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

这是一个简单的类,用于封装一系列Product对象(本例只需要一个基本的类)。假设我需要确定ShoppingCart类中Product对象的总价值,却又不能修改类本身,也许是因为它来自第三方,而我没有源代码。这时就可以通过扩展方法来添加所需的功能。清单4-20显示了我添加进 Models 文件夹下的 MyExtensionMethods.cs 文件的MyExtensionMethods类。

清单 4-20:Models 文件夹下的 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;
        }
    }
}

第一个参数前面的this关键字将TotalPrices标记为扩展方法。第一个参数告诉 .NET 扩展方法可以应用于哪个类 —— 本例为ShoppingCart。我可以通过cartParam参数引用已经应用了扩展方法的ShoppingCart类的实例。在方法内,我枚举了ShoppingCart里的Product对象,并返回Product.Price属性值的和。列表4-21演示了如何在 Home 控制器的 action 方法中使用扩展方法。

注意:扩展方法不允许您突破类为方法、字段和属性定义的访问规则。您可以通过使用扩展方法来扩展类的功能,但只能使用您可以访问的类成员。

清单 4-21:Controllers 文件夹下的 HomeController.cs 文件,应用扩展方法

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类的一部分,即使它完全是由不同类定义的扩展方法。.NET 将在当前类的范围内查找扩展方法,这意味着它们是相同命名空间的一部分,或者是using语句的主题命名空间中的一部分。运行程序,将会看到如下输出:

Total: $323.95

将扩展方法应用于接口

还可以创建应用于接口的扩展方法,这使得我可以调用实现了接口的所有类的扩展方法。清单4-22显示了已更新的ShoppingCatr类,它实现了IEnumerable<Product>接口。

清单 4-22 Models 文件夹下的 ShoppingCart.cs 文件,实现一个接口

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<Product>,如清单4-23所示。

清单 4-23:Models 文件夹下的 MyExtensionMethods.cs 文件,更新扩展方法

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<Product>,这意味着方法体内的foreach循环直接作用于Product对象。更改为使用接口表示我可以计算由任何IEnumerable<Product>枚举的Product对象的总价值,其中包括ShoppingCart实例以及Product对象数组,如清单4-24所示。

清单 4-24:Controllers 文件夹下的 HomeController.cs 文件,应用扩展方法

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<T>的扩展方法如果返回IEnumerable<T>,可以使用yield关键字将选择条件应用于源数据中的项,以生成较少的结果集。清单4-25演示了这种方法,我将其添加到了MyExtensionMethods类中。

清单 4-25:Controllers 文件夹下的 MyExtensionMethods.cs 文件,一个过滤扩展方法

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-26演示了此方法的使用。

清单 4-26:Controllers 文件夹下的 HomeController.cs 文件,使用扩展方法

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扩展方法。编写此方法是为了能够按价格筛选Product对象,这意味着如果我想按名称进行筛选,就必须创建第二个方法,如清单4-27所示。

清单 4-27:Models 文件夹下的 MyExtensionMethods.cs 文件,添加一个过滤方法

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-28演示了在控制器中使用两个过滤方法来创建两个不同的总计。

清单 4-28:Controllers 文件夹下的 HomeController.cs 文件,使用两个过滤方法

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-29显示了单个筛选Product对象枚举的扩展方法,但是,这就将关于结果中包含哪些内容的决定委托给一个单独的函数。

清单 4-29:Models 文件夹下的 MyExtensionMethods.cs 文件,创建一个通用过滤器

using System;
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> 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则将Product对象包含在结果中。要使用Filter方法,我可以指定一个方法或创建一个独立的函数,如清单4-30所示。

清单 4-30:Controllers 文件夹下的 HomeController.cs 文件,为筛选对象使用函数

using System;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using LanguageFeatures.Models;

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-31所示。

清单 4-31:Controllers 文件夹下的 HomeController.cs 文件,使用 Lambda 表达式

using System;
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
                .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 表达式使用粗体显示。参数是没有指定类型,该类型将自动推断。=>字符读为 “goes to(转为)”,并将参数链接到 lambda 表达式的结果。在我的例子中,一个名为pProduct参数转化为bool结果,如果Price属性在第一个表达式中大于等于20,或者Name属性在第二个表达式中以S开头,则返回true。这段代码与单独的方法和函数委托的工作方式相同,但更简洁,而且对大多数人来说更容易阅读。

Lambda表达式的其他形式

如果不需要在 lambda 表达式中表示我的委托的逻辑,可以很容易地调用一个方法,如下所示:

prod => EvaluateProduct(prod)

如果 lambda 表达针对的是多个参数的委托,则必须将参数括在圆括号中,如下:

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

最终如果 lambda 表达式逻辑需要超过一条语句,可以用大括号{}将它们括起来并以return语句结束,如下: (prod, count) => { // ...multiple code statements... return result; } 你可以不在代码中使用 lambda 表达式,但它们是简单而清晰地表达复杂函数的一种简洁的方式,我非常喜欢它,你会看到它们在整本书中得到了广泛的使用

使用 Lambda 表达式方法和属性

Lambda 表达式可以用于实现构造器、方法和属性。在 MVC 开发,尤其在编写控制器时,通常方法中会使用单个语句,该语句选择要显示的数据和要呈现的视图。在清单4-32,我重写了Index action 方法,使其遵循这个常见的模式。

Listing 4-32:Controllers 文件夹下的 HomeController.cs 文件,创建常用的 Action 模式

using System;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using LanguageFeatures.Models;
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-33所示。

清单 4-33:HomeController.cs 文件中将 action 方法表示为 Lambda 表达式

using System;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using LanguageFeatures.Models;
using System.Linq;

namespace LanguageFeatures.Controllers
{
    public class HomeController : Controller
    {
        public ViewResult Index() =>
            View(Product.GetProducts().Select(p => p?.Name));

    }
}

方法的 Lambda 表达式省略了return关键字,并使用=>将方法签名(包括它的参数)与其实现相关联。清单4-33中Index方法的工作方式与清单4-32的相同,但表达得更为简洁。同样的基本方法也可以用于定义属性。清单4-34为Product类添加一个使用 lambda 表示的属性。

清单 4-34:Models 文件夹下的 Product.cs 文件,将属性表达为 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 };
        }
    }
}

使用类型推断和匿名类型

var关键字允许您在不显式指定类型的情况下定义一个本地变量,如清单4-35所示。这叫类型推断或隐含类型。

清单 4-35:Controllers 文件夹下的 HomeController.cs 文件,使用类型推断

using System;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using LanguageFeatures.Models;
using System.Linq;

namespace LanguageFeatures.Controllers
{
    public class HomeController : Controller
    {
        public ViewResult Index()
        {
            var names = new[] { "Kayak", "Lifejacket", "Soccer ball" };
            return View(names);
        }
    }
}

并非names变量没有类型;而是我让编译器从代码中推断类型。编译器检查数组声明并计算出它是字符串数组。运行程序,会输出如下结果:

Kayak
Lifejacket
Soccer ball

使用匿名类型

将对象初始化器和类型推断相结合,我可以在不定义类或结构体的情况下创建简单的视图模型对象,这些对象对于在控制器和视图之间传输数据非常有用,如清单4-36所示。

清单 4-36:Controllers 文件夹下的 HomeController.cs 文件,创建匿名类型

using System;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using LanguageFeatures.Models;
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));
        }
    }
}

products数组中的每个对象都是不具名类型对象。这并不意味着它如 JavaScript 那样变量是动态的。只是表示类型定义将由编译器动态地创建,仍然使用强类型机制。例如,您可以获取并设置已初始化器中定义的属性。运行程序,将看到如下输出:

Kayak
Lifejacket
Soccer ball
Corner flag

C# 编译器基于初始化器中的名称和参数类型生成类。两个拥有相同属性名称和类型的匿名类型对象将被分配给同一个自动生成类。这意味着products数组中的所有对象将有相同的类型,因为它们定义了相同的属性。

提示:我不得不使用var关键字来定义匿名类型对象数组,因为直到代码编译后类型才被创建,所以我不知道所使用的类型名称。一个匿名类型对象数组中的元素必须定义相同的属性,否则编译器无法计算出数组应当使用哪种类型。

为了演示这一点,我在清单4-37修改了示例中的输出,以便它显示类型名称,而不是Name属性的值。

清单 4-37:Controllers 文件夹下的 HomeController.cs 文件,显示类型名称

using System;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using LanguageFeatures.Models;
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));
        }
    }
}

使用异步方法

异步方法在后台运行和关闭,并在完成时通知您,允许您的代码在执行后台工作时处理其他业务。异步方法是消除代码瓶颈并允许应用程序利用多处理器以及处理器核心并行执行工作的重要工具。

在MVC中,异步方法可以让服务器在调度和执行请求的方式上更为灵活,从而提高应用程序的总体性能。两个 C# 关键字asyncawait用于异步地执行工作。

为了准备本节,需要向示例项目添加一个新的 .NET 程序集,以便可以发出异步 HTTP 请求。在【解决方案资源管理器】中右键单击 LanguageFeatures 项,在弹出菜单中选择【编辑LanguageFeatures.csproj】,并添加清单4-38所示元素。

清单 4-38:LanguageFeatures 文件夹下的 LanguageFeatures.csproj 文件,添加一个包

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Folder Include="wwwroot\" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.6" />
    <PackageReference Include="System.Net.Http" Version="4.3.2" />
  </ItemGroup>

</Project>

当你保存文件,Visual Studio 将会下载 System.Net.Http 程序集,并将它添加到项目中。我会在第6章详细描述此过程。

直接使用 Tasks

C# 和 .NET 对异步方法有很好的支持,但是代码往往很冗长,不习惯并行编程的开发人员经常会被奇异的语法所困扰。作为例子,清单4-39演示了一个名为GetPageLength的异步方法,它定义于MyAsyncMethods的类中,此类存在于 Models 文件夹下 MyAsyncMethods.cs 的文件中。

清单 4-39:Models 文件夹下的 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");
            return httpTask.ContinueWith((Task<HttpResponseMessage> antecedent) =>
            {
                return antecedent.Result.Content.Headers.ContentLength;
            });
        }
    }
}

此方法使用System.Net.Http.HttpClient对象请求 Apress 主页,并返回它的长度。.NET 使用 Task 以异步的方式完成工作。Task 对象是强类型的,它是基于后台工作产生的结果。所以当我调用HttpClient.GetAsync方法时,返回的是一个Task<HttpResponseMessage>。这告诉了我,请求将在后台执行,请求的结果将是一个HttpResponseMessage对象。

提示:在我使用后台这个单词时,就已经跳过很多细节,仅专注于对 MVC 世界重要的关键点。.NET 对异步方法和并行编程的支持非常好,我鼓励您更多地了解它,如果您想要创建真正高性能的应用程序,可以利用多核和多处理器硬件。我在本书介绍不同的特性时,您将看到 MVC 如何使创建异步 Web 应用程序变得非常容易。

让大多数程序员陷入困境的部分是 continuation,它是指定后台任务完成时要发生的事情的机制。本例中,我使用了ContinueWith方法来处理从HttpClient.GetAsync方法获取的HttpResponseMessage对象,它使用 lambda 表达式返回一个属性值,该属性值包含从 Apress Web 服务器获得的内容的长度。以下是 continuation 代码:

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

注意,我使用了两次return关键字。这是让人困惑的地方。第一个return关键字指定返回的是Task<HttpResponseMessage>对象,当 task 完成后,将返回ContentLength标头的长度。ContentLength标头返回的是long?(可空 long 值),这意味着GetPageLength方法的结果为Task<long?>,如下:

...
public static Task<long?> GetPageLength() {
...

不要担心无法理解 —— 你并不是唯一陷入困惑的人。正是出于这个原因,微软在C#中添加了关键字,以简化异步方法。

使用 async 和 await 关键字

微软在 C# 中引入了两个关键字,专门用于简化使用异步方法(如HttpClient.GetAsync)。两个关键字是asyncawait,您可以在清单4-40中看到我是如何使用它们简化示例中的方法的。

清单 4-40:Models 文件夹下的 MyAsyncMethods.cs 文件,使用asyncawait关键字

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方法返回的Task结果,之后再在同一方法中继续执行其他语句。

应用await关键字意味着我可以将GetAsync方法的结果当作一个常规方法对待,并将返回的HttpResponseMessage对象分配给一个变量。更好的是,我可以正常的方式使用return关键字来从另一个方法生成结果 —— 本例的ContentLength属性值。这种技术显得更为自然,这意味着我不必担心ContinueWith方法和return关键字的多次使用。

在使用await关键字时,还必须如我在本例中做的那样,给方法签名加上async关键字。方法结果类型没有改变 —— 本例的GetPageLength方法依然返回Task<long?>。这是因为 await 和 async 是使用一些聪明的编译器技巧实现的,这意味着它们允许更自然的语法,但是却不会改变所应用的方法中正在发生的事情。调用我的GetPageLength方法的人必须要处理Task<long?>结果,这是因为仍然会有一个产生可空long后台操作 —— 当然,该程序员也可以选择继续使用awaitasync关键字。

这种模式贯穿到 MVC 控制器中,这使得编写异步操作方法变得很容易,如清单4-41所示。

清单 4-41:Controllers 文件夹下的 HomeController.cs 文件,一个异步 Action 方法

using System;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using LanguageFeatures.Models;
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 action 方法的结果改为Task<ViewResult>,这告诉 MVC,action 方法在完成时将返回一个生成ViewResult对象的Task,它将提供所渲染的视图及其所需数据的详细信息。我已经在方法的定义中添加了async关键字,从而可以在调用MyAsyncMethods.GetPathLength方法时使用await关键字。MVC 和 .NET 负责处理 continuations,其结果是易于编写、易于阅读和易于维护的异步代码。如果运行程序,将会看到类似于下面的输出(会有不同的长度,因为 Apress 网站的内容经常发生变化):

Length: 54576

获取名称

Web 应用程序开发中有许多任务需要引用参数、变量、方法或类的名称。常见的示例包括在处理来自用户的输入时抛出异常或创建验证错误。传统的方法是使用字符串值硬编码名称,如清单4-42所示。

清单 4-42:Controllers 文件夹下的 HomeController.cs 文件,硬编码一个名称

using System;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using LanguageFeatures.Models;
using System.Linq;
using System.Threading.Tasks;

namespace LanguageFeatures.Controllers
{
    public class HomeController : Controller
    {
        public async Task<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方法的调用生成一个字符串序列,每个字符串包含对NamePrice属性的硬编码引用。运行程序,产生如下输出:

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

这种方法的问题是很容易出错,要么是因为名称输入错误,要么是代码被重构,字符串中的名称没有正确更新。结果可能会产生歧误,这对于显示给用户的消息是有问题的。C# 支持nameof表达式,编译器负责生成名称字符串,如清单4-43所示。

清单 4-43:Controllers 文件夹下的 HomeController.cs 文件,使用nameof表达式

using System;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using LanguageFeatures.Models;
using System.Linq;
using System.Threading.Tasks;

namespace LanguageFeatures.Controllers
{
    public class HomeController : Controller
    {
        public async Task<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 包含对nameof表达式智能感知支持,因此将提示您选择引用,重构代码时将正确更新表达式。由于编译器负责处理nameof,所以使用无效引用会导致编译器错误,从而防止错误或过时的转义引用通知

总结

本章我向您概述了一个高效的 MVC 程序员需要了解的 C# 语言的关键特性。C# 是一种非常灵活的语言,通常可以使用不同的方法来处理任何问题。这些特性是您在开发 web 应用程序时最常遇到的特性,它们贯穿本书示例。在下一章中,我将介绍 Razor 视图引擎,并解释如何在 MVC Web 应用程序中使用 Razor 视图引擎生成动态内容。

;

© 2018 - IOT小分队文章发布系统 v0.3