第四章 基本C#特性
本章我将讨论一些在web应用开发中用到, 但会造成迷惑的C#特性. 但这不是介绍C#, 所以我仅仅为每个特性提供一个简单的例子, 以便你能够看懂本书后面的代码, 以及在自己的项目中应用. 下面的表格概括了这一章节.
(主要是因为不熟悉C#语言或C#新版本的语法糖, 比如转行的某语言程序员….后面又臭又长又啰嗦的手把手教学懒得翻译了, 直接提关键点)
| - | - |
| 问题 | 解决方案 |
| 避免访问空引用的属性 | 空条件操作符
| 简化C#属性 | 自动属性
| 简化字符串拼接 | 内插字符串
| 在一步中创建对象并为属性赋值 | 对象或集合初始化器
| 测试一个对象的类型或特征 | 模式匹配
| 向无法更改的类中添加函数 | 扩展属性
| 简化委托和单语句方法 | lambda表达式
| 使用不明确的类型 | var关键字
| 创建对象而不定义类型 | 匿名类
| 简化异步方法 | async和await关键字
| 获取类方法或熟悉的名字而不定义字符串 | nameof表达式
准备示例项目
创建一个名为LanguageFeatures
的项目, 使用ASP.NET COre Web Application
模板, 创建一个空项目.
启用ASP.NET Core MVC
因为创建了一个空项目, 所以需要手动启用MVC支持
// StartUp.cs
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();
}
}
}
我将在第十四章中介绍如何配置MVC应用程序, 但添加此处的两个语句就可以完成使用基本配置和约定的MVC框架.
创建MVC应用组件
MVC创建好后, 我将添加展示重要C#语言特性的MVC应用组件.
创建模型
首先要创建一个简单的模型类来存储数据, 于是添加了Models
文件夹, 并创建了一个类Product.cs
:
// 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 };
}
}
}
GetProducts
数组包含一个null
, 我稍后将用来说明一下有用的语言特性.
创建控制器和视图
创建一个文件夹Controllers
, 以及控制器类HomeController.cs
, 在默认的MVC配置中, Home
控制器是MVC默认发送请求的地方.
// 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
行为渲染默认视图, 并传递一个字符串数组. 添加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>
运行后将在浏览器中看到如下输出:
C#
Language
Features
使用空条件操作符
空条件运算符可以更优雅地检测空值. 在MVC开发中, 要确定一个请求是否包含一个特定的头或值, 或者模型是否包含一个特定的数据项时, 可能会有很多检查空值的工作. 通常处理null需要进行显式检查, 当必须检查对象及其属性时, 这会变得冗长且容易出错. 要确保不出错, 就要判断大量的NullReferenceException
, 空条件运算符使这个过程更简洁.
// 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
对象的Name
和Value
值, 并在视图中显示. 问题是既不知道集合中的对象是否为null, 也不知道对象的属性是否为null, 所以使用了空条件操作符, 像这样:
string name = p?.Name;
decimal? price = p?.Price;
空条件操作符是一个问号(?
), 如果p is null
, name
也会被设置为null
, 否则name
会被设置为Person.Name
. 另外空条件操作符必须跟在可空类型后使用, 所以使用decimal?
类型.
空条件操作符链
空条件操作符可以在在对象层次中迭代, 这是让它真正成为简化代码并安全导航的有效工具的因素. 向Product
类中添加一个属性, 创造一个更复杂的对象层次结构
// 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
中添加了一个Related
属性, 指向一个Product
对象.
// 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 relatedName = p?.Related?.Name;
results.Add(string.Format("Name: {0}, Price: {1}, Related: {2}", name, price, relatedName));
}
return View(results);
}
}
}
建立了这样的空条件操作符链:
string relatedName = p?.Related?.Name;
输出结果为
Name: Kayak, Price: 275, Related: Lifejacket
Name: Lifejacket, Price: 48.95, Related:
Name: , Price: , Related:
组合空条件操作符和空合操作符
空合操作符是两个问号(??
)
var output = input ?? defalutValue;
如上, 若input is null
, 则output == input
, 否则output == defaultValue
, 即为不可空的值’output`指定一个替换null的默认值.
组合起来
// Controllers\HomeController.cs
string name = p?.Name ?? "<No Name>";
decimal? price = p?.Price ?? 0;
string relatedName = p?.Related?.Name ?? "<None>";
输出结果
Name: Kayak, Price: 275, Related: Lifejacket
Name: Lifejacket, Price: 48.95, Related: <None>
Name: <No Name>, Price: 0, Related: <None>
使用自动实现属性
C#支持自动实现属性(属性, property, 通过get\set对数据进行访问控制, java中的setter\getter)
可读可写的属性如下
public string Name { get; set; }
这实际是一个语法糖, 编译时编译器会转换为
public string Name {
get { return _name; }
set { _name = value; }
}
这里_name
只是一个代称, 不确定字段名到底是什么.
自动属性初始化器
从C# 3.0起就支持自动属性, 而C#的最新版本(7.0??)支持自动属性的初始化器, 可以在不使用构造函数的情况下设置初始值, 之前只可以为字段设置.
public string Category { get; set; } = "Watersports";
初始化器不会妨碍setter的更改. 编译器只是会整理初始化器, 并得到一个包含各属性默认值的构造函数
只读自动属性
public bool InStock { get; } = true;
相当于
public Product(bool stock = true) {
InStock = stock;
}
public bool InStock { get; }
由于没有set权限, 只能在构造对象时赋值
内插字符串
string.Format()
是C#拼接字符串的传统方式, 现在可以使用新方法
results.Add(string.Format("Name: {0}, Price: {1}, Related: {2}", name, price, relatedName));
可替换为
results.Add($"Name: {name}, Price: {price}, Related: {relatedName}");
内插字符串以$
为前缀, 包含孔(以{}
包含), 孔内可以填写简单的表达式. 计算字符串时, 会使用指定的表达式的值填充这些孔.
内插字符串支持string.Format()
的所有格式说明符, 只要用在孔中, 如$"Price: {Price:C2}"
将会把价格值格式化为具有两个十进制数字的货币值.
对象和集合初始化器
对象初始化器可以将创建对象和赋值简化为一步(个人觉得是因为已经简化了构造函数, 所以有必要这样做). 例子:
Product kayak = new Product {
Name = "Kayak",
Category = "Water Craft",
Price = 275M
};
如果没有这个特性, 就不得不这样写
Product kayak = new Product();
kayak.Name = "Kayak";
kayak.Category = "Water Craft";
kayak.Price = 275M;
相似的特性是集合初始化器, 旧的语法:
public ViewResult Index() {
string[] names = new string[3];
names[0] = "Bob";
names[1] = "Joe";
names[2] = "Alice";
return View("Index", names);
}
可以简化为
public ViewResult Index() {
return View("Index", new string[] { "Bob", "Joe", "Alice" });
}
索引初始化器
最新的C#版本整理了使用索引的集合(如Dictionary)的方法, 并提供了初始化器语法. 旧的语法:
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);
}
可以简化为:
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#最重要的新特性之一是支持模式匹配, 用于测试对象是否某种特定类型, 或有特定的特征, 这也是一种语法糖, 使用is
关键字
if (data[i] is decimal d) {
// ...
}
如果data[i]
是decimal
类型, 则表达式data[i] is decimal d
值为true, 并将data[i]
赋值给d
.
结合switch语句的模式匹配
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;
}
}
语法如下
case <Type> outputVariableName [when condition]:
// ...
扩展方法
扩展方法是一种给不属于自己的或不方便修改的类增加方法的方便途径. 通过在函数定义中使用this
关键字, 可以定义扩展方法
public static decimal TotalPrices(this ShoppingCart cartParam) {
decimal total = 0;
foreach (Product prod in cartParam.Products) {
total += prod?.Price ?? 0;
}
return total;
}
扩展方法必须定义在自己的static类中, 并定义为public static. 第一个this参数指定了可以调用此方法的对象, 并可以在方法体中使用. 如上, 则可以调用cartParam.TotalPrices()
, 第一个参数在调用时被忽略(类似python中类方法的定义)
注意: 扩展方法只能访问可以访问的类成员.
在接口中使用扩展方法
namespace LanguageFeatures.Models {
public class ShoppingCart : IEnumerable<Product> {
// ...
}
public static class MyExtensionMethods {
// ...
}
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}" });
}
}
}
因为定义了IEnumerable<Product>
的扩展方法, 所以直接继承接口的ShoppingCart
可以使用扩展方法, 泛型类Array<Product>
也可以使用
过滤器扩展方法
过滤器扩展方法可以用于筛选对象集合, 使用yield
关键字(生成器), 效果类似于LINQ
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
, 采用了一个额外的参数来筛选产品, 返回一个IEnumerable<Product>
对象, 包含价格大于参数的对象
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}" });
}
}
}
输出(Kayak + Lifejacket + Corner flag)
Total: $358.90
Lambda表达式
Lambda表达式令人迷惑, 特别是用它来简化的特性也令人迷惑. 要理解下面的问题, 请参照上文中定义的FilterByPrice
方法, 现在又想根据Name过滤数据
public static IEnumerable<Product> FilterByName(
this IEnumerable<Product> productEnum, char firstLetter) {
foreach (Product prod in productEnum) {
if (prod?.Name?[0] == firstLetter) {
yield return prod;
}
}
}
要是想使用多个过滤器呢? 这里采用了定义泛型委托的方式
public static IEnumerable<Product> Filter(
this IEnumerable<Product> productEnum,
Func<Product, bool> selector) {
foreach (Product prod in productEnum) {
if (selector(prod)) {
yield return prod;
}
}
}
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}" });
}
}
可简化为
decimal priceFilterTotal = productArray
.Filter(p => (p?.Price ?? 0) >= 20)
.TotalPrices();
decimal nameFilterTotal = productArray
.Filter(p => p?.Name?[0] == 'S')
.TotalPrices();
…也不知道摘的这些大家能不能看懂…反正会用的是肯定能看懂的, 就是把一行语句能写完的函数简化一下, 一个语法糖
(param1, param2, ...) => expression
或
(param1, param2, ...) => {
// ...
return result;
}
用在方法和属性中
public ViewResult Index() {
return View(Product.GetProducts().Select(p => p?.Name));
}
可以简化成这样
public ViewResult Index() =>
View(Product.GetProducts().Select(p => p?.Name));
另一个例子
public bool NameBeginsWithS => Name?[0] == 'S';
类型推断和匿名类型
类型推断
写C#的话, var
应该没人没用过吧…?
var name = new SimpleClassWithLongLongLongLongName();
一个优点是不用再打一遍类型名, 另一个优点不容易出类型错误.
匿名类型
想创建一个对象, 又不想定义一个类(可能就临时用一下), 用var
关键字
var productA = new { Name = "Kayak", Price = 275M };
之后就可以用productA.Name
和productA.Price
属性. 但这个类没办法访问啊(被编译器分配一个不容易出错的类名)…所以要创建集合的话也必须用var
.
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 }
};
异步方法
异步方法启动后将在后台完成工作, 并在完成后发出通知, 允许代码在执行后台工作时处理其他业务(多线程). 异步方法是消除代码瓶颈的重要工具, 允许应用程序使用多个处理器和处理器内核进行并行工作.
在MVC中, 可以使用异步方法来提高应用程序的整体性能(应对IO阻塞), 使用关键字async
和await
首先修改项目设置, 添加.NET组件以启用异步HTTP请求. 右击解决方案资源管理器中的项目, 编辑<ProjectName>.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.0" />
<PackageReference Include="System.Net.Http" Version="4.3.2" />
</ItemGroup>
</Project>
保存文件后, VS会自动下载软件包. 直接使用NuGet安装也行. 详见第六章
直接处理任务
C#和.NET对异步方法支持很好, 但await
和async
是比较新的特性, 之前的实现很繁琐. 如下代码
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
对象。
大多数程序员困惑的地方是continuation
, 这种机制可以指定后台任务完成后希望做什么. 此例中, 使用ContinueWith
方法来处理从HttpClient.GetAsync
获取的HttpResponseMessage
对象. 用lambda表达式返回内容长度值.
令人困惑的是使用两次return
. 第一个return
返回Task<HttpResponseMessage>
对象, 在任务结束后将返回ContentLength
的标头, 然后返回一个long?
结果, 所以返回值类型是Task<long?>
.
很多人被这种语法搞懵了, 所以微软简化了C#的异步方法语法.
使用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
方法的视为一个常规方法, 并将其返回值赋值给一个变量.
在使用await
时, 必须给方法加上async
关键字. 且返回值应为Task<>
类型. 控制器也使用相同的方法处理
namespace LanguageFeatures.Controllers {
public class HomeController : Controller {
public async Task<ViewResult> Index() {
long? length = await MyAsyncMethods.GetPageLength();
return View(new string[] { $"Length: {length}" });
}
}
}
获取名字
Web应用开发中有许多时候需要获取参数\变量\方法\类名字. 常见的例子包括处理来自用户的输入时抛出的异常或验证错误. 传统的方法是使用字符串来硬编码名字, 如
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}"));
}
LINQSelect
方法生成一个字符串序列, 描述了Name和Price属性, 输出为
Name: Kayak, Price: 275
Name: Lifejacket, Price: 48.95
Name: Soccer ball, Price: 19.50
Name: Corner flag, Price: 34.95
这里主要讨论的是这个部分Name: ...... Price:
. 这种方法容易出错, 主要是引入错误的输入或者代码重构忘了更新字符串. 这对显示给用户的信息来说很成问题, 应该使用nameof()
方法, 如下
return View(products.Select(p =>
$"{nameof(p.Name)}: {p.Name}, {nameof(p.Price)}: {p.Price}"));
输出和上面相同
总结
本章中, 我们浏览了一个高效MVC程序员需要知道的关键C#语言特性. C#是一种灵活的语言, 通常有不同的方法来处理问题, 但这些特性是在开发web应用中经常遇到的.
在下一章中, 我将介绍Razor引擎, 并解释如何在MVC web应用中生成动态内容.