C#5 初次接触(全)
原文:
zh.annas-archive.org/md5/2E6DA4A6D245D14BD719EE0F1D9AAED3
译者:飞龙
协议:CC BY-NC-SA 4.0
前言
C#是一种非常富有表现力和强大的语言,让您可以专注于应用程序而不是低级样板。在过去的十年中,C#编译器已经发展,包括了来自动态和函数式语言的许多功能,同时保持静态类型。最近,它已经应对了并发硬件的激增,引入了新的异步编程功能。
本书将帮助您快速了解最新版本的 C#。在设置开发环境后,您将了解语言的所有最新功能,包括任务并行框架、动态语言运行时、TPL 数据流,以及使用 async 和 await 进行异步编程。
本书涵盖内容
第一章 开始使用 C#,简要介绍了 C#的诞生,并设置开发环境以编译 C# 5。我们将涵盖编译器和框架的安装,以及所有主要编辑器,如 Visual Studio 和 MonoDevelop。
第二章 C#的演变,展示了 C#语言的成长和成熟。随着每个版本的发布,都引入了新功能,使 C#编程变得更加简单和富有表现力。
第三章 异步行动,讨论了异步编程,重点放在 5.0 版本上。从任务并行库(TPL)开始,到在这个版本中新引入的 async 和 await 关键字,您现在可以轻松编写响应迅速且可扩展的应用程序。
第四章 创建 Windows Store 应用,介绍了 Windows 8 引入了一种新的应用类型,运行在 WinRT 框架上,可以创建在 x86 和 ARM 架构上运行的应用程序。在本章中,我们将探讨创建一个简单的应用程序,该应用程序连接到互联网,从 Flickr 下载并显示图像。
第五章 移动 Web 应用,向您展示了如何使用 ASP.NET MVC 和 HTML 5 为用户创建非常复杂和引人入胜的体验。世界正在走向移动化,支持移动客户端的 Web 变得越来越重要。您将学习如何构建一个使用 HTML 5 地理位置 API 实时连接用户的移动优化 Web 应用程序。
第六章 跨平台开发,向您展示了作为 C#开发人员,您可以使用 Mono 框架针对非 Microsoft 平台进行开发。在本章中,您将学习如何使用 MonoMac 和 MonoDevelop 为 Mac OS 创建一个实用程序应用。使用 C#的能力可以转化为一个引人入胜的机会,如果您能够在不同平台之间共享大部分代码。
本书所需内容
为了测试和编译本书中的所有示例,您需要安装.NET 4.5 框架,该框架支持从 Windows Vista 及更高版本的所有 Windows 版本,您可以在以下网址找到:
www.microsoft.com/en-us/download/details.aspx?id=30653
为了编译和测试 Windows Store 和 ASP.NET MVC 项目(第四章 创建 Windows Store 应用和第五章 移动 Web 应用),您需要安装 Visual Studio 2012 的一个版本(www.microsoft.com/visualstudio
)。此外,Windows Store 项目要求您在 Windows 8 上运行 Visual Studio 2012。
本书的最终项目在第六章中,跨平台开发是使用 MonoMac (www.mono-project.com/MonoMac
)和 MonoDevelop (monodevelop.com
)创建一个 Mac OS 应用程序。您必须在 Mac 上开发这个项目。
本书适用对象
本书适用于想了解最新版本 C#的开发人员。假定您具有基本的编程知识。有先前版本 C#或.NET Framework 的经验会有所帮助,但不是必需的。
约定
在本书中,您会发现一些文本样式,用于区分不同类型的信息。以下是一些这些样式的示例,以及它们的含义解释。
文本中的代码单词显示如下:"默认情况下,csc
将输出一个可执行文件。"
代码块设置如下:
using System;namespace program
{class MainClass{static void Main (string[] args){Console.WriteLine("Hello, World");}}
}
任何命令行输入或输出都以以下方式书写:
PS ~> $env:Path += ";C:\Windows\Microsoft.NET\Framework\v4.0.30319"
新术语和重要单词以粗体显示。您在屏幕上看到的单词,比如菜单或对话框中的单词,会以这样的方式出现在文本中:"通过单击文件 | 新建项目…来创建一个新项目。"。
注意
警告或重要提示会以这样的方式出现在一个框中。
提示
提示和技巧会以这样的方式出现。
第一章:入门 C#
在这一章中,我们将讨论 C#首次推出时行业的一般状况,以及它成为一种优秀语言的一些原因。在本章结束时,你将拥有一个完全可用的开发环境,准备好运行本书中的所有示例。
起源
每个漫画超级英雄都有一个起源故事,每个行业的专业人士也是如此。与同事分享起源故事很棒,因为它可以作为对过去情况的反思,以及对事物如何演变和未来可能走向的思考。我的个人故事起源于上世纪 90 年代末的高中时期,当时我看着比我大五岁、正在上大学的哥哥学习 C++。通过一些神秘的指令,复杂的程序变得生动起来,准备好行动。我着迷了。
这种力量的初次体验只是开始。大约在同一时间,我班上的一个朋友开始制作一款游戏,同样是用 C++编写,风格类似于 NES 游戏《塞尔达传说》。虽然我曾经短暂地瞥见过旧的 QBasic 游戏,比如《Gorillas》,但我对他在小型演示中所取得的质量感到惊讶。我决定认真学习如何编程,考虑到我认识的每个人都在使用 C++,这成为了我第一门编程语言的默认选择。
我写的第一个程序是一个非常简单的财务预算程序。当时我刚刚开始在高中的第一份工作,我非常清楚管理金钱所涉及的新责任,所以我想写一个程序来帮助我更好地管理我的资金。首先,它要求输入我的工资金额,然后输入我必须支付的账单清单。
经过一些基本的计算,它给我提供了一个报告,告诉我在照顾好我的责任之后还剩下多少可支配收入。就程序而言,它并不是最复杂的软件,但它帮助我学会了一些基础知识,比如循环、条件语句、存储不确定数量项目的列表,以及对数组执行聚合操作。
这是一个巨大的个人胜利,但在初步探索 C++后,我发现自己遇到了一些困难。对于一个刚接触编程的人(还在上高中),C++很难完全掌握。我不仅需要学习软件的基础知识,还必须时刻注意我使用的内存。最终,我发现了当时对我来说更简单的网页开发工具。我从复杂性谱的一端移动到了另一端。
当时的软件领域大部分被计算机语言所主导,这些语言可以分为三大类:低级系统语言,比如 C++,在性能和灵活性方面提供了最多的功能,但也很难和复杂;解释性语言,比如 JavaScript 和 VBScript,其指令在运行时被评估,非常易于使用和学习,但无法与低级语言的性能相匹敌;最后是一组介于两者之间的语言。
这条中间路线涵盖了诸如 Java 和 Visual Basic 等语言,既提供了最好的一面,也带来了最糟糕的一面。在这些语言中,你有一个垃圾收集器,这意味着当你创建一个对象时,你不必在完成后显式释放已使用的内存。它们还被编译成中间语言(例如,VB 的 p 代码和 Java 的字节码),然后在目标平台上本地运行的虚拟机中执行。由于这种中间语言类似于机器代码,它能够比纯解释语言执行得更快。然而,这种性能仍然不是真正与经过适当调整的 C++程序相匹敌的,因此与 C++相比,Java 和 Visual Basic 程序通常被认为是慢语言。
尽管存在一些缺点,但对于微软来说,拥有一个受管理的内存环境的好处是显而易见的。因为程序员不必担心指针和手动内存管理等复杂概念,程序可以更快地编写,并且出现更少的错误。快速应用程序开发(简称RAD)似乎是微软平台的未来方向。
在 90 年代末,他们开发了一个 Java 虚拟机的版本,据许多人说比市场上其他实现更快。不幸的是,由于他们包含了一些专有扩展,并且他们没有完全实现 Java 1.1 标准,他们在 1997 年遇到了一些法律问题。这最终导致微软停止了他们对 Java 实现的开发,并最终在 2001 年将其从他们的平台中移除。
虽然我们无法知道接下来发生的事情是否是对微软 Java 虚拟机的法律行动的直接结果,但我们知道的是,1999 年微软开始研发一种名为Cool(类 C 对象导向语言)的新编程语言。
C#诞生
然后发生了这件事;在 2000 年,微软宣布他们正在开发一种新的编程语言。这种最初被称为 Cool 的语言在 2000 年奥兰多的专业开发者大会上作为C#公布。这种新语言的一些亮点是:
-
它基于 C 系列编程语言的语法,因此对于有 C++、Java 或 JavaScript 经验的人来说,语法非常熟悉。
-
C#的内存管理类似于 Java 和 Visual Basic,具有非常强大的垃圾收集器。这意味着用户可以专注于他们应用程序的内容,而不必担心样板式的内存管理代码。
-
C#编译器以及静态类型系统意味着某些类别的错误可以在编译时捕获,而不必像在 JavaScript 中那样在运行时处理它们。这是一个即时编译器,这意味着代码将在运行时编译为本机可执行文件,并针对执行代码的操作系统进行优化。性能是新平台的一个重要目标。
-
这种语言具有强大且广泛的基类库,这意味着许多功能块将直接内置到框架中。除了一些行业标准库,如 Boost,没有很多常见的 C/C++库,这导致人们经常重写常见功能。另一方面,Java 有很多库,但它们是由不同的开发人员编写的,这意味着功能和风格的一致性是一个问题。
-
它还与其他在公共语言运行时(CLR)上运行的语言具有互操作性。因此,一个程序可以使用用不同语言编写的功能,从而为每种语言使用其最擅长的功能。
-
微软向 ISO 工作组提交了规范。这为框架周围的充满活力的开源社区打开了大门,因为这意味着总会有一个标准可供参考。一个名为Mono的流行开源实现了.NET Framework 和 C#,让你可以在不同的平台上运行你的代码。
尽管这个列表中描述的元素都不是特别新的,但 C#的目标是吸收以前的编程语言的最佳特点,包括 C++的强大和功能、JavaScript 的简单性以及 VBScript/ASP 的易于托管等。
任何语言(C、C++或 Java)的人都可以很轻松地在 C#中提高生产力。C#找到了生产力、功能和学习曲线的完美交汇点。
在接下来的十年里,这种语言将继续发展出一套非常有吸引力的功能,使编写优秀的程序变得更加容易和快速。现在在其第五个版本中,C#语言已经变得更加表达力和强大,具有语言集成查询(LINQ)、任务并行库(TPL)、动态语言运行时(DLR)和异步编程功能等特性。更重要的是,通过 Mono 框架,你不仅可以针对 Windows,还可以针对其他主流平台,如 Linux、Mac OS、Android、iOS,甚至游戏主机,如 Playstation Vita。
无论你过去十年是否一直在写 C#,还是刚刚开始学习,这本书都会带你了解最新版本 5.0 的所有功能。我们还将探讨 C#的演变和历史,以便你了解为什么某些功能会以这种方式发展,以及如何充分利用它们。
不过,在我们开始之前,我们需要配置你的计算机,以便能够编译所有的示例。本章将指导你安装所有你需要的东西,以便你能够完成本书中的每个示例。
工具
每当你接触一个新的编程语言或工具时,你可以问自己几个问题,以便快速熟练掌握这个环境,比如:
-
你如何构建一个程序,或者为部署做好准备?
-
你如何调试一个程序?快速找出问题所在以及问题出现的位置。这和一开始编写程序一样重要。
在接下来的章节中,我们将回顾一些可用的工具,以便在你的本地计算机上建立一个开发环境。这些选项在许多不同的许可条款和成本结构之间变化。无论你的情况或偏好如何,你都能够建立一个开发环境,并且在本章结束时你将能够回答之前的问题。
Visual Studio
微软为 C#语言提供了事实上的编译器和开发环境。尽管自.NET Framework 首次发布以来,编译器就作为一个命令行可执行文件可用,但大多数开发人员会留在 Visual Studio 的范围内,这是微软的集成开发环境(IDE)。
Visual Studio 的完整版本
微软的 Visual Studio 完整商业版本有几个不同的版本,每个版本都有累积的功能数量,随着你的提升而增加。
-
专业版:这是基本的商业套餐。它允许你在所有可用的语言中构建所有可用的项目。在 C#的上下文中,一些可用的项目类型包括 ASP.NET WebForms、ASP.NET MVC、Windows 8 应用、Windows Phone、Silverlight、库、控制台,以及一个强大的测试框架。
-
高级版:在这个版本中,除了包括所有专业功能外,还包括了代码度量、扩展测试工具、架构图、实验室管理和项目管理功能。
-
Ultimate:此版本包括代码克隆分析、更多测试工具(包括 Microsoft Fakes)和 IntelliTrace,以及所有先前级别的功能。
在www.microsoft.com/visualstudio/11/enus/products/visualstudio
上查看这些版本。
许可证
完整版本的 Visual Studio 有几种不同的许可证选项。
-
MSDN 订阅:微软开发人员网络提供订阅服务,您可以支付年费以获得 Visual Studio 的各个版本。此外,您可以作为微软 MVP 计划的一部分获得 MSDN 订阅,该计划奖励开发社区中的活跃成员。您可以在
msdn.microsoft.com/en-us/subscriptions/buy/buy.aspx
找到有关购买 MSDN 订阅的更多信息。 -
BizSpark:如果您正在创建一家初创公司,微软提供 BizSpark 计划,让您在三年内免费获得 Microsoft 软件(包括 Visual Studio)的访问权限。毕业后,您可以保留在计划过程中下载的许可证,并获得 MSDN 订阅的折扣,以及其他校友福利。BizSpark 是任何想要使用微软技术堆栈的企业家的绝佳选择。在
www.microsoft.com/bizspark
上查看您是否符合 BizSpark 计划的资格。 -
DreamSpark:学生可以参加 DreamSpark 计划,该计划允许您下载 Visual Studio Professional(以及其他应用程序和服务器)。只要您是有效学术机构的学生,您就可以访问使用 C#编写应用程序所需的一切。立即在
www.dreamspark.com/
注册。 -
个人和批量许可:如果之前的商业版 Visual Studio 的选项都不合适,那么您可以直接从微软或各种经销商购买许可证,网址为
www.microsoft.com/visualstudio/en-us/buy/small-midsize-business
。
Express
Visual Studio Express产品系列是一个几乎完整功能的 Visual Studio 版本,而且是免费的。任何人都可以下载这些产品并开始学习和开发,而无需付费。
可用的版本如下:
-
Visual Studio Express 2012 for Windows 8:用于创建 Windows 8 的 Metro 样式应用程序
-
Visual Studio Express 2012 for Windows Phone:这让您为微软的 Windows Phone 设备编写程序。
-
Visual Studio Express 2012 for Web:所有 Web 应用程序都可以使用这个版本的 Visual Studio 构建,从 ASP.NET(表单和 MVC)到 Azure 托管项目
-
Visual Studio Express 2012 for Desktop:可以使用这个版本构建针对经典Windows 8 桌面环境的应用程序。
人们普遍误解 Visual Studio Express 只能用于非商业项目,但事实并非如此。您完全可以在遵守最终用户许可协议的情况下开发和发布商业产品。唯一的限制是技术上的,如下所示:
-
Visual Studio 的 Express 版本受垂直堆栈的限制,这意味着您必须为每种受支持的项目类型(Web、桌面、手机等)安装单独的产品。不过,这几乎不是一个巨大的限制,在最复杂的解决方案中才会成为负担。
-
没有插件。对于全版本的 Visual Studio,有许多提高生产力的插件可用,因此对于一些用户来说,这种排除可能是一个大问题。然而,好消息是,最近记忆中最受欢迎的插件之一,NuGet,现在已经随 Visual Studio 2012 的所有版本一起发布。NuGet 帮助您管理项目的库依赖关系。您可以浏览 NuGet 目录,并添加开源第三方库,以及来自 Microsoft 的库。
Visual Studio 的 Express 版本可以从www.microsoft.com/visualstudio/11/en-us/products/express
下载。
使用 Visual Studio
无论您决定使用哪个版本的 Visual Studio,一旦产品安装完成,入门都非常简单。以下是步骤:
-
启动 Visual Studio,或者如果您使用 Express 版本,则启动 Visual Studio Express for Desktop。
-
通过点击文件 | 新建项目...来创建一个新项目。
-
从已安装 | 模板 | Visual C#中选择控制台应用程序。
-
给项目取一个名称,比如
program
,然后点击确定。 -
在
Main
方法中添加一行代码如下:
Console.WriteLine("Hello World");
- 通过选择调试 | 无调试运行来运行程序。
您将看到预期的Hello World输出,现在可以开始使用 Visual Studio 了。
命令行编译器
如果您喜欢以比 Visual Studio 这样的 IDE 更低的级别工作,您总是可以选择直接使用命令行编译器。微软通过从www.microsoft.com/en-us/download/details.aspx?id=8483
下载和安装.NET 4.5 可再发行包,为您提供了免费编译 C#代码所需的一切。
一旦下载并安装了,您可以在C:\windows\microsoft.net\Framework\v4.0.30319\csc.exe
找到编译器,假设您保留了所有默认安装选项:
注意
请注意,如果您安装了.NET Framework 4.5 版本,它实际上会替换 4.0 框架。这就是为什么之前提到的路径显示为v4.0.30319
。你不会是第一个被.NET Framework 版本搞糊涂的人。
使命令行编译器更容易使用的一个小技巧是将其添加到环境的Path
变量中。如果您使用 PowerShell(我强烈鼓励),您可以通过运行以下命令轻松实现:
PS ~> $env:Path += ";C:\Windows\Microsoft.NET\Framework\v4.0.30319"
这样你就可以只输入csc
而不是整个路径。命令行编译器的使用非常简单,看下面的类:
using System;namespace program
{class MainClass{static void Main (string[] args){Console.WriteLine("Hello, World");}}
}
将此类保存为名为program.cs
的文件,使用您喜欢的文本编辑器。保存后,您可以使用以下命令从命令行编译它:
PS ~\book\code\ch1> csc .\ch1_hello.cs
这将生成一个名为ch1_hello.exe
的可执行文件,当执行时,将产生一个熟悉的问候,如下所示:
PS ~\book\code\ch1> .\ch1_hello.exe
Hello, World
默认情况下,csc
将输出一个可执行文件。但是,您也可以使用目标参数来生成库。考虑以下类:
using System;namespace program
{public class Greeter{public void Greet(string name){Console.WriteLine("Hello, " + name);}}
}
这个类封装了前一个程序的功能,并且通过让您定义要问候的名称,甚至使其可重用。尽管这是一个有点陈词滥调的例子,但重点是要展示如何创建一个.dll
文件,您可以从多个程序中使用。
PS ~\dev\book\code\ch1> csc /target:library .\ch1_greeter.cs
将生成一个名为ch1_greeter.dll
的程序集,然后您可以从稍微修改过的前一个程序中使用它,如下所示:
using System;namespace program
{class MainClass{static void Main (string[] args){Greeter greeter = new Greeter();greeter.Greet("Componentized World");}}
}
如果您尝试像以前一样编译前一个程序,编译器将正确地抱怨不知道Greeter
类的任何信息,如下所示:
PS ~\book\code\ch1> csc .\ch1_greeter_program.cs
Microsoft (R) Visual C# Compiler version 4.0.30319.17626
for Microsoft (R) .NET Framework 4.5
Copyright (C) Microsoft Corporation. All rights reserved.ch1_greeter_program.cs(9,13): error CS0246: The type ornamespace name 'Greeter' could not be found (are youmissing a using directive or an assembly reference?)
ch1_greeter_program.cs(9,35): error CS0246: The type ornamespace name 'Greeter' could not be found (are youmissing a using directive or an assembly reference?)
每当程序出现错误时,它将显示在输出中,并提供有关发现错误的文件和行的信息,因此您可以轻松找到它。为了使其工作,您将不得不告诉编译器使用使用/r:
参数创建的ch1_greeter.dll
文件,如下所示:
PS ~\book\code\ch1> csc /r:ch1_greeter.dll .\ch1_greeter_program.cs
现在当您运行生成的ch1_greeter_program.exe
程序时,您将看到输出显示Hello, Componentized World。
尽管大多数开发人员现在不会直接使用命令行编译器,但了解它的可用性以及如何使用它是很好的,特别是如果您必须支持高级场景,例如将多个模块合并为单个程序集。
SharpDevelop
当您启动 SharpDevelop 时,加载屏幕上的标语The Open Source .NET IDE是一个简洁的描述。自.NET Framework 的早期,它为开发人员提供了一个免费的选项来编写 C#,在 Microsoft 发布 Express 版本之前。自那时起,它一直在不断成熟和增加功能,截至 4.2 版本,SharpDevelop 支持针对.NET 4.5 的目标,更具体地说,支持 C# 5.0 的编译和调试。虽然 Visual Studio Express 是一个引人注目的选择,但缺乏源代码控制插件可能会成为一些用户的断档因素。幸运的是,SharpDevelop 将乐意让您在 IDE 中集成源代码控制服务器。此外,一些更为专业的项目类型,如创建 Windows 服务(Express 不支持的少数项目类型之一),在 SharpDevelop 中得到了充分支持。
项目使用与 Visual Studio 相同的格式(.sln,.csproj),因此项目的可移植性很高。通常可以将在 Visual Studio 中编写的项目打开在 SharpDevelop 中。
从www.icsharpcode.net/OpenSource/SD/
下载应用程序。
安装非常简单,您可以通过创建以下示例程序来验证正确的安装:
-
启动 SharpDevelop。
-
通过单击文件 | 新建 | 解决方案来创建一个新项目。
-
从C# | Windows 应用程序中选择控制台应用程序。
-
给项目取一个名称,如
program
,然后单击创建。 -
右键单击项目窗口中的项目节点,选择属性菜单项;检查编译选项卡,看看目标框架是否说.NET Framework 4.0 Client Profile。
-
如果是这样,只需单击更改按钮,在更改目标框架下拉菜单中选择.NET Framework 4.5,最后单击转换按钮。
-
通过选择调试 | 无调试运行来运行程序。
您将看到预期的Hello World输出。
MonoDevelop
Mono框架是 Common Language Runtime 和 C#的开源版本。它经过了十多年的积极开发,因此非常成熟和稳定。Mono 有适用于几乎任何平台的版本,包括 Windows,OS X,Unix/Linux,Android,iOS,PlayStation Vita,Wii 和 Xbox 360。
MonoDevelop 基于 SharpDevelop,但在一段时间前分叉,专门作为运行在多个平台上的 Mono 的开发环境。它可以在 Windows,OS X,Ubuntu,Debian,SLE 和 openSUSE 上运行;因此,作为开发人员,您可以真正选择要在哪个平台上工作。
您可以通过从www.go-mono.com/mono-downloads/download.html
安装 Mono Development Kit 2.11 或更高版本来开始。
安装了适用于您平台的软件后,您可以继续从monodevelop.com/
安装最新版本的 MonoDevelop。
使用 C# 5.0 编译器只需几个简单的步骤:
-
启动 MonoDevelop。
-
通过单击文件 | 新建 | 解决方案…来创建一个新项目。
-
从C#菜单中选择控制台应用程序。
-
给项目命名为
program
,然后单击“前进”,然后单击“确定”。 -
在“解决方案”窗口中右键单击项目节点,然后选择“选项”菜单项。现在转到“生成”|“常规”以查看“目标框架”是否为“Mono/.NET 4.0”。
-
如果是这样,那么只需从下拉菜单中选择“.NET Framework 4.5”,然后单击“确定”按钮。
-
通过选择“运行”|“开始调试”来运行程序。
如果一切顺利,您将看到一个终端窗口(例如在 OS X 上运行),显示“Hello World”文本。
总结
C#语言的引入正好是在合适的时候,它是一种现代的面向对象的语言,汲取了之前许多语言的优点。具有低级别的强大功能和即时编译,垃圾回收环境的简单性,以及运行时的灵活性,可以轻松与其他语言进行互操作,还有一个出色的基类库、蓬勃发展的开源社区,以及能够针对多个平台进行开发的能力,这些都使 C#成为一个引人注目的选择。
在本章中,我们讨论了设置开发环境并下载所有相关工具和运行时的步骤。
-
Visual Studio 有商业和免费选项。
-
命令行对于直接插入使用 shell 命令的自动化工具非常有用。
-
SharpDevelop 是 Visual Studio 的开源替代品。
-
MonoDevelop 是.NET Framework 和 C#的开源实现的官方 IDE。这使您可以针对多个平台进行开发。
一旦您选择了首选的开发环境,并按照本章详细介绍的步骤进行操作,您就可以继续阅读本书的其余部分以及其中包含的所有示例。
在下一章中,我们将讨论语言的演变,这将帮助您了解沿途引入的功能,并有助于我们如今拥有 C# 5.0 的地位。
第二章:C#的演变
在这一章中,我们回顾了整个 C#的历史,直到最新版本。我们可能无法涵盖所有内容,但我们将涉及主要特性,特别是那些在历史上具有重要意义的特性。每个版本都带来了独特的功能,这些功能将成为未来版本创新的基石。
C# 1.0 - 起初
当引入 C#时,微软希望借鉴许多其他语言和运行时的最佳特性。它是一种面向对象的语言,具有主动管理内存的运行时。这种语言(和框架)的一些特性包括:
-
面向对象
-
托管内存
-
丰富的基类库
-
公共语言运行时
-
类型安全
无论您的技术背景如何,C#中都有一些您可以相关的东西。
运行时
谈论 C#时不可能不先谈论运行时,称为公共语言运行时(CLR)。CLR 的一个主要基础是能够与多种语言进行互操作,这意味着您可以使用多种不同的语言编写程序,并且它将在相同的运行时上运行。这种互操作性是通过同意一组共同的数据类型来实现的,称为公共类型系统。
在.NET Framework 之前,不同语言之间没有清晰的机制进行交流。一个环境中的字符串可能与另一种语言中的字符串概念不匹配,它们是否以空字符结尾?它们是否以 ASCII 编码?数字如何表示?没有办法知道,因为每种语言都有自己的特点。当然,人们试图想出解决这个问题的办法。
在 Windows 世界中,最著名的解决方案是使用组件对象模型(COM),它使用类型库来描述其中包含的类型。通过导出此类型库,程序可以与可能使用另一种技术编写的其他进程进行通信,因为您共享了如何通信的细节。然而,这并不是没有复杂性的,因为任何使用 Visual Basic 6 编写 COM 库的人都可以告诉您。不仅是因为工具抽象了底层技术,过程通常相当不透明,而且部署是一场噩梦。甚至有一个为处理它而知名的短语,DLL 地狱。
.NET Framework 在某种程度上是对这个问题的回应。它引入了公共类型系统的概念,这些规则是 CLR 上运行的每种语言都需要遵守的规则,包括常见的数据类型,如字符串和数字类型,对象继承的方式以及类型可见性。
为了最大的灵活性,不直接编译成本机二进制代码,而是使用程序代码的中间表示作为实际的二进制映像,该映像被分发和执行,称为MSIL。然后第一次运行程序时编译此 MSIL,以便为正在运行的特定处理器架构放置优化(即时(JIT)编译器)。这意味着在服务器和桌面上运行的程序可能具有不同的性能特征,这取决于硬件。在过去,您将不得不编译两个不同的版本。
固有的多语言支持的另一个好处是它作为迁移策略。与 C#同时推出了许多不同的语言。拥有现有代码库的公司可以轻松地将其程序转换为与.NET 兼容的版本,该版本与 CLR 兼容,并随后可以从其他.NET 语言(如 C#)中使用。其中一些语言包括以下内容:
-
VB.NET:这是流行的 Visual Basic 6 的自然继承者。
-
J#:这是用于.NET 的 Java 语言的一个版本。
-
C++的托管扩展:通过一个标志,以及一些新的关键字和语法,你可以将现有的 C++应用程序编译成与 CLR 兼容的版本。
虽然许多这些语言都已经发布,并且被宣传为得到充分支持,但如今真正留存下来的只有 VB.Net 和 C#,而 C++/CLI 则略有些。许多新的语言,如 F#、IronPython 和 IronRuby,多年来都在 CLR 上涌现,并且它们仍然在开发中,拥有活跃的社区。
内存管理
公共语言运行时提供了垃圾收集器,这意味着当一个对象不再被其他对象引用时,内存将被自动回收。当然,这并不是一个新概念;许多语言,如 JavaScript 和 Visual Basic 都支持垃圾收集。另一方面,非托管语言允许你手动在堆上分配内存。虽然这种能力在实现低级解决方案时给了你更多的权力,但也给了你更多犯错误的机会。
以下是 CLR 允许的两种数据类型:
-
值类型:这些数据类型是使用
struct
关键字创建的 -
引用类型:这些数据类型是使用
class
关键字创建的
C#中的每种原始数据类型,如int
和float
,都是struct
,而每个类都是引用类型。关于这些类型在内部分配的语义学有一些细微差别(栈与堆),但在日常使用中,这些差异通常并不重要。
当然你也可以创建自己的自定义类型。例如,以下是一个简单的值类型:
public struct Person
{public int Age;public string Name;
}
通过更改一个关键字,你可以将这个对象更改为引用类型,如下所示:
public class Person
{public int Age;public string Name;
}
struct
实例和class
实例之间有两个主要区别。首先,struct
实例不能继承,也不能被继承。而class
实例则是创建面向对象层次结构的主要工具。
其次,class
实例参与垃圾回收过程,而struct
实例则不参与,至少不是直接参与。互联网上的许多讨论往往将值类型的内存分配策略概括为在栈上分配,而引用类型在堆上分配(见下图),但这并不是全部故事:
通常这种说法总是有一些道理的,因为当你在一个方法中实例化class
时,它总是会在堆上,而创建一个值类型,比如int
实例会在栈上。但是如果值类型被包装在引用类型中,例如,当值类型是class
实例中的一个字段时,它将与类数据的其余部分一起分配在堆上。
语法特性
C#这个名字是对 C 语言的一个俏皮的引用,就像 C++是 C(加上一些东西),C#在语法上也与 C 和 C++大致相似,尽管有一些明显的变化。与 C 不同,C#是一种面向对象的语言,但它有一些有趣的特性,使得它比其他面向对象的语言更容易和更高效。
其中一个例子是属性的 getter 和 setter。在其他语言中,如果你想控制如何公开对class
实例的数据的访问和修改,你必须将字段设置为私有,这样外部人员就不能直接更改它。然后你会创建两个方法,一个以get
为前缀来检索值,另一个以set
为前缀来设置值。在 C#中,编译器会为你生成这些方法对。看下面的代码:
private int _value;public int Value
{get { return _value; }set { _value = value; }
}
另一个有趣的创新是 C#如何提供一流的事件支持。在其他语言中,比如 Java,他们通过,例如,有一个名为setOnClickListener(OnClickListener listener)
的方法来近似事件。要使用它,你必须定义一个实现OnClickListener
接口的新类并将其传递进去。这种技术确实有效,但可能有点冗长。在 C#中,你可以定义所谓的delegate来表示一个方法作为一个适当的、独立的对象,如下所示:
public delegate void MyClickDelegate(string value);
然后,这个delegate
可以作为类的事件使用,如下所示:
public class MyClass
{public event MyClickDelegate OnClick;
}
要注册在事件被触发时收到通知,你只需创建委托并使用+=
语法将其添加到委托列表中,如下所示:
public void Initialize()
{MyClass obj = new MyClass();obj.OnClick += new MyClickDelegate(obj_OnClick);
}void obj_OnClick(string value)
{// react to the event
}
语言将自动将委托添加到委托列表中,每当事件被触发时,委托列表都会收到通知。在 Java 中,这种行为必须手动实现。
当 C#推出时,还有许多其他有趣的语法特性,比如异常的工作方式、使用语句等。但为了简洁起见,让我们继续。
基类库
默认情况下,C#带有一个称为基类库(BCL)的丰富而广泛的框架。BCL 提供了各种功能,如下图所示:
该图显示了包含在基类库中的一些命名空间(重点是一些)。虽然还有许多其他命名空间,但这些是一些最重要的命名空间之一,为许多由微软和第三方发布的功能和库提供了基础设施。
在学习编程时,你会发现处理信息集合的数据结构类型之一。通常,你会学会使用数组来编写大多数程序和算法。不过,使用数组时,你必须提前知道集合的大小。System.Collections
命名空间提供了一组处理未知数量数据的数据结构集合,使其易于处理。
在我写的第一个程序中(在上一章中简要描述),我使用了一个预先分配的数组。为了保持简单,数组是用任意大的元素数量分配的,这样我就不会在数组中用完空间。当然,在专业编写的非平凡程序中,这是行不通的,因为如果遇到比预期更大的数据集,你要么会用完空间,要么会浪费内存。在这里,我们可以使用最基本的集合类型之一,ArrayList
集合,来解决这个问题,如下所示:
// first, collect the paycheck amount
Console.WriteLine("How much were you paid? ");
string input = Console.ReadLine();
float paycheckAmount = float.Parse(input);// now, collect all of the bills
Console.WriteLine("What bills do you have to pay? ");
ArrayList bills = new ArrayList();
input = Console.ReadLine();
while (input != "")
{float billAmount = float.Parse(input);bills.Add(billAmount);input = Console.ReadLine();
}// finally, summ the bills and do the final output
float totalBillAmount = 0;
for (int i = 0; i < bills.Count; i++)
{float billAmount = (float)bills[i];totalBillAmount += billAmount;
}if (paycheckAmount > totalBillAmount)
{Console.WriteLine("You will have {0:c} left over after paying bills", paycheckAmount - totalBillAmount);
}
else if (paycheckAmount < totalBillAmount)
{Console.WriteLine("Not enough money, you need to find an extra {0:c}", totalBillAmount - paycheckAmount);
}
else
{Console.WriteLine("Just enough to cover bills");
}
如你所见,创建了一个ArrayList
集合的实例,但没有指定大小。这是因为集合类型在内部管理它们的大小。这种抽象解除了你对大小的责任,所以你可以担心更重要的事情。
其他可用的一些集合类型如下:
-
HashTable
:这种类型允许你提供查找键和值。它通常用于创建非常简单的内存数据库。 -
Stack
:这是一种后进先出的数据结构。 -
Queue
:这是一种先进先出的数据结构。
查看具体的集合类并不能完全说明问题。如果你跟踪继承链,你会注意到每个集合都实现了一个名为IEnumerable
的接口。这将成为整个语言中最重要的接口之一,所以早期熟悉它是很重要的。
IEnumerable
和它的姊妹类IEnumerator
抽象了对项目集合的枚举概念。你总是会看到这些接口一起使用,它们非常简单。你可以看到这一点,如下所示:
namespace System.Collections
{public interface IEnumerable{IEnumerator GetEnumerator();}public interface IEnumerator{object Current { get; }bool MoveNext();void Reset();}
}
乍一看,你可能会想为什么集合实现IEnumerable
,它有一个返回IEnumerator
的方法,而不直接实现IEnumerator
。枚举器负责枚举集合。但这样做是有原因的。如果集合本身是枚举器,那么你就无法同时迭代同一个集合。因此,每次调用GetEnumerator()
通常会返回一个单独的枚举器,尽管这并不是必须的。
尽管接口非常简单,但事实证明,拥有这种抽象是非常强大的。C#实现了一个很好的简写语法,可以在不使用索引变量的情况下迭代集合。这在下面的代码中有解释:
int[] numbers = new int[3];
numbers[0] = 1;
numbers[1] = 2;
numbers[2] = 3;foreach (int number in numbers)
{Console.WriteLine(number);
}
foreach
语法之所以有效,是因为它是编译器实际生成的代码的简写。它将生成与枚举器的交互的代码。因此,前面示例中的循环将看起来像编译后的 MSIL,如下所示:
IEnumerator enumerator = numbers.GetEnumerator();while (enumerator.MoveNext())
{int number = (int)enumerator.Current;Console.WriteLine(number);
}
再次,我们有一个例子,C#编译器生成的代码与你实际编写的代码不同。这将是 C#演变的关键,使你更容易表达你编写的常见代码模式,从而让你更高效、更有生产力。
对于一些开发人员来说,C#在刚开始时是 Java 的廉价模仿。但对于像我这样的开发人员来说,它是一股清新的空气,提供了比 VBScript 等解释性语言更好的性能改进,比 C++等语言更多的安全性和简单性,以及比 JavaScript 等语言更多的低级功能。
C# 2.0
C#语言、Runtime 和.NET Framework 的第一个重大更新是一个大事件。这个版本的重点是使语言更简洁、更容易编写。
语法更新
第一个更新为属性语法添加了一个小功能。在 1.0 中,如果你想要一个只读属性,你的唯一选择就是排除 setter,如下所示:
private int _value;public int Value
{get { return _value; }
}
所有内部逻辑都必须直接与_value
成员交互。在许多情况下这是可以的,除了需要有一些逻辑来控制何时以及如何允许更改该值的情况。或者类似地,如果需要触发事件,你必须创建一个私有方法,如下所示:
private void SetValue(int value)
{if (_value < 5)_value = value;
}
在 C# 2.0 中不再需要这样做,因为现在可以创建一个私有的 setter,如下所示:
private int _value;public int Value
{get { return _value; }private set{if (_value < 5)_value = value;}
}
一个小功能,但它增加了一致性,因为分开的 getter 和 setter 方法是 C#试图摆脱的第一个版本的东西之一。
另一个有趣的补充是可空类型。对于值类型,编译器不允许你将它们设置为 null 值,然而,现在你有一个新的关键字符,可以用来表示可空值类型,如下所示:
int? number = null;
if (number.HasValue)
{int actualValue = number.Value;Console.WriteLine(actualValue);
}
只需添加问号,值类型就被标记为可空,你可以使用.HasValue
和.Value
属性来决定在空值的情况下该做什么。
匿名方法
委托是 C#相对于其他语言的一个很好的补充。它们是事件系统的构建块。然而,C# 1.0 中实现的一个缺点是,它们使得阅读代码变得更加困难,因为当事件被触发时执行的代码实际上是在其他地方编写的。继续简化代码的趋势,匿名方法让你可以内联编写代码。例如,给定以下委托定义:
public delegate void ProcessNameDelegate(string name);
你可以使用匿名方法创建委托的实例,如下所示:
ProcessNameDelegate myDelegate = delegate(string name)
{Console.WriteLine("Processing Name = " + name);
};myDelegate("Joel");
这段代码是内联的,简短且易于理解。它还允许您像 JavaScript 等其他语言中的一等函数一样使用委托。但它不仅仅是更容易阅读。如果您想要向不接受参数的委托传递参数,您必须创建一个自定义类来包装方法实现和存储的值。这样,当调用委托(从而执行目标方法)时,它就可以访问该值。任何早期的多线程代码都充满了以下代码:
public class CustomThreadStarter
{private int value;public CustomThreadStarter(int val){this.value = val;}public void Execute(){// do something with 'value'}
}
这个类在构造函数中接受一个值并将其存储在一个私有成员中。然后稍后当调用委托时,该值可以被使用,就像在这种情况下使用它来启动一个新线程。这在以下代码中显示:
CustomThreadStarter starter = new CustomThreadStarter(55);
ThreadStart start = new ThreadStart(starter.Execute);
Thread thread = new Thread(start);
thread.Start();
使用匿名委托,编译器可以介入并大大简化先前提到的使用模式:
int value = 55;
Thread thread = new Thread(delegate(){// do something with 'value'Console.WriteLine(value);});
thread.Start();
这看起来可能很简单,但这里有一些严重的编译器魔法。编译器分析了代码,意识到匿名方法在方法体中需要value
变量,并自动生成了一个类,类似于我们在 C# 1.0 中必须创建的CustomThreadStarter
。结果是您可以轻松阅读的代码,因为它就在那里,与其余部分上下文相关。
部分类
在 C# 1.0 中,使用代码生成器来自动化诸如自定义集合之类的事情是常见的做法。当您想要向生成的代码添加自己的方法和属性时,通常需要从该类继承,或者在某些情况下,直接编辑生成的文件。这意味着您必须非常小心,以避免重新生成代码,否则会有覆盖自定义逻辑的风险。您会在许多第一代工具中找到类似以下评论的注释:
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:2.0.50727.3053
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
C# 2.0 在您的工具库中添加了一个额外的关键字partial
。使用partial类,您可以将类分解成多个文件。要查看这个过程,请创建以下类:
// A.generated.cs
public partial class A
{public string Name;
}
这代表了自动生成的代码。请注意,文件名中包含.generated
;这是一种采用的约定,尽管这对于工作来说并非必需,只是这两个文件都是同一个项目的一部分。然后在另一个文件中,您可以包含其余的实现,如下所示:
// A.cs
public partial class A
{public int Age;
}
然后所有成员将在运行时对生成的类型可用,因为编译器会负责将类拼接在一起。您可以自由地随意重新生成第一个文件,而不必担心覆盖您的更改。
泛型
C# 2.0 的主要特性增加是泛型,它允许您创建可以重复使用多种类型对象的类。过去,这种编程只能以两种方式实现。您可以使用参数的公共基类,以便从该类继承的任何对象都可以传递,而不管具体的实现方式如何。这种方法有点有效,但当您想要创建一个非常通用的数据结构时,它会变得非常有限。另一种方法实际上只是第一种方法的派生。而不是使用自己定义的基类,而是沿着继承树一直使用object
作为类型参数。
这是因为.NET 中的所有类型都派生自object
,因此您可以传入任何东西。这是原始集合类使用的方法。但即使这样也存在问题,特别是在传递值类型时,由于装箱的影响。您还必须每次都从对象中转换类型。
幸运的是,所有这些问题都可以通过使用泛型来缓解:
public class Message<T>
{public T Value;
}
在此示例中,我们定义了一个名为T
的泛型类型参数。泛型类型参数的实际名称可以是任何名称,T
只是一个约定的用法。当您实例化Message
类时,可以使用以下语法指定要存储在Value
属性中的对象的类型,如下所示:
Message<int> message = new Message<int>();
message.Value = 3;
int variable = message.Value;
因此,您可以将整数分配给字段,而不必担心性能,因为该值不会被装箱。当您想要使用它时,您也不必进行转换,就像使用对象一样。
泛型非常强大,但并非无所不能。为了突出一个关键的不足,我们将讨论几乎每个 C#开发人员在 2.0 首次发布时尝试的第一件事情之一——泛型数学。数学密集型应用程序的开发人员可能会使用领域的数学库。例如,游戏开发人员(或者实际上是做任何涉及 2D 或 3D 空间计算的人)将始终需要一个良好的Vector
结构,如下所示:
public struct Vector
{public float X;public float Y;public void Add(Vector other){this.X += other.X;this.Y += other.Y;}
}
但问题是它使用float
数据类型进行计算。如果您想要将其泛化并支持其他数值类型,比如int
、double
或decimal
,您该怎么办?乍一看,您可能会认为可以使用泛型来支持这种情况,如下所示:
public struct Vector<T>
{public T X;public T Y;public void Add(Vector<T> other){this.X += other.X;this.Y += other.Y;}
}
编译将导致错误,运算符'+='无法应用于类型'T'和'T'的操作数。这是因为,默认情况下,由于编译器无法知道在使用的类型上定义了哪些方法(以及由此推断出的操作),因此仅object
数据类型的成员可用于泛型参数。
幸运的是,微软在某种程度上预料到了这一点,并添加了一种称为泛型类型约束的东西。这些约束让您向编译器提示调用者将被允许使用的类型的种类,这反过来意味着您可以使用您约束的特性。例如,看看以下代码:
public void WriteIt<T>(T list) where T : IEnumerable
{foreach (object item in list){Console.WriteLine(item);}
}
在这里,我们添加了一个约束,即类型参数T
必须是IEnumerable
。因此,您可以编写代码,并确保任何调用此方法的调用者只会使用实现IEnumerable
接口作为类型参数的类型。您可以使用的其他参数约束如下:
-
class
:这表示类型参数必须是引用类型。 -
struct
:这意味着只允许值类型。 -
new()
:此类型必须有一个无参数的公共构造函数。它将允许您使用类似T value = new T()
的语法来创建类型参数的新实例。否则,您唯一能做的就是像T value = default(T)
这样的事情,对于引用类型,它将返回 null,对于数值原语,它将返回零。 -
<接口名称>
:这限制了类型参数使用此处提到的接口,如前面提到的IEnumerable
。 -
<类的名称>
:使用此约束的任何类型必须是此类型,或者在继承链中的某个时候继承自此类型。
很不幸,因为数值数据结构是值类型,它们无法继承,因此没有通用类型可用于类型约束,以便为您提供执行数学运算所需的数学运算符。
一般来说,泛型在“框架”样式的代码中最有用,也就是说,对于应用程序的一般基础设施,或者诸如集合之类的数据结构。实际上,在 C# 2.0 中提供了一些很棒的新集合类型。
泛型集合
泛型非常适合集合,因为集合本身实际上不必与其包含的对象进行交互;它只需要一个放置它们的地方。因此,在集合中,对类型参数没有约束。所有新的泛型集合都可以在以下命名空间中找到:
using System.Collections.Generic;
正如我们之前讨论的那样,C# 1.0 中最基本的集合类型是ArrayList
集合,当时它的工作效果非常好。然而,由于它使用object
作为其有效载荷类型,值类型将被装箱,并且每次想要取出一个值时,您都必须将对象转换为目标对象类型。有了泛型,我们现在有了List<T>
如下:
List<int> list = new List<int>();
list.Add(1);
list.Add(2);
list.Add(3);int value = list[1]; // returns 2;
使用方式与ArrayList
集合几乎相同,但具有泛型的性能优势。一些其他可用的泛型类类型如下:
-
Queue<T>
:这与非泛型的Queue
相同,先进先出(FIFO)。 -
Stack<T>
:与非泛型版本的Stack
没有区别,后进先出(LIFO)。 -
Dictionary<T, K>
:这取代了 C# 1.0 中的Hashtable
集合。它使用两个泛型参数来表示每个字典项的键和值。这意味着您可以使用除字符串以外的键。
迭代器方法
也许在 C# 2.0 中出现的更独特的特性之一是迭代器方法。它们是一种让编译器自动生成对序列的自定义迭代的方法。这个描述有点抽象,不可否认,因此最容易解释的方法是通过一些代码,如下所示:
private static IEnumerable<string> GetStates()
{yield return "Orlando";yield return "New York";yield return "Atlanta";yield return "Los Angeles";
}
在以前的方法中,您会看到一个返回IEnumerable<string>
的方法。然而,在方法体中,只有四行连续的代码使用了yield
关键字。这告诉编译器生成一个自定义的枚举器,将方法分解为每个yield
之间的单个部分,以便在调用者枚举返回的值时执行。这在以下代码中显示:
foreach (string state in GetStates())
{Console.WriteLine(state);
}
// outputs Orlando, New York, Atlanta, and Los Angeles
有许多不同的方法来处理和使用迭代器,但这里的重点是 C#编译器在此版本中变得更加智能。它能够接受您的代码并扩展它。这使您能够以更高的抽象级别编写代码,这是 C#演变中的一个持续主题。
C# 3.0
如果您认为 C# 2.0 是一个重大更新,那么 3.0 版本的发布就更大了!在一个单独的章节(更不用说章节的一部分)中很难充分展现它。因此,我们将重点关注主要特性,特别是它与 C#的演变相关的部分。
不过,首先我们应该谈谈 C#、CLR 和.NET Framework 之间的区别。直到现在,它们大多数版本都是相同的(即 C# 2.0、CLR 2.0 和.NET Framework 2.0),然而,他们发布了一个没有语言或 CLR 更改的.NET Framework(3.0)的更新。然后在.NET 3.5 中,他们发布了 C# 3.0。以下图表解释了这些差异:
令人困惑,我知道。尽管 C#语言和.NET Framework 都进行了升级,但 CLR 保持不变。尤其是考虑到所有新特性,这很难相信,但这表明了 CLR 的开发人员的前瞻性思维,以及 C#语言/编译器的良好工程化和可扩展性,他们能够在没有新运行时支持的情况下添加新特性。
语法更新
像往常一样,我们将开始审查此版本语言的语法更改。首先是属性,您会记得它们已经是对旧式的 getter 和 setter 方法的改进。在 C# 3.0 中,编译器可以自动生成简单 getter 和 setter 的后备字段,如下所示:
public string Name { get; set; }
这个特性单独就可以减少许多具有许多属性的类的代码行数。另一个引入的不错的特性是对象初始化器。看下面这个简单的类:
public class Person
{public string Name { get; set; }public int Age { get; set; }
}
如果您想创建一个实例并对其进行初始化,通常需要编写以下代码:
Person person = new Person();
person.Name = "Layla";
person.Age = 11;
但使用对象初始化器,您可以在对象实例化的同时进行如下操作:
Person person = new Person { Name = "Layla", Age = 11 };
编译器实际上会生成几乎与以前相同的代码,因此没有语义上的区别。但是你可以以更简洁和易读的方式编写你的代码。集合也得到了类似的处理,现在你可以使用以下语法初始化数组:
int[] numbers = { 1, 2, 3, 4, 5 };
而且,以前初始化时非常冗长的字典现在可以非常容易地创建如下:
Dictionary<string, int> states = new Dictionary<string,int>
{{ "NY", 1 },{ "FL", 2 },{ "NJ", 3 }
};
这些改进使得编写代码的过程更加简单和快速。但是,C#语言设计者似乎并不满足于此。每次你实例化一个新变量时,都需要写出整个类型名称,当你开始使用复杂的泛型类型时,这可能会给你的程序增加很多额外的字符。不用担心!在 C# 3.0 中甚至不需要这样做!看看下面的代码:
var num = 8;
var name = "Ashton";
var map = new Dictionary<string, int>();
只要右侧的赋值清楚地指明了类型,编译器就可以负责确定左侧的类型。敏锐的读者无疑会认出var
关键字来自 JavaScript。虽然看起来相似,但实际上并不一样。C#仍然是静态类型的,这意味着每个变量必须在编译时知道。以下代码将无法编译:
var num = 8;
num = "Tabbitha";
因此,实际上这只是一个快捷方式,帮助你输入更少的字符,编译器非常擅长推断这些东西。事实上,它可以推断的不仅仅是这些。如果有足够的上下文,它还可以推断泛型类型参数。例如,考虑以下简单的泛型方法:
public string ConvertToString<T>(T value)
{return value.ToString();
}
当你调用它时,而不是在调用中声明类型,编译器可以查看被传递的类的类型,并简单地假定这是应该用于类型参数的类型,如下所示:
string s = ConvertToString(234);
在这一点上,我想象 C#语言团队中的某个人说:“当我们让现有的语法变成可选时,为什么不完全摒弃对类定义的需求呢!”似乎他们确实这样做了。如果你需要一个数据类型来保存一些字段,你可以内联声明如下:
var me = new { Name = "Joel", Age = 31 };
编译器将自动创建一个与你刚刚创建的类型匹配的类。这个特性有一些限制:你必须使用var
关键字,并且不能从方法中返回匿名类型。当你编写算法并且需要一个快速但复杂的数据类型时非常有用。
所有这些小的语法改变都使得 C#语言写起来更加愉快。它们也为我们接下来要讨论的下一个重要特性铺平了道路。
LINQ
语言集成查询(LINQ)是 C# 3.0 的旗舰特性。它承认了现代程序的很大一部分围绕着以某种方式查询数据。LINQ 是一组多样化的特性,为语言提供了对从多种来源查询数据的一流支持。它通过在查询概念周围提供强大的抽象,然后添加语言支持来实现这一点。
C#语言团队从这样一个前提开始,即 SQL 已经是一个很好的用于处理基于集合的数据的语法。但不幸的是,它并不是语言的一部分;它需要不同的运行时,比如 SQL Server,并且只能在那个上下文中工作。LINQ 不需要这样的上下文切换,因此你可以简单地获取到你的数据源的引用,然后进行查询。
在概念上,你可以对集合进行以下高级别的操作:
-
过滤:这是根据某些条件从集合中排除项目的操作
-
聚合:这涉及到常见的聚合操作,比如分组和求和
-
投影:这是从集合中提取或转换项目
以下是一个简单的 LINQ 查询的样子:
int[] numbers = { 1, 2, 3, 4, 5, 6 };IEnumerable<int> query = from num in numberswhere num > 3select num;foreach (var num in query)
{Console.WriteLine(num);
}
// outputs 4, 5, and 6
它看起来像 SQL,有点像。多年来一直有很多关于为什么语法不像 SQL 中的 select 语句一样开始的问题,但原因归结为工具。当您开始输入时,他们希望您能够在输入查询的每个部分时获得智能感知。通过从'from'开始,您基本上告诉编译器在查询的其余部分中将使用什么类型,这意味着它可以在编译时提供类型支持。
LINQ 的一个有趣之处在于它适用于任何IEnumerable
。想一想,你的程序中的每个集合现在都很容易搜索。而且不仅如此,您还可以聚合和整理输出。例如,假设您想要按州获取每个州的城市数量,如下所示:
var cities = new[]
{new { City="Orlando", State="FL" },new { City="Miami", State="FL" },new { City="New York", State="NY" },new { City="Allendale", State="NJ" }
};var query = from city in citiesgroup city by city.State into stateselect new { Name = state.Key, Cities = state };foreach (var state in query)
{Console.WriteLine("{0} has {1} cities in this collection", state.Name, state.Cities.Count());
}
此查询使用 group by 子句按公共键(在本例中为州)对值进行分组。最终输出还是一个新的匿名类型,其中包含两个属性,名称和该州的城市集合。运行此程序将为佛罗里达州输出FL has 2 cities in this collection。
到目前为止,在这些示例中,我们一直在使用所谓的查询语法。这很好,因为对于了解 SQL 的人来说非常熟悉。然而,就像 SQL 一样,更复杂的查询有时可能会变得相当冗长和复杂。有另一种编写 LINQ 查询的方法,对于一些人来说可能更容易阅读,甚至可能更灵活,称为LINQ 方法语法,它建立在语言的另一个新功能之上。
扩展方法
通常,扩展类型功能的唯一方法是从类继承并将功能添加到子类型中。所有用户都必须使用新类型才能获得该新类型的好处。然而,这并不总是一个选择,例如,如果您正在使用具有值类型的第三方库(因为您无法从值类型继承)。假设我们在第三方库中有以下struct
,我们无法访问修改源代码:
public struct Point
{public float X;public float Y;
}
使用扩展方法,您可以按以下方式向此类型添加新方法:
public static class PointExtensions
{public static void Add(this Point value, Point other){value.X += other.X;value.Y += other.Y;}
}
扩展方法必须放在公共静态类中。方法本身将是静态的,并且将在第一个参数上使用this
关键字来表示要附加到的类型。使用前面的方法看起来就像该方法一直是类型的一部分一样:
var point = new Point { X = 28.5381f, Y = 81.3794f };
var other = new Point { X = -2.6809f, Y = -1.1011f };point.Add(other);
Console.WriteLine("{0}, {1}", point.X, point.Y);
// outputs "25.8572, 80.2783"
您可以将扩展方法添加到任何类型,无论是值类型还是引用类型。接口和密封类也可以扩展。如果您查看 C# 3.0 中的所有更改,您会注意到您现在编写的代码更少,因为编译器正在为您在幕后生成越来越多的代码。结果是代码看起来类似于其他一些动态语言,如 JavaScript。
C# 4.0
随着语言的第四次迭代,微软试图通过将每个组件的版本号递增到 4.0 来简化之前几个版本所造成的版本混乱。
C# 4.0 将更多的动态功能引入语言,并继续努力使 C#成为一个非常强大但灵活的语言。添加的一些功能主要是为了使与本地平台代码的交互更加容易。例如,协变、逆变和可选参数等功能简化了与 Microsoft Word 交互的 Interop 程序集的调用过程。总的来说,这些并不是非常惊人的东西,至少对于普通的开发人员来说不是。
然而,通过添加一个新关键字dynamic
,C#更接近成为一种非常动态的语言;或者至少继承了许多动态语言的特性。还记得当引入泛型时,如果有一个裸类型参数(即没有类型约束),它被视为对象。编译器在运行时对类型有关的方法和属性没有额外的信息,因此您只能将其视为对象进行交互。
在 C# 4.0 中,现在您可以编写可以在运行时绑定到正确属性和方法的代码。以下是一个简单的例子:
dynamic o = GetAString() ;string s = o.Substring(2, 3);
提示
如果您正在从较早版本的框架迁移项目,请确保在使用动态编程时添加对Microsoft.CSharp.dll
的引用。如果没有这个引用,您将收到编译错误。
在这个假设的场景中,您有一个返回string
的方法。接收GetAString()
方法返回值的变量标记有dynamic
关键字。这意味着您在该对象上调用的每个属性和方法都将在运行时动态评估。这使得 C#可以轻松地与动态语言(如 IronPython 和 IronRuby)以及您自己的自定义动态类型进行交互。
这是否意味着 C#不再是静态类型的?不,恰恰相反;C#仍然是静态类型的,只是在这种情况下,您已经告诉编译器以不同的方式处理这段代码。它通过重写您的动态代码来使用动态语言运行时(DLR)来实现这一点,实际上编译出您的代码的表达式树,在运行时进行评估。
您可以通过继承内置类DynamicObject
轻松创建自己的动态对象,如下所示:
public class Bag : DynamicObject
{private Dictionary<string, object> members = new Dictionary<string, object>();public override IEnumerable<string> GetDynamicMemberNames(){return members.Keys;}public override bool TryGetMember(GetMemberBinder binder, out object result){return members.TryGetValue(binder.Name, out result);}public override bool TrySetMember(SetMemberBinder binder, object value){members[binder.Name] = value;return true;}
}
在这个简单的例子中,我们继承自DynamicObject
并重写了一些方法来获取和设置成员值。这些值在内部存储在字典中,这样当 DLR 请求时,您可以取出正确的值。使用这个类非常像 JavaScript 中的灵活对象。看下面的代码:
dynamic bag = new Bag();bag.Name = "Joel";
bag.Age = 31;
bag.CalcDoubleAge = new Func<int>(() => bag.Age * 2);Console.WriteLine(bag.CalcDoubleAge());
如果需要存储新值,只需设置属性。如果要定义新方法,可以使用委托作为成员的值。当然,您必须意识到这不会像拥有常规静态类型的类那样快,每个值必须在运行时查找,并且因为值在内部存储为对象,任何值类型都将被装箱。但有时这些缺点是完全可以接受的,特别是当它可以简化您的代码时。
总结
对我来说,观察 C#从第一个版本发展到今天是一段令人惊奇的旅程。每个后续版本都比上一个版本更强大,而且在整个过程中都有一个非常坚实的代码简化主题。编译器本身在为您生成代码方面变得越来越好,这样您就可以在程序中实现非常强大的功能,而不必承担冗长实现基础设施(泛型、迭代器、LINQ 和 DLR)的认知负担。
在本章中,我们看了一些 C#每个版本引入的主要特性。
-
C# 1.0:内存管理,基类库和语法特性,如属性和事件。
-
C# 2.0:泛型,迭代器方法,部分类,匿名方法和语法更新,如属性上的可见性修饰符和可空类型。
-
C# 3.0:语言集成查询(LINQ),扩展方法,自动属性,对象初始化程序,类型推断(
var
)和匿名类型。 -
C# 4.0:动态语言运行时(DLR),协变和逆变
现在,我们转向最新版本,C# 5.0。Iliquamet quae volor aut ium ea dolore doleseq uibusam, quiasped utem atet etur sus。
第三章:行动中的异步性
我们将探讨 C# 5.0 版本中的新功能。值得注意的是,其中大部分与语言中新增的内置异步功能相关,这些功能允许您轻松地充分利用运行软件的硬件。我们还将讨论任务并行库(TPL),它引入了用于异步编程的原语,C# 5.0 语言支持简单的异步性,TPL DataFlow,基于代理的异步编程的更高级抽象,以及利用新的异步性功能的框架改进,例如对 I/O API 的改进。
总的来说,.NET Framework 的最新版本是庞大的,本章介绍的概念将作为本书其余部分涵盖材料的参考。
异步性
当我们谈论 C# 5.0 时,主要谈论的是新的异步编程功能。异步性是什么意思?嗯,它可能意味着一些不同的事情,但在我们的上下文中,它只是同步的相反。当您将程序的执行分解为异步块时,您可以并行执行它们。
成功的陷阱:与峰会、高峰或穿越沙漠寻找胜利的旅程形成鲜明对比,我们希望我们的客户通过使用我们的平台和框架简单地获得胜利的实践。在我们让人陷入麻烦的程度上,我们失败了。-Rico Mariani
不幸的是,构建异步软件并不总是容易的,但是通过 C# 5.0,您将看到它可以有多容易。正如您在下图中所看到的,同时执行多个操作可以为您的程序带来各种积极的特性:
并行执行可以为程序的执行带来性能改进。最好的方法是通过一个例子来将其置于上下文中,这个例子在桌面软件的世界中经常遇到。
假设您正在开发一个应用程序,并且该软件应满足以下要求:
-
当用户单击按钮时,启动对 Web 服务的调用。
-
完成 Web 服务调用后,将结果存储到数据库中。
-
最后,绑定结果并将其显示给用户。
实现这个解决方案的天真方式存在许多问题。首先,许多开发人员以一种使用户界面在等待接收这些 Web 服务调用的结果时完全无响应的方式编写代码。然后,一旦结果最终到达,我们继续让用户等待,而我们将结果存储在数据库中,这是用户在这种情况下不关心的操作。
过去缓解这类问题的主要手段一直是编写多线程代码。这当然并不新鲜,因为多线程硬件已经存在多年,以及利用这些硬件的软件功能。大多数编程语言没有为这些硬件提供很好的抽象层,通常让(或要求)您直接针对硬件线程进行编程。
幸运的是,微软引入了一个新的库,以简化编写高度并发程序的任务,这将在下一节中解释。
任务并行库
任务并行库(TPL)是在.NET 4.0 中引入的(以及 C# 4.0)。我们在第二章C#的演变中没有涉及它,原因有几个。首先,这是一个庞大的主题,在如此狭小的空间内无法得到适当的审查。其次,它与 C# 5.0 中的新异步特性高度相关,以至于它们是新特性的字面基础。因此,在本节中,我们将介绍 TPL 的基础知识,以及它如何以及为什么起作用的一些背景信息。
TPL 引入了一个新类型,即Task
类型,它将必须完成的任务的概念抽象成一个对象。乍一看,您可能会认为这种抽象已经存在于Thread
类中。虽然Task
和Thread
之间有一些相似之处,但实现有着相当不同的含义。
使用Thread
类,您可以直接针对操作系统支持的最低级别的并行性进行编程,如下面的代码所示:
Thread thread = new Thread(new ThreadStart(() =>
{
Thread.Sleep(1000);
Console.WriteLine("Hello, from the Thread");}));
thread.Start();Console.WriteLine("Hello, from the main thread");
thread.Join();
在前面的示例中,我们创建了一个新的Thread
类,当启动时,它将休眠一秒钟,然后输出文本Hello, from the Thread。在调用thread.Start()
之后,主线程上的代码立即继续执行,并输出Hello, from the main thread。一秒钟后,我们看到后台线程的文本被打印到屏幕上。
在某种意义上,使用Thread
类的这个示例展示了如何轻松地将执行分支到后台线程,同时允许主线程的执行继续进行,不受阻碍。然而,使用Thread
类作为您的“并发原语”的问题在于,该类本身就是实现的指示,也就是说,将创建一个操作系统线程。就抽象而言,它实际上并不是一个抽象;您的代码必须同时管理线程的生命周期,同时处理线程正在执行的任务。
如果您有多个任务要执行,生成多个线程可能是灾难性的,因为操作系统只能生成有限数量的线程。对于性能密集型应用程序,线程应被视为一种重量级资源,这意味着您应该避免使用过多的线程,并尽可能地保持它们活动。正如您可能想象的那样,.NET Framework 的设计者并没有简单地让您在没有任何帮助的情况下编写程序。框架的早期版本有一种机制来处理这个问题,即ThreadPool
,它允许您排队一个工作单元,并让线程池管理一组线程的生命周期。当一个线程变得可用时,您的工作项就会被执行。以下是使用线程池的一个简单示例:
int[] numbers = { 1, 2, 3, 4 };foreach (var number in numbers)
{
ThreadPool.QueueUserWorkItem(new WaitCallback(o =>{
Thread.Sleep(500);string tabs = new String('\t', (int)o);
Console.WriteLine("{0}processing #{1}", tabs, o);}), number);
}
这个示例模拟了多个任务,应该并行执行。我们从一个数字数组开始,对于每个数字,我们都想排队一个工作项,它将休眠半秒钟,然后写入控制台。这比自己尝试管理多个线程要好得多,因为线程池会在有更多工作时负责生成更多线程。当达到并发线程的配置限制时,它将保持工作项,直到有线程可用来处理它。这是您自己使用线程时会做的所有工作。
然而,线程池并不是没有问题的。首先,它没有提供在工作项完成时同步的方法。如果您想在作业完成时收到通知,您必须自己编写通知,无论是通过引发事件,还是使用线程同步原语,比如ManualResetEvent
。您还必须小心不要排队太多的工作项,否则可能会遇到线程池大小的系统限制。
使用 TPL,我们现在有一个称为Task
的并发原语。考虑以下代码:
Task task = Task.Factory.StartNew(() =>{
Thread.Sleep(1000);
Console.WriteLine("Hello, from the Task");});Console.WriteLine("Hello, from the main thread");task.Wait();
乍一看,代码看起来与使用Thread
的示例非常相似,但它们是非常不同的。一个很大的区别是,使用Task
时,您并没有承诺实现。TPL 在幕后使用一些非常有趣的算法来管理工作负载和系统资源,并且实际上允许您通过使用自定义调度程序和同步上下文来自定义这些算法。这使您能够以高度控制并行执行程序。
处理多个任务,就像我们在线程池中所做的那样,也更容易,因为每个任务都内置了同步功能。为了演示如何快速并行化任意数量的任务是多么简单,我们从与前一个线程池示例中相同的整数数组开始:
int[] numbers = { 1, 2, 3, 4 };
因为Task
可以被视为表示异步任务的原始类型,我们可以将其视为数据。这意味着我们可以使用诸如 Linq 之类的东西将数字数组投影到任务列表中,如下所示:
var tasks = numbers.Select(number =>
Task.Factory.StartNew(() =>{
Thread.Sleep(500);string tabs = new String('\t', number);
Console.WriteLine("{0}processing #{1}", tabs, number);}));
最后,如果我们想要等到所有任务都完成后再继续,我们可以通过调用以下方法轻松实现:
Task.WaitAll(tasks.ToArray());
一旦代码到达这个方法,它将等待数组中的每个任务完成后才继续。这种控制水平非常方便,特别是当您考虑到过去,您必须依赖许多不同的同步技术来实现与 TPL 代码中仅用几行代码实现的完全相同的结果时。
到目前为止,我们讨论的使用模式仍然存在一个很大的断裂,即生成任务的过程和子进程之间的断裂。将值传递到后台任务中非常容易,但当您想要检索一个值然后对其进行操作时,情况就会变得棘手。考虑以下要求:
-
进行网络调用以检索一些数据。
-
查询数据库以获取一些配置数据。
-
处理网络数据的结果,以及配置数据。
以下图表显示了逻辑:
网络调用和对数据库的查询可以并行进行。根据我们迄今为止对任务的了解,这不是问题。然而,对这些任务的结果进行操作将会稍微复杂一些,如果不是因为 TPL 恰好提供了对这种情况的支持。
还有一种特殊的Task
,在这种情况下特别有用,称为Task<T>
。这个任务的泛型版本期望运行的任务最终返回一个值。任务的客户端可以通过任务的.Result
属性访问该值。当您调用该属性时,如果任务已完成并且结果可用,它将立即返回。但是,如果任务尚未完成,它将阻塞当前线程的执行,直到完成。
使用这种承诺给您结果的任务,您可以编写程序,以便计划并启动所需的并行性,并以非常逻辑的方式处理响应。看看以下代码:
varwebTask = Task.Factory.StartNew(() =>{
WebClient client = new WebClient();return client.DownloadString("http://bing.com");});vardbTask = Task.Factory.StartNew(() =>{// do a lengthy database queryreturn new{
WriteToConsole=true};});if (dbTask.Result.WriteToConsole)
{
Console.WriteLine(webTask.Result);
}
else
{
ProcessWebResult(webTask.Result);
}
在前面的示例中,我们有两个任务,webTask
和dbTask
,它们将同时执行。webTask
只是从bing.com
下载 HTML。由于访问互联网的动态特性,访问互联网上的内容可能会非常不稳定,因此您永远不知道需要多长时间。对于dbTask
任务,我们模拟访问数据库以返回一些存储的设置。尽管在这个简单的示例中,我们只是返回一个静态的匿名类型,但数据库访问通常会访问网络上的不同服务器;同样,这是一个像从互联网上下载东西一样的 I/O 绑定任务。
与我们使用Task.WaitAll
等待它们都执行不同,我们可以简单地访问任务的.Result
属性。如果任务已经完成,结果将被返回,执行可以继续,如果没有,程序将简单地等待直到完成。
能够在不必手动处理任务同步的情况下编写代码是很棒的,因为程序员需要记住的概念越少,他/她就可以将更多的资源投入到程序中。
提示
如果你对返回值的任务的概念感到好奇,你可以搜索有关"Futures"和"Promises"的资源:
en.wikipedia.org/wiki/Promise_%28programming%29
在最简单的层面上,这是一个承诺在“未来”给你一个结果的构造,这正是Task<T>
所做的。
任务的可组合性
拥有一个适当的异步任务抽象使得协调多个异步活动变得更容易。一旦第一个任务被启动,TPL 允许你使用所谓的continuations将多个任务组合成一个统一的整体。看看下面的代码:
Task<string> task = Task.Factory.StartNew(() =>
{
WebClient client = new WebClient();return client.DownloadString("http://bing.com");
});task.ContinueWith(webTask =>{
Console.WriteLine(webTask.Result);});
每个任务对象都有.ContinueWith
方法,它让你将另一个任务链接到它上面。这个继续任务将在第一个任务完成后开始执行。与之前的示例不同,我们依赖.Result
方法来等待任务完成,从而可能阻塞主线程,而继续任务将以异步方式运行。这是一个更好的组合任务的方法,因为你可以编写不会阻塞 UI 线程的任务,这会导致非常响应迅速的应用程序。
任务的可组合性并不仅限于提供 continuations,TPL 还提供了考虑情况的能力,其中一个任务必须启动多个子任务。你可以控制这些子任务的完成方式如何影响父任务。在下面的示例中,我们将启动一个任务,该任务将依次启动多个子任务:
int[] numbers = { 1, 2, 3, 4, 5, 6 };varmainTask = Task.Factory.StartNew(() =>{// create a new child task
foreach (intnum in numbers){
int n = num;
Task.Factory.StartNew(() =>{
Thread.SpinWait(1000);
int multiplied = n * 2;
Console.WriteLine("Child Task #{0}, result {1}", n, multiplied);});}});
mainTask.Wait();
Console.WriteLine("done");
每个子任务都会写入控制台,这样你就可以看到子任务和父任务的行为。当你执行前面的程序时,会得到以下输出:
Child Task #1, result 2
Child Task #2, result 4
done
Child Task #3, result 6
Child Task #6, result 12
Child Task #5, result 10
Child Task #4, result 8
请注意,即使在写入done之前调用了外部任务的.Wait()
方法,子任务的执行在任务结束后仍会继续一段时间。这是因为,默认情况下,子任务是分离的,这意味着它们的执行与启动它的任务没有关联。
提示
在前面的示例代码中,一个无关但重要的部分是,你会注意到在使用任务之前,我们将循环变量赋值给一个中间变量。
int n = num;
Task.Factory.StartNew(() =>{
int multiplied = n * 2;
如果你记得我们在第二章中对 continuations 的讨论,C#的演变,你的直觉会告诉你应该能够直接在 lambda 表达式中使用num
。这实际上与闭包的工作方式有关,在尝试在循环中“传递”值时,这是一个常见的误解。因为闭包实际上创建了对值的引用,而不是复制值,使用循环值将导致在循环迭代时每次都会改变,你将得不到你期望的行为。
正如你所看到的,缓解这个问题的一个简单方法是在将其传递到 lambda 表达式之前将值设置为一个本地变量。这样,它将不会是一个在使用之前改变的整数的引用。
然而,你可以选择将子任务标记为Attached
,如下所示:
Task.Factory.StartNew(() =>DoSomething(),
TaskCreationOptions.AttachedToParent);
TaskCreationOptions
枚举有许多不同的选项。特别是在这种情况下,将任务附加到其父任务的能力意味着父任务将在所有子任务完成之前不会完成。
TaskCreationOptions
中的其他选项让你向任务调度程序提供提示和指令。从文档中,以下是所有这些选项的描述:
-
None
: 这指定应该使用默认行为。 -
PreferFairness
: 这是对TaskScheduler
类的一个提示,尽可能公平地安排任务,这意味着更早安排的任务更有可能更早运行,而更晚安排的任务更有可能更晚运行。 -
LongRunning
: 这指定任务将是一个长时间运行的、粗粒度的操作。它向TaskScheduler
类提供了一个提示,表明可能需要过度订阅。 -
AttachedToParent
: 这指定任务附加到任务层次结构中的父任务。 -
DenyChildAttach
: 这指定如果尝试将子任务附加到创建的任务,则会抛出InvalidOperationException
类型的异常。 -
HideScheduler
: 这样可以防止在创建的任务中看到环境调度程序作为当前调度程序。这意味着在创建的任务中执行的StartNew
或ContinueWith
等操作将把Default
作为当前调度程序。
这些选项以及 TPL 的工作方式最好的部分是,它们大多数只是提示。因此,你可以建议你启动的任务是长时间运行的,或者你更希望较早安排的任务先运行,但这并不保证一定会这样。框架将负责以最有效的方式完成任务,因此如果你更喜欢公平性,但一个任务花费太长时间,它将开始执行其他任务,以确保它继续最优地使用可用资源。
任务的错误处理
在任务世界中的错误处理需要特别考虑。总之,当抛出异常时,CLR 将展开堆栈帧,寻找一个适当的 try/catch 处理程序来处理错误。如果异常达到堆栈的顶部,应用程序就会崩溃。
然而,在异步程序中,并不是有一个单一的线性执行堆栈。所以当你的代码启动一个任务时,不会立即清楚在任务内部抛出的异常会发生什么。例如,看看下面的代码:
Task t = Task.Factory.StartNew(() =>
{throw new Exception("fail");
});
这个异常不会作为未处理的异常冒泡上来,如果你在代码中不处理它,你的应用程序不会崩溃。实际上它已经被处理了,但是由任务机制处理。然而,如果你调用.Wait()
方法,异常将在那一点上冒泡到调用线程。这在下面的例子中显示:
try
{
t.Wait();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
当你执行时,它将打印出一条不太有用的消息发生了一个或多个错误,而不是实际包含在异常中的失败消息。这是因为在任务中发生的未处理异常将被包装在一个AggregateException
异常中,当处理任务异常时,你可以专门处理它。看看下面的代码:
catch (AggregateException ex)
{
foreach (var inner in ex.InnerExceptions){
Console.WriteLine(inner.Message);}
}
如果你仔细想想,这是有道理的,因为任务与继续和子任务是可组合的,这是表示此任务引发的所有错误的好方法。如果你更愿意在更细粒度的级别上处理异常,你也可以传递一个特殊的TaskContinuationOptions
参数,如下所示:
Task.Factory.StartNew(() =>{throw new Exception("Fail");}).ContinueWith(t =>{// log the exception
Console.WriteLine(t.Exception.ToString());}, TaskContinuationOptions.OnlyOnFaulted);
这个继续任务只有在附加的任务出现故障时才会运行(例如,如果有未处理的异常)。错误处理当然是开发人员编写代码时经常忽视的事情,因此熟悉在异步世界中处理异常的各种方法是很重要的。
async 和 await
现在异步的基础已经建立,我们准备最终开始讨论 C# 5.0。我们将讨论的第一个功能可能是对我们开发应用程序方式影响最大的功能——使用引入async
和await
关键字的新语言特性进行异步编程。
在我们深入讨论之前,让我们快速回顾一下版本情况。尽管当 CLR、C#和.NET Framework 都增加到 4.0 时,似乎情况会有所改善,但它已经退化为令人困惑的领域。以下图表显示了版本之间的比较:
C# 5.0 随附于.NET 4.5,其中还包括一个新版本的公共语言运行时。因此,当您开发 C# 5.0 应用程序时,通常会针对 Framework 的 4.5 版本。
提示
如果您绝对需要针对 Framework 的 4.0 版本,您可以下载Visual Studio 2012 的 Async Targeting Pack,这将使您能够将 C# 5.0 应用程序编译和部署到.NET 4.0。但是,请记住,这仅适用于 C# 5.0 语言功能,如 async/await。.NET 4.5 中的其他 Framework 更新将不可用。
您可能会问自己,考虑到任务并行库是在之前的 Framework 版本中引入的,那么到底有什么新东西呢?不同之处在于,语言本身现在积极参与程序的异步操作。让我们从一个简单的示例开始,展示这个功能的运作方式:
public async void DoSomethingAsync()
{
Console.WriteLine("Async: method starting");awaitTask.Delay(1000);Console.WriteLine("Async: method completed");
}
从程序员的逻辑角度来看,这是一个非常简单的方法。它写入控制台以表明Async: method starting,然后等待一秒,最后写入Async: method completed。请特别注意该方法中的两个关键字:async
和await
。
在程序的另一部分中,我们在调用该方法之前和之后将其写入控制台,如下所示:
Console.WriteLine("Parent: Starting async method");DoSomethingAsync();Console.WriteLine("Parent: Finished calling async method");
除了两个新关键字之外,这段代码看起来完全是顺序的。如果不知道async
的工作原理,您可能会假设写入控制台的消息会按照这种模式出现:parent
,async
,async
,parent
。尽管这是语句编写的顺序,但这不是它们执行的顺序。您可以看到以下示例:
Parent: Starting async method
Child: Async method starting
Parent: Finished calling async method
Child: Async method completed
语句的顺序是错乱的,因为该方法或其中的一部分是异步执行的。这里发生的情况是,编译器正在分析该方法,并以一种方式将其分解,以便在await
关键字之后发生的一切都是异步执行的。调用线程的执行立即返回并继续,await
调用之后的一切都是在继续执行。
大多数开发人员第一次遇到这种情况时的第一反应是:“什么!?”
虽然一开始可能很难理解,但一旦了解编译器如何处理这一点,您就可以开始建立一个有助于您的心智模型。如果我们使用 TPL 编写相同的异步方法,它看起来会像以下内容:
public void DoSomethingAsyncWithTasks()
{
Console.WriteLine("Child: Async method starting");var context = TaskScheduler.FromCurrentSynchronizationContext();Task.Delay(1000).ContinueWith(t =>{
Console.WriteLine("Child: Async method completed");}, context);
}
在这个方法中,我们突出显示了原始方法中的代码行。调用返回Task
的Task.Delay
方法用于启动任务(在这个示例中,只是等待一秒)。然后,下一行代码被放入一个继续执行的部分,这部分将在调用任务完成后立即执行。
这段重写的代码的另一个有趣且可能更重要的特性是,继续执行将在与异步任务之前的代码相同的同步上下文中运行。因此,它实际上将在await
关键字之前的代码所在的同一线程上运行。当处理 UI 代码时,这一点变得特别重要,因为您不能在主 UI 线程之外的线程上设置属性值或调用 UI 控件方法,否则会引发异常。
提示
要清楚的是,这并不完全是编译器生成的代码。在幕后,它将创建一个代表重写代码执行每个阶段的状态机。当您开始使用循环调用和等待异步方法时,这可能会变得非常复杂。
尽管如此,从逻辑上讲,前面的例子与编译器在这种情况下生成的代码是相同的。因此,与其花费大量时间试图解释编译器正在做什么,不如创建一个您可以使用的行为的逻辑心智模型。
到目前为止,您会注意到我们给出的每个例子都是在一个方法中完成异步工作,然后由另一个方法调用并等待其值。方法或函数是异步拼图的核心部分。就像您可以使用任务一样,您可以从异步方法中返回值。
在这个例子中,我们有一个异步方法,返回类型设置为Task<string>
:
public asyncTask<string>GetStringAsynchronously()
{await Task.Delay(1000);return "This string was delayed";
}
因为该方法被标记为async
关键字,您可以返回一个实际的字符串,而不必将其包装在任务中。当调用者等待结果时,它将是一个字符串,因此您可以将其视为简单的返回类型,如下所示:
public async void CallAsynchronousStringMethod ()
{
string value = await GetStringAsynchronously();Console.WriteLine(value);
}
再次看到,您可以处理异步操作,而不必担心执行它们的基础设施。正如我们之前展示的,当我们重写以前的方法以使用任务时,编译器如何处理返回值就变得明显了。看看以下代码:
var context = TaskScheduler.FromCurrentSynchronizationContext();GetStringAsynchronously().ContinueWith(task =>{
string value = task.Result;
Console.WriteLine(value);}, context);
组合异步调用
另一个有助于思考编译器如何使用任务和延续重写异步方法的方式的原因是,它使得 TPL 的使用方式始终保持在前沿。这意味着您可以将新关键字与任务的所有现有特性结合使用,以使您的应用程序并行化以满足您的需求。这一点很重要,因为如果您每次都使用await
关键字,您可能会错过并行性的机会。
在下面的例子中,我们两次调用一个异步方法。该方法返回Task<string>
,所以我们不是使用await
来调用,因为这样会(逻辑上)在第一个任务完成之前阻止第二个任务的执行,而是将返回值放入变量中,并使用Task.WhenAll
方法等待它们都完成,如下所示:
private async void Sample_04()
{Task<string>firstTask = GetAsyncString("first task");Task<string>secondTask = GetAsyncString("second task");await Task.WhenAll(firstTask, secondTask);Console.WriteLine("done with both tasks");
}public async Task<string>GetAsyncString(string value)
{
Console.WriteLine("Starting task for '{0}'", value);await Task.Delay(1000);return value;
}
这允许两个任务同时执行,并且仍然可以使用await
关键字来组合您的程序。
使用异步方法处理错误
使用异步方法处理错误非常简单。因为 C#编译器已经完全重写了方法以等待手头任务的完成,所以它让您可以使用与自 C# 1.0 以来一直在使用的基于异常的错误处理方法相同的方法。
以下是一个从Task
中抛出异常的异步方法的示例:
private async Task ThisWillThrowAnException()
{
Console.WriteLine("About to start an async task that throws an exception");await Task.Factory.StartNew(() =>{throw new Exception("fail");});
}
正如我们在使用任务处理错误部分中讨论的那样,如果您将此方法的返回值视为常规任务进行交互,那么异常不会直接在调用代码的相同上下文中引发。它要么在调用任务的.Wait
方法时引发,要么您可以在特殊的延续中处理它。但是如果您使用await
与该方法,那么您可以将代码包装在try
/catch
块中,如下所示:
try
{await ThisWillThrowAnException();
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
当从async
方法中引发未处理的异常时,此代码的执行将无缝地转移到catch
块。这意味着您实际上不必考虑如何处理从异步上下文中抛出的异常,您只需像处理常规同步代码一样catch
它们。
异步的影响
到目前为止,我们只讨论了在.Net 4.0 和 C# 5.0 中发布的异步编程功能的机制。然而,使并行软件应用易于编程的重要性值得再次强调。有几个因素突出了这些新发展的重要性。
第一个是摩尔定律,它著名地指出 CPU 中的晶体管数量可能每年翻一番。虽然这个定律多年来一直成立,但在过去的十年里,成本和热量方面已经达到了一些实际限制,使得在单个 CPU 上商业上可能的事情。因此,制造商开始制造带有多个 CPU 的计算机。这些新设计仍然能够跟上摩尔定律的预测,但程序必须专门编写以利用硬件。
async
影响的另一个重要因素是分布式计算的兴起。如今,将程序构建为在多台计算机上运行的个体程序变得越来越流行。这些点对点或客户端-服务器架构很少受 CPU 限制,因为在网络(或互联网)上的一台计算机与另一台计算机之间通信的延迟。面对这种架构,非常重要的是能够并行化计算,以便用户界面不必等待网络调用完成。
未来,利用并行性的机会的软件应用程序将是性能和可用性上更优越的应用程序。许多最大的互联网公司,如谷歌,已经开始利用大规模并行化,以解决在单台计算机上根本无法计算的非常大的问题。async
关键字使得你几乎不必考虑如何以及何时利用它(几乎)。
.NET 4.5 框架的改进
除了所有 C# 5.0 语言的改进之外,.NET Framework 4.5 还带来了一些改进。当然,这些改进对所有.NET 语言(即 VB.NET)都是可用的,但随着 C# 5.0 一起提供,它们值得一提。
TPL DataFlow
框架中一个有趣的新成员是TPL DataFlow库,旨在改进应用程序的架构。该库的 NuGet 描述如下:
TPL Dataflow 是一个用于构建并发应用程序的.NET 框架库。它通过进程内消息传递、数据流和流水线原语来促进 actor/agent 导向的设计。TDF 建立在任务并行库(TPL)提供的 API 和调度基础设施之上,并与 C#提供的异步支持集成。
可以通过 NuGet 搜索 TPL DataFlow 或访问 NuGet 网站nuget.org/packages/Microsoft.Tpl.Dataflow
来安装它。
如描述所述,数据流建立在任务并行库的基础上,这是一个趋势,我相信你已经在这个版本中开始看到了,其中 TPL,以及 C# 5 的async
/await
,帮助你并行化你的程序;它这样做,而不会对如何构造应用程序进行任何规定。相反,TPL DataFlow 库提供了用于在应用程序的不同部分之间进行通信的各种构建块。
TPL DataFlow 引入了两个接口,就像IEnumerable
一样,它们既简单又深刻地影响着。以下图表显示了这些接口:
我们从ITargetBlock<T>
开始,这是一段将处理多个发布消息的代码块。您主要通过调用.Post
方法向块发布消息来与其交互。方程的另一边是ISourceBlock<T>
,它充当数据源。这些接口以及 TPL DataFlow 库提供的具体实现一起,帮助您创建结构化为离散生产者和消费者的应用程序。
ActionBlock
ActionBlock<T>
块是ITargetBlock<T>
的最简单实现。它在构造函数中接受一个委托,该委托定义了在向其发布消息时将采取的操作。以下是如何定义一个简单的块,它接受一个字符串并将其写入控制台:
var block = new ActionBlock<string>(s =>
{
Console.WriteLine(s);
});
一旦您定义了块,就可以开始向其发布消息。操作块是异步执行的,这不是必需的,只是为了展示此实现如何处理消息的发布。请查看以下代码:
for (inti = 0; i< 30; i++)
{
block.Post("Processing #" + i.ToString());
}
在这里,我们看到一个非常简单的循环,它迭代 30 次并向目标操作发布一个字符串。一旦您定义了目标块,您可以使用 TPL DataFlow 库提供的多种不同的源块实现来创建非常有趣的路由场景。
TransformBlock
您将发现一种非常有用的ISourceBlock<T>
是TransformBlock<T, K>
块。顾名思义,转换块允许您接收一种数据,并可能将其转换为另一种数据。在以下示例中,我们将创建两个块;TransformBlock
将接收一个整数并将其转换为字符串。然后生成的输出将被路由到接受字符串进行处理的ActionBlock
。请查看以下示例代码:
TransformBlock<int, string> transform = new TransformBlock<int, string>(i =>{// take the integer input, and convert to a stringreturn string.Format("squared = {0}", i * i);});ActionBlock<string> target = new ActionBlock<string>(value =>{// now use the string generated by the transform block
Console.WriteLine(value);});transform.LinkTo(target);
转换块的输入和输出类型以通用参数的形式指定。您可以使用.LinkTo
方法将操作块添加到数据流链的末尾,该方法将源块的所有输出定向到目标块。以下是代码解释:
for (inti = 0; i< 30; i++) transform.Post(i);
当您向转换块发布整数时,您会看到消息首先流经转换块,然后路由到操作块。
BatchBlock
以下图表显示了另一种源块,它可以帮助您处理信息流,即批处理块:
通常,如果每条消息的处理都有一定的成本,例如对数据库的信息查找,这种批处理处理可能很有用。在这种情况下,您可以批量处理查询值,并一次性对多条消息进行单个数据库查找,并随着批处理大小的增加摊销查找成本。请查看以下示例:
var batch = new BatchBlock<string>(5);var processor = new ActionBlock<string[]>(values =>{
Console.WriteLine("Processing {0} items:", values.Length);
foreach (var item in values){
Console.WriteLine("\titem: {0}", item);}});batch.LinkTo(processor);for (inti = 0; i< 32; i++)
{
batch.Post(i.ToString());
}
您可以将批处理块视为特定类型的转换块,该转换块在前端接收单个消息实例,等待到达指定数量的这些消息,然后将该组作为数组传递到目标块。当您的系统必须为接收到的每条消息查找参考数据等设置时,这可能很有用。如果您可以一次处理多条消息,则初始化成本可以随时间摊销。您处理的消息越多,成本越低。以下示例显示了如何实现这一点:
// manually trigger
batch.TriggerBatch();
如果您知道尚未达到阈值消息数量,还可以手动触发批处理。通过这种方式,如果您的系统必须在一定时间内处理消息,您可以处理较小大小的批处理。
BroadcastBlock
以下图表显示的广播块是一个有趣的源块:
它的工作方式是,您可以将多个目标块链接到广播器。当消息发布到广播器时,它将被传递到每个目标。这个块的一个明显的应用是编写一个必须同时为多个客户端提供服务的服务器应用程序。然后,每个客户端由一个目标块表示,该目标块链接到广播器。每当您需要通知每个客户端时,您只需将消息发布到广播器。看下面的例子:
var broadcast = new BroadcastBlock<string>(value =>
{return value;
});broadcast.LinkTo(new ActionBlock<string>(value =>Console.WriteLine("receiver #1: {0}", value)));
broadcast.LinkTo(new ActionBlock<string>(value =>Console.WriteLine("receiver #2: {0}", value)));
broadcast.LinkTo(new ActionBlock<string>(value =>Console.WriteLine("receiver #3: {0}", value)));
broadcast.LinkTo(new ActionBlock<string>(value =>Console.WriteLine("receiver #4: {0}", value)));broadcast.Post("value posted");
在这个例子中,我们链接了四个单独的动作块。当我们发布value posted时,我们将在控制台输出中看到四个单独的接收验证。在某种程度上,这与 C#语言中现有的事件系统非常相似。
异步 I/O
为了充分利用新的async
/await
功能,.NET Framework 的一些核心功能已经发展。即,包括流、网络和文件操作在内的 I/O 功能。这是巨大的,因为如前所述,I/O 绑定操作正在主导现代应用程序的执行时间。因此,API 中对这些操作的任何改进都可以视为一个好迹象。
在最低级别上是对Stream
API 的添加。自.NET 1.0 以来,这一直是我最喜欢的抽象之一,因为它可以以多种不同的方式使用。读写文件、网络套接字或数据库,都使用流 API 来表示未知大小的一系列字节。当然,这里的限制因素是,根据您使用的流实现,性能和延迟可能会有很大的变化。因此,您不应该像编写写入内存流的代码那样编写网络流的代码,因为性能会有很大的不同。
然而,通过async
,这种情况发生了变化,因为Stream
类已经收到了该类所有方法的新的可等待版本。在下面的例子中,我们编写了一个异步方法,该方法接受一组数字,并将它们写入字符串,如下所示:
private static async void WriteNumbersToStream(Stream stream, IEnumerable<int> numbers)
{
StreamWriter writer = new StreamWriter(stream);foreach (intnum in numbers){await writer.WriteLineAsync(num.ToString()); }
}
尽管在以前可能以类似的方式编写这样的代码,但是像.WriteLineAsync
这样的方法的添加使您可以编写简单的代码,而无需担心流阻塞调用线程的执行。
由于流 API 的基础改进,其他领域,如读写文件,也有所改进。看下面的代码:
private static async void WriteContentstoConsoleAsync(string filename)
{
FileStream file = File.OpenRead(filename);StreamReader reader = new StreamReader(file);while (!reader.EndOfStream){string line = await reader.ReadLineAsync();
Console.WriteLine(line);}
}
老实说,多年来我看到了这种方法的变体,当然,是以非异步方式编写的。没有异步性,如果您尝试读取一个非常大的文件,这种方法绝对会崩溃。一个很好的例子是随每个 Windows 版本一起提供的记事本应用程序。如果您尝试打开一个非常大的文件,准备等待,因为界面将在文件从磁盘流出时被冻结。
但是在这里的异步版本中,接口不会被拖慢,无论文件的大小如何。这就是async
的伟大特性,它接受开发人员可能编写的代码,并使得常见的性能问题,如缓冲,不会对应用程序的性能产生太大影响。这是“成功之坑”的完美例子。
调用者属性
唯一与异步无关的改进之一是调用者属性。在 Java 中,有一个非常常见的约定,即您必须指定一个名为TAG
的类级静态变量,该变量将包含该类的一些有用的字符串标识符,如下所示:
private static final String TAG = "NameOfThisClass";
每当您想要将信息写入系统日志(logcat
)时,您只需使用TAG
变量,以便您可以轻松地在日志输出中识别信息,如下所示:
Log.e(TAG, "some log message");
所以任何时候你需要记录一些东西,调用者负责自行报告关于何时何地以及为什么记录这些日志的元数据。当然,对于日志记录这样的元数据的需求跨越了语言,因此 C#语言设计者最终添加了一个很好的小功能来帮助你。
C#一直拥有非常强大的反射系统,因此一直可以查看日志方法中的堆栈信息。这简化了日志调用,因为调用者不必做任何特殊处理。然而,当应用程序在发布模式下编译时,这种方法容易返回意外结果,因为编译器进行了优化。此外,一些相关的类已经在可移植库中被排除。
你现在可以在 C# 5 中的日志方法中添加一些经过编译器优化的参数。当你调用该方法时,编译器将插入适当的元数据,以便在运行时返回正确的值,如下所示:
public void Log([CallerMemberName]string name = null)
{
Console.WriteLine("The caller is named {0}", name);
}
以下是另外两个你可以使用的属性:
-
[CallerFilePath]
:这给出了调用者所在文件的路径 -
[CallerLineNumber]
:这是方法被调用的确切行号
总结
在本章中,我们探索了 C# 5 中的异步编程世界,并学到了以下内容:
-
软件中的异步性,以及由此延伸的并发性,是实现最佳性能的关键。
-
任务并行库是所有新的异步编程特性的基本构建块。深入理解 TPL 将非常有用。
-
C# 5.0 语言支持简单的异步,这很容易成为该语言最重要的升级之一。它建立在 TPL 的基础上,使构建响应迅速且性能良好的应用程序变得简单。
-
TPL DataFlow 为基于代理的异步编程提供了更高级别的抽象,可以帮助你创建易于维护的程序。
-
利用新的异步特性的框架改进,比如改进的 I/O API,可以帮助你充分利用分布式计算的世界。
展望未来,我相信这些特性将使使用 C#编写快速、无 bug 且易于维护的程序变得非常容易。这里涵盖的概念可以作为本书其余材料的参考。
第四章:创建 Windows Store 应用程序
在本书的前半部分,我们看了如何设置开发环境以利用 C# 5.0,回顾了 C#的历史和演变,并审查了最新版本中可用的新功能。在本章(以及本书的其余部分),我们将看一些实际应用,您可以在这些功能中使用这些功能。
本章将指导您创建一个 Windows Store 应用程序。该应用程序将在新的 Windows Runtime 环境中运行,可以针对 x86 和 ARM 架构。在本章中,我们将创建一个 Windows Store 应用程序,连接到互联网上的基于 HTTP 的 Web 服务并解析 JSON,并在 XAML 页面中显示结果。
完成本章后,您将拥有一个完全可用的项目,可以将其用作您自己应用程序的基础。然后,您可以将此应用程序上传到 Windows Store,并有可能从销售中赚钱。
制作 Flickr 浏览器
我们要创建的项目是一个浏览图像的应用程序。作为来源,我们将使用流行的图片网站flickr.com
。
选择这个作为我们项目的几个原因。首先,Flickr 提供了一个广泛的 Web API 来浏览他们网站的各个部分,因此这是一种访问数据存储库的简单方式。其次,很多时候您会发现自己通过互联网访问外部数据,因此这是如何在本机应用程序中处理这种访问的一个很好的例子。最后,图片是很好的视觉享受,因此应用程序最终会成为您可以展示的东西。
启动项目
如果您使用的是 Visual Studio Express,您必须使用名为 VS Express for Windows 8 的版本。我们通过在以下截图中显示的新建项目对话框中创建一个新的 Windows Store 应用程序来开始这个过程:
选择空白应用程序(XAML)项目模板,该模板提供了创建应用程序所需的最低限度。当然,您应该鼓励使用其他模板创建项目,以了解如何创建一些常见的 UI 范例,例如网格应用程序。但是现在,空白应用程序模板保持简单。
连接到 Flickr
现在我们有了项目结构,我们可以通过首先连接到 Flickr API 来开始项目。一旦您在 Flickr 上创建了用户帐户,您就可以在www.flickr.com/services/api/
上访问 API 文档。
一定要浏览文档,了解可能的操作。一旦您准备好继续,获得访问其数据的一个关键因素将是提供 API 密钥,申请一个非常容易——只需访问www.flickr.com/services/apps/create/apply/
上的申请表格。
申请非商业密钥,然后提供有关您将要构建的应用程序的信息(名称、描述等)。
完成应用程序后,您将获得两个数据:API 密钥和 API 密钥。.NET Framework 的 Windows RT 版本包含许多差异。对于习惯于使用桌面版本框架的开发人员,其中一个差异是配置系统的缺失。因此,尽管 C#开发人员习惯于将静态配置值(例如 API 密钥)输入到app.config
文件中,但我们在这里无法这样做,因为这些 API 在 WinRT 应用程序中根本不可用。为了简化配置系统,我们可以创建一个静态类来包含常量并使其易于访问。
我们首先在DataModel
命名空间中添加一个新类。如果项目中还没有它,只需添加一个名为DataModel
的文件夹,然后添加一个包含以下内容的新类:
namespace ViewerForFlickr.DataModel
{public static class Constants{public static readonly string ApiKey = "<The API Key>";public static readonly string ApiSecret = "<The API Secret>";}
}
当然,在其中写<The API Key>
和<The API Secret>
的地方,您应该用分配给您自己帐户的密钥和秘密替换它。
接下来,我们需要一种实际访问互联网上的 API 的方法。由于 C# 5.0 中的新的async
特性,这非常简单。添加另一个名为WebHelper
的类到DataModel
文件夹中,如下所示:
internal static class WebHelper
{public static async Task<T> Get<T>(string url){HttpClient client = new HttpClient();Stream stream = await client.GetStreamAsync(url);Var serializer = new DataContractJsonSerializer(typeof(T));return (T)serializer.ReadObject(stream);}
}
尽管代码行数很少,但实际上这段代码中发生了很多事情。使用HttpClient
类,只需调用一个方法来异步下载数据。返回的数据将以JavaScript 对象表示法(JSON)格式返回。然后,我们使用DataContractJsonSerializer
将结果直接反序列化为我们使用方法的泛型参数定义的强类型类。这就是 C# 5.0 的伟大之处之一;在框架的先前版本中,这个方法有更多的代码行。
有了定义的WebHelper
类,我们可以开始从远程 API 中收集实际数据。Flickr 提供的一个有趣的 API 端点是有趣列表,它返回最近发布到其服务的照片列表。这很棒,因为您保证始终有一组精彩的图片可供显示。您可以通过阅读www.flickr.com/services/api/flickr.interestingness.getList.html
上的 API 文档来熟悉该方法。
当配置为使用 JSON 格式时,服务返回的数据如下所示:
{"photos": {"page": 1,"pages": 5,"perpage": 100,"total": "500","photo": [{"id": "7991307958","owner": "8310501@N07","secret": "921afedb45","server": "8295","farm": 9,"title": "Spreading one's wings [explored]","ispublic": 1,"isfriend": 0,"isfamily": 0}]},"stat": "ok"
}
作为 JSON 返回的对象包含分页信息,例如当前所在的页面,以及照片信息数组。由于我们将使用内置的DataContractJsonSerializer
类来解析 JSON 结果,因此需要创建所谓的数据契约。这些是与 JSON 字符串表示的对象结构匹配的类;序列化程序将从 JSON 字符串中获取数据并填充数据契约的属性,因此您可以以强类型方式访问它。
提示
在 C#中有许多其他解决方案可用于处理 JSON。可以说,其中一个更受欢迎的解决方案是 James Newton-King 的 Json.NET,您可以在json.net
找到。
这是一个非常灵活的库,可以在解析和创建 JSON 字符串时比其他库更快。许多开源项目都依赖于此库。我们之所以不在这里使用它,只是为了简单起见,因为DataContractJsonSerializer
随框架提供。
我们开始创建数据契约,通过查看 JSON 结构的最深层级,该层级表示有关单个照片的信息。数据契约只是一个类,其中每个字段在 JSON 对象中都有一个属性,并且已经用一些属性进行了装饰。在类定义的顶部,添加[DataContract]
属性,这只是告诉序列化程序可以使用这个类,然后为每个属性添加一个[DataMember(Name=
")]
属性,这有助于序列化程序知道哪些成员映射到哪些 JSON 属性。
将以下示例代码中的类与 JSON 字符串进行比较:
[DataContract]
public class ApiPhoto
{[DataMember(Name="id")]public string Id { get; set; }[DataMember(Name="owner")]public string Owner { get; set; }[DataMember(Name="secret")]public string Secret { get; set; }[DataMember(Name="server")]public string Server { get; set; }[DataMember(Name="farm")]public string Farm { get; set; }[DataMember(Name="title")]public string Title { get; set; }public string CreateUrl(){string formatString = "http://farm{0}.staticflickr.com/{1}/{2}_{3}_{4}.jpg";string size = "m";return string.Format(formatString,this.Farm,this.Server,this.Id,this.Secret,size);}
}
在这里传递给数据成员属性的Name
参数是因为属性的大小写与 JSON 对象中返回的不匹配。当然,您也可以将属性命名为与 JSON 对象完全相同,但那样它就不符合常规的.NET 命名约定了。
您应该注意的一件事是,照片对象本身没有指向图像的 URL。Flickr 为您提供了如何构建图像 URL 的指导。在前面的示例中,类中包含的.CreateUrl
方法将使用类的属性信息构建图像的 URL。您可以在www.flickr.com/services/api/misc.urls.html
获取有关构建 Flickr URL 的不同选项的更多信息。
接下来是对象链,我们有一个包含一些关于结果的元数据的对象,比如页面、页面数和每页的项目数。您可以使用这些信息允许用户浏览结果。该对象还包含一个ApiPhoto
对象的数组,如下所示:
[DataContract]
public class ApiPhotos
{[DataMember(Name="page")]public int Page { get; set; }[DataMember(Name="pages")]public int Pages { get; set; }[DataMember(Name="perpage")]public int PerPage { get; set; }[DataMember(Name="total")]public int Total { get; set; }[DataMember(Name="photo")]public ApiPhoto[] Photo { get; set; }
}
最后,我们创建一个对象来表示外层对象,它只有一个属性,如下所示:
[DataContract]
public class ApiResult
{[DataMember(Name="photos")]public ApiPhotos Photos { get; set; }
}
现在我们已经创建了所有的数据契约,我们准备把一切都放在一起。请记住,这段代码将通过互联网获取数据,这使得它成为使用async
/await
的绝佳选择。因此,当我们规划界面时,我们希望确保它是可等待的。在Models
文件夹中创建一个名为Flickr.cs
的新类。
public static class Flickr
{private static async Task<ApiPhotos> LoadInteresting(){string url = "http://api.flickr.com/services/rest/?method=flickr.interestingness.getList&api_key={0}&format=json&nojsoncallback=1";url = string.Format(url, Constants.ApiKey);ApiResult result = await WebHelper.Get<ApiResult>(url);return result.Photos;}
}
在这个课程中,我们创建了一个名为.LoadInteresting()
的方法,它使用我们在本章前面提供的 API 密钥构建了一个指向有趣的终点的 URL。接下来,它使用WebHelper
类进行 HTTP 调用,并传入ApiResult
类,因为它是表示 JSON 结果格式的对象。一旦网络调用返回一个值,它将被反序列化,然后我们返回照片信息。
创建 UI
现在我们已经有了一个易于访问的方法来获取数据,并准备开始创建用户界面。当您使用 C#为 Windows Store(以前称为Metro)创建应用程序时,您将使用 XAML,您将使用的一个非常常见的模式是Model-View-ViewModel(MVVM)。这种架构模型类似于Model-View-Controller(MVC),在这种模式中,两端分别有一个模型和视图,中间有一个组件来“粘合”这些部分。它与 MVC 的不同之处在于,“控制器”的角色由 XAML 提供的绑定系统承担,它负责在数据更改时更新视图。因此,您只需提供一个轻量级的包装器来使 ViewModel 中的某些绑定场景变得更容易,如下图所示:
在这个应用程序中,您的Model组件代表了问题域的核心逻辑。ApiPhotos
和.LoadInteresting
方法代表了这个相对简单程序中的模型。View块由我们将要创建的 XAML 代码表示。因此,我们需要ViewModel块来将Model块与View块连接起来。
在创建项目时,有几段代码是自动包含的。其中一个有用的代码可以在Common/StandardStyles.xaml
文件中找到。这个文件包含了许多有用的样式和模板,您可以在应用程序中使用。我们将使用其中一个模板来显示我们的图片。名为Standard250x250ItemTemplate
的模板定义如下:
<DataTemplate x:Key="Standard250x250ItemTemplate"><Grid HorizontalAlignment="Left" Width="250" Height="250"><Border Background="{StaticResource ListViewItemPlaceholderBackgroundThemeBrush}"><Image Source="{Binding Image}" Stretch="UniformToFill" AutomationProperties.Name="{Binding Title}"/></Border><StackPanel VerticalAlignment="Bottom" Background="{StaticResource ListViewItemOverlayBackgroundThemeBrush}"><TextBlock Text="{Binding Title}" Foreground="{StaticResource ListViewItemOverlayForegroundThemeBrush}" Style="{StaticResource TitleTextStyle}" Height="60" Margin="15,0,15,0"/><TextBlock Text="{Binding Subtitle}" Foreground="{StaticResource ListViewItemOverlaySecondaryForegroundThemeBrush}" Style="{StaticResource CaptionTextStyle}" TextWrapping="NoWrap" Margin="15,0,15,10"/></StackPanel></Grid>
</DataTemplate>
请注意数据绑定到模板控件的各个属性的方式。这种绑定格式自动完成了通常在 MVC 模式的“控制器”组件中完成的大部分工作。因此,您可能需要更改某些模型中数据的表示方式,以便轻松绑定,这就是我们使用 ViewModel 的原因。
此模板中绑定的属性与ApiPhoto
类中可用的属性不同。我们将使用 ViewModel 将模型转换为可以轻松绑定的内容。继续创建一个名为FlickrImage
的新类,其中包含模板期望的属性,如下所示:
public class FlickrImage
{public Uri Image { get; set; }public string Title { get; set; }
}
向Flickr
类添加以下字段和方法:
public static readonly ObservableCollection<FlickrImage> Images = new ObservableCollection<FlickrImage>();public static async void Load()
{var result = await LoadInteresting();var images = from photo in result.Photoselect new FlickrImage{Image = new Uri(photo.CreateUrl()),Title = photo.Title};Images.Clear();foreach (var image in images){Images.Add(image);}
}
Load()
方法首先调用LoadInteresting()
方法,该方法通过互联网访问 Flickr API 并检索有趣的照片(当然是异步的)。然后使用 LINQ 将结果转换为 ViewModels 的列表,并更新静态的Images
属性。请注意,它不会重新实例化实际的集合,而是Images
属性是ObservableCollection
集合,这是 ViewModel 中首选使用的集合类型。您可以在初始化页面时绑定到 XAML UI,然后随时重新加载数据,集合会负责通知 UI,绑定框架会负责更新屏幕。
定义了 ViewModel 后,我们准备创建实际的用户界面。首先从Visual C# | Windows Store菜单中的添加新项对话框中添加一个基本页面项目。将文件命名为FlickrPage.xaml
,如下截图所示:
这将创建一个非常基本的页面,其中包含一些简单的东西,如返回按钮。您可以通过打开App.xaml.cs
文件并将OnLaunched
方法更改为启动FlickrPage
而不是默认的MainPage
来更改应用程序启动时打开的页面。当然,我们可以在此示例中使用MainPage
,但是您将使用的大多数页面都需要返回按钮,因此您应该熟悉使用基本页面模板。
我们的界面将是最小化的,实际上,它只包含一个控件,我们将其放在FlickrPage.xaml
文件中,放在包含返回按钮的部分下方,如下所示:
<GridViewGrid.Row="2"ItemsSource="{Binding Images}"ItemTemplate="{StaticResource Standard250x250ItemTemplate}"/>
GridView
将负责以网格布局显示我们绑定到它的项目,非常适合显示一堆图片。我们将ItemsSource
属性绑定到我们的图像,ItemTemplate
用于格式化每个单独的项目,绑定到标准模板。
现在 XAML 已经设置好,我们所要做的就是实际加载数据并为页面设置DataContext
!打开名为FlickrPage.xaml.cs
的文件,并将以下两行代码添加到LoadState
方法中:
Flickr.Load();
this.DataContext = new { Images = Flickr.Images };
我们首先启动加载过程。请记住,这是一个异步方法,因此它将开始从互联网请求数据的过程。然后,我们设置数据上下文,这是 XAML 绑定用来获取数据的。请参阅以下截图:
总结
本章带您了解了构建 Windows Store 应用程序的过程,该应用程序使用第三方 API 访问互联网上的信息。语言的异步编程特性使得实现这些功能非常容易。
以下是一些关于如何进一步完善您在此构建的应用程序的想法:
-
当您点击缩略图时,使应用程序导航到图像的较大版本(提示:查看
ApiPhoto
中的CreateUrl
方法) -
找出应用程序无法访问互联网时会发生什么(提示:查看第三章中的使用异步方法处理错误部分,动态行为)
-
添加查看不同类型图像的功能(提示:尝试使用
Grid App
项目模板,并查看不同的 Flickr API 方法,如flickr.photos.search
) -
思考将分页添加到照片列表中(提示:查看
ApiPhotos
中的数据和ISupportIncrementalLoading
接口)
创建在本地计算机上运行的本地应用程序,让您可以针对每个技术周期硬件性能不断提升进行优化。使用 C# 5 中可用的新的异步编程功能,将确保您能充分利用这些硬件。在下一章中,我们将转向云端,并使用 ASP.NET MVC 构建一个 Web 应用程序。
第五章:移动 Web 应用
在上一章中,我们看到了一个用于在 Windows 商店分发的本机桌面应用程序的创建。在本章中,我们将创建一个 Web 应用程序,让用户登录,并在地图上看到与自己在同一物理区域的其他用户。我们将使用以下技术:
-
ASP.NET MVC 4:这让你可以使用模型-视图-控制器设计模式和异步编程构建 Web 应用程序
-
SignalR:这是一个异步的双向通信框架
-
HTML5 GeoLocation:为应用程序提供真实世界的位置
-
使用 Google 进行客户端地图映射:这是为了可视化地理空间信息
这些技术共同让你创建非常强大的 Web 应用程序,并且借助与 C# 5 一同发布的 ASP.NET MVC 4,现在更容易创建可以轻松访问互联网的移动应用程序。在本章结束时,我们将拥有一个 Web 应用程序,它使用现代浏览器功能,如 WebSockets,让你与其他在你附近的 Web 用户连接。所有这些都使选择 C#技术栈成为创建 Web 应用程序的一个非常引人注目的选择。
使用 ASP.NET MVC 的移动 Web
ASP.NET 已经发展成为支持多种不同产品的服务器平台。在 Web 端,我们有 Web Forms 和 MVC。在服务端,我们有 ASMX Web 服务、Windows 通信框架(WCF)和 Web 服务,甚至一些开源技术,如 ServiceStack 也已经出现。
Web 开发可以被总结为技术的大熔炉。成功的 Web 开发人员应该精通 HTML、CSS、JavaScript 和 HTTP 协议。在这个意义上,Web 开发可以帮助你成为一名多语言程序员,可以在多种编程语言中工作。我们将在这个项目中使用 ASP.NET MVC,因为它在 Web 开发的背景下应用了模型-视图-控制器设计模式,同时允许每个贡献的技术有机会发挥其所长。它在下图中显示:
你的模型块将包含所有包含业务逻辑的代码,以及连接到远程服务和数据库的代码。控制器块将从模型层检索信息,并在用户与视图块交互时将信息传递给它。
关于使用 JavaScript 进行客户端开发的有趣观察是,许多应用程序的架构选择与开发任何其他本机应用程序时非常相似。从在内存中维护应用程序状态的方式到访问和缓存远程信息的方式,有许多相似之处。
构建一个 MeatSpace 跟踪器
接下来是我们要构建的应用程序!
正如术语CyberSpace指的是数字领域一样,术语MeatSpace在口语中用来指代在现实世界中发生的事物或互动。我们将在本章中创建的项目是一个移动应用程序,可以帮助你与 Web 应用程序的其他用户在你附近的物理位置进行连接。构建一个在真实世界中知道你位置的移动网站的对比非常吸引人,因为就在短短几年前,这类应用程序在 Web 上是不可能的。
这个应用程序将使用 HTML 5 地理位置 API 来让你在地图上看到应用程序的其他用户。当用户连接时,它将使用 SignalR 与服务器建立持久连接,这是一个由几名微软员工发起的开源项目。
迭代零
在我们开始编写代码之前,我们必须启动项目,迭代零。我们首先创建一个新的 ASP.NET MVC 4 项目,如下截图所示。在这个例子中,我正在使用 Visual Studio 2012 Express for Web,当然,完整版本的 Visual Studio 2012 也可以使用。
一旦选择了 MVC 4 项目,就会出现一个对话框,其中包含几种不同类型的项目模板。由于我们希望我们的 Web 应用程序可以从手机访问,所以我们选择 Visual Studio 2012 中包含的新项目模板之一,Mobile Application。该模板预装了一些有用的 JavaScript 库,列举如下:
-
jQuery和jQuery.UI:这是一个非常流行的库,用于简化对 HTML DOM 的访问。该库的 UI 部分提供了一个漂亮的小部件工具包,可在各种浏览器上使用,包括日期选择器等控件。
-
jQuery.Mobile:这提供了一个框架,用于创建移动友好的 Web 应用程序。
-
KnockoutJS:这是一个 JavaScript 绑定框架,可以让您实现 Model-View-ViewModel 模式。
-
Modernizr:这允许您进行丰富的功能检测,而不是查看浏览器的用户代理字符串来确定您可以依赖的功能。
我们将不会使用所有这些库,当然,您也可以选择不同的 JavaScript 库。但这些提供了一个方便的起点。您应该花一些时间熟悉项目模板创建的文件。
您应该首先查看主HomeController
类,因为这是(默认情况下)应用程序的入口点。默认情况下包含一些占位文本;您可以轻松更改此文本以适应您正在构建的应用程序。对于我们的目的,我们只需更改一些文本,以充当简单的信息,并鼓励用户注册。
修改Views/Home/Index.cshtml
文件如下:
<h2>@ViewBag.Message</h2>
<p>Find like-minded individuals with JoinUp
</p>
注意@ViewBag.Message
标题,您可以按照以下方式更改HomeController
类的Index
操作方法中的特定值:
public ActionResult Index()
{ViewBag.Message = "MeetUp. TalkUp. JoinUp";return View();
}
还有其他视图,您可以更改以添加自己的信息,例如关于和联系页面,但对于这个特定的演示目的来说,它们并不是关键的。
进行异步操作
ASP.NET MVC 的最新版本中最强大的新增功能之一是能够使用 C# 5 中的新async
和await
关键字编写异步操作方法。要清楚,自 ASP.NET MVC 2 以来,您就已经有了创建异步操作方法的能力,但它们相当笨拙且难以使用。
您必须手动跟踪正在进行的异步操作的数量,然后让异步控制器知道它们何时完成,以便它可以完成响应。在 ASP.NET MVC 4 中,这不再是必要的。
例如,我们可以重写我们在上一节中讨论的Index
方法,使其成为异步的。假设我们希望在登陆页面的标题中打印的消息来自数据库。因为这可能需要与另一台机器上的数据库服务器通信,所以这是一个完美的异步方法候选者。
首先,创建一个可等待的方法,用作从数据库中检索消息的占位符,如下所示:
private async Task<string> GetSiteMessage()
{await Task.Delay(1);return "MeetUp. TalkUp. JoinUp";
}
当然,在您的实际代码中,这将连接到数据库,例如,它只是在返回字符串之前引入了一个非常小的延迟。现在,您可以按照以下方式重写Index
方法:
public async Task<ActionResult> Index()
{ViewBag.Message = await GetSiteMessage();return View();
}
您可以看到在先前代码中突出显示的方法的更改,您只需向方法添加async
关键字,将返回值设置为Task<ActionResult>
类,然后在方法体中使用await
。就是这样!现在,您的方法将允许 ASP.NET 运行时通过处理其他请求来最大程度地优化其资源,同时等待您的方法完成处理。
获取用户位置
一旦我们定义了初始着陆页面,我们就可以开始查看已登录的界面。请记住,我们应用程序的明确目标是帮助您在现实世界中与其他用户建立联系。为此,我们将使用包括移动浏览器在内的许多现代浏览器中包含的一个功能,以检索用户的位置。为了将所有人连接在一起,我们还将使用一个名为SignalR的库,它可以让您与用户的浏览器建立双向通信渠道。
该项目的网站简单地描述如下:
.NET 的异步库,用于帮助构建实时的、多用户交互式的 Web 应用程序。
使用 SignalR,您可以编写一个应用程序,让您可以双向与用户的浏览器进行通信。因此,您不必等待浏览器与服务器发起通信,实际上您可以从服务器调用并向浏览器发送信息。有趣的是,SignalR 是开源的,因此您可以深入了解其实现。但是对于我们的目的,我们将首先向我们的 Web 应用程序添加一个引用。您可以通过 Nuget 轻松实现这一点,只需在包管理控制台中运行以下命令:
install-package signalr
或者,如果您更喜欢使用 GUI 工具,可以右键单击项目的引用节点,然后选择管理 NuGet 包。从那里,您可以搜索 SignalR 包并单击安装按钮。
安装了该依赖项后,我们可以开始勾画用户在登录时将看到的界面,并为我们提供应用程序的主要功能。我们通过使用Empty MVC Controller
模板向Controllers
文件夹添加一个新的控制器来开始添加新屏幕的过程。将类命名为MapController
,如下所示:
public class MapController : Controller
{public ActionResult Index(){return View();}
}
默认情况下,您创建的文件将与先前代码中的文件相似;请注意控制器前缀(Map
)和操作方法名称(Index
)。创建控制器后,您可以添加视图,根据约定,使用控制器名称和操作方法名称。
首先,在Views
文件夹中添加一个名为Map
的文件夹,所有此控制器的视图都将放在这里。在该文件夹中,添加一个名为Index.cshtml
的视图。确保选择Razor
视图引擎,如果尚未选择。生成的 razor 文件非常简单,它只是设置页面的标题(使用 razor 代码块),然后输出一个带有操作名称的标题,如下所示:
@{ViewBag.Title = "JoinUp Map";
}<h2>Index</h2>
现在我们可以开始修改此视图并添加地理位置功能。将以下代码块添加到Views/map/Index.cshtml
的底部:
@section scripts {@Scripts.Render("~/Scripts/map.js")
}
此脚本部分在站点范围模板中定义,并确保以正确的顺序呈现脚本引用,以便所有其他主要依赖项(例如 jQuery)已被引用。
接下来,我们创建了在先前代码中引用的map.js
文件,其中将保存我们所有的 JavaScript 代码。在我们的应用程序中,首先要做的是让我们的地理位置工作起来。将以下代码添加到map.js
中,以了解如何获取用户的位置:
$(function () {var geo = navigator.geolocation;if (geo) {geo.getCurrentPosition(userAccepted, userDenied);} else {userDenied({message:'not supported'}); }
});
这从一个传递给 jQuery 的函数定义开始,当 DOM 加载完成时将执行该函数。在该方法中,我们获取对navigator.geolocation
属性的引用。如果该对象存在(例如,浏览器实现了地理位置),那么我们调用.getCurrentPosition
方法并传入两个我们定义的回调函数,如下所示:
function userAccepted(pos) {alert("lat: " +pos.coords.latitude +", lon: " +pos.coords.longitude);
}function userDenied(msg) {alert(msg.message);
}
保存了带有上述代码的map.js
后,您可以运行 Web 应用程序(F5)以查看其行为。如下截图所示,用户将被提示是否要允许 Web 应用程序跟踪他们的位置。如果他们点击允许,将执行userAccepted
方法。如果他们点击拒绝,将执行userDenied
消息。当未提供位置时,您可以使用此方法来相应地调整应用程序。
使用 SignalR 进行广播
用户的位置确定后,接下来的过程将涉及使用 SignalR 将每个连接的用户的位置广播给其他每个用户。
我们可以做的第一件事是通过在Views/Map/Index.cshtml
的脚本引用中添加以下两行来为 SignalR 添加脚本引用:
<ul id="messages"></ul>@section scripts {@Scripts.Render("~/Scripts/jquery.signalR-0.5.3.min.js")@Scripts.Render("~/signalr/hubs")@Scripts.Render("~/Scripts/map.js")
}
这将初始化 SignalR 基础设施,并允许我们在实现服务器之前构建应用程序的客户端部分。
提示
在撰写本文时,jQuery.signalR
库的版本 0.5.3 是最新版本。根据您阅读本书的时间,这个版本很可能已经改变。只需在通过 Nuget 添加 SignalR 依赖项后查看Scripts
目录,以查看您应该在此处使用哪个版本。
接下来,删除map.js
类的所有先前内容。为了保持组织,我们首先声明一个 JavaScript 类,其中包含一些方法,如下所示:
var app = {geoAccepted: function(pos) {var coord = JSON.stringify(pos.coords);app.server.notifyNewPosition(coord);},initializeLocation: function() {var geo = navigator.geolocation;if (geo) {geo.getCurrentPosition(this.geoAccepted);} else {error('not supported');}},onNewPosition: function(name, coord) {var pos = JSON.parse(coord);$('#messages').append('<li>' + name + ', at '+ pos.latitude +', '+ pos.longitude +'</li>');}
};
您将认出initializeLocation
方法,它与我们先前在其中初始化地理位置 API 的代码相同。在此版本中,初始化函数传递了另一个函数geoAccepted
,作为用户接受位置提示时执行的回调。最终函数onNewPosition
旨在在有人通知服务器有新位置时执行。SignalR 将广播位置并执行此函数,以让此脚本知道用户的名称和他们的新坐标。
页面加载时,我们希望初始化与 SignalR 的连接,并在此过程中使用我们刚刚在名为app
的变量中创建的对象,可以按如下方式完成:
$(function () {var server = $.connection.serverHub;server.onNewPosition = app.onNewPosition;app.server = server;$.connection.hub.start().done(function () {app.initializeLocation();});
});
Hubs,在 SignalR 中,是一种非常简单的方式,可以轻松地由客户端的 JavaScript 代码调用方法。在Models
文件夹中添加一个名为ServerHub
的新类,如下所示:
public class ServerHub : Hub
{public void notifyNewPosition(string coord){string name = HttpContext.Current.User.Identity.Name;Clients.onNewPosition(name, coord);}
}
在此 hub 中定义了一个方法notifyNewPosition
,它接受一个字符串。当我们从用户那里获得坐标时,此方法将将其广播给所有其他连接的用户。为此,代码首先获取用户的名称,然后调用.onNewPosition
方法将名称和坐标与所有连接的用户一起广播。
有趣的是,Clients
属性是一个动态类型,因此onNewPosition
实际上并不存在于该属性的方法中。该方法的名称用于自动生成从 JavaScript 代码调用的客户端方法。
为了确保用户在访问页面时已登录,我们只需在MapController
类的顶部添加[Authorize]
属性,如下所示:
[Authorize]
public class MapController : Controller
按下F5运行您的应用程序,看看我们的进展如何。如果一切正常,您将看到如下截图所示的屏幕:
当人们加入网站时,他们的位置被获取并推送给其他人。同时,在客户端,当收到新的位置时,我们会添加一个新的列表项元素,详细说明刚刚收到的名称和坐标。
我们正在逐步逐一地构建我们的功能,一旦我们验证了这一点,我们就可以开始完善下一个部分。
映射用户
随着位置信息被推送给每个人,我们可以开始在地图上显示他们的位置。对于这个示例,我们将使用 Google Maps,但您也可以轻松地使用 Bing、Nokia 或 OpenStreet 地图。但是,这个想法是为您提供一个空间参考,以查看谁还在查看相同的网页,以及他们相对于您在世界上的位置。
首先,在Views/Map/Index.cshtml
中添加一个 HTML 元素来保存地图,如下所示:
<div id="map"style="width:100%; height: 200px;">
</div>
这个<div>
将作为实际地图的容器,并将由 Google Maps API 管理。接下来在map.js
引用上面的脚本部分添加 JavaScript,如下所示:
@section scripts {@Scripts.Render("~/Scripts/jquery.signalR-0.5.3.min.js")@Scripts.Render("~/signalr/hubs")@Scripts.Render("http://maps.google.com/maps/api/js?sensor=false");@Scripts.Render("~/Scripts/map.js")
}
与 SignalR 脚本一样,我们只需要确保它在我们自己的脚本(map.js
)之前被引用,以便在我们的源中可用。接下来,我们添加代码来初始化地图,如下所示:
function initMap(coord) {var googleCoord = new google.maps.LatLng(coord.latitude, coord.longitude);if (!app.map) {var mapElement = document.getElementById("map");var map = new google.maps.Map(mapElement, {zoom: 15,center: googleCoord,mapTypeControl: false,navigationControlOptions: { style: google.maps.NavigationControlStyle.SMALL },mapTypeId: google.maps.MapTypeId.ROADMAP});app.map = map;}else {app.map.setCenter(googleCoord);}
}
当获取位置时,将调用此函数。它通过获取用户最初报告的位置,并将对map
ID 的<div>
HTML 元素的引用传递给google.maps.Map
对象的新实例,将地图的中心设置为用户报告的位置。如果再次调用该函数,它将简单地将地图的中心设置为用户的坐标。
为了显示所有位置,我们将使用 Google Maps 的一个功能来在地图上放置一个标记。将以下函数添加到map.js
中:
function addMarker(name, coord) {var googleCoord = new google.maps.LatLng(coord.latitude, coord.longitude);if (!app.markers) app.markers = {};if (!app.markers[name]) {var marker = new google.maps.Marker({position: googleCoord,map: app.map,title: name});app.markers[name] = marker;}else {app.markers[name].setPosition(googleCoord);}
}
这个方法通过使用一个关联的 JavaScript 数组来跟踪已添加的标记,类似于 C#中的Dictionary<string, object>
集合。当用户报告新位置时,它将获取现有的标记并将其移动到新位置。这意味着,对于每个登录的唯一用户,地图将显示一个标记,然后每次报告新位置时都会移动它。
最后,我们对应用对象中的现有函数进行了三个小的更改,以便与地图进行交互。首先在initializeLocation
中,我们从getCurrentPosition
更改为使用watchPosition
方法,如下所示:
initializeLocation: function() {var geo = navigator.geolocation;if (geo) {geo.watchPosition(this.geoAccepted);} else {error('not supported');}
},
watchPosition
方法将在用户位置发生变化时更新用户的位置,这应该导致所有位置的实时视图,因为它们将其报告给服务器。
接下来,我们更新geoAccepted
方法,该方法在用户获得新坐标时运行。我们可以利用这个事件在通知服务器新位置之前初始化地图,如下所示:
geoAccepted: function (pos) {var coord = JSON.stringify(pos.coords);initMap(pos.coords);app.server.notifyNewPosition(coord);
},
最后,在通知我们的页面每当用户报告新位置时的方法中,我们添加一个调用addMarker
函数,如下所示:
onNewPosition: function(name, coord) {var pos = JSON.parse(coord);addMarker(name, pos);$('#messages').append('<li>' + name + ', at '+ pos.latitude +', '+ pos.longitude +'</li>');
}
测试应用
当测试应用程序时,您可以在自己的计算机上进行一些初步测试。但这意味着您将始终只有一个标记位于地图的中心(即您)。为了进行更深入的测试,您需要将您的 Web 应用程序部署到可以从互联网访问的服务器上。
有许多可用的选项,从免费(用于测试)到需要付费的解决方案。当然,您也可以自己设置一个带有 IIS 的服务器并以这种方式进行管理。在 ASP.NET 网站的 URL www.asp.net/hosting
上可以找到一个寻找主机的好资源。
一旦应用程序上传到服务器,尝试从不同的设备和不同的地方访问它。接下来的三个屏幕截图证明了应用程序在桌面上的工作:
在 iPad 上,您将看到以下屏幕:
在 iPhone 上,您将看到以下屏幕:
总结
就是这样……一个 Web 应用程序,可以根据您的实际位置,实时连接您与该应用程序的其他用户。为此,我们探索了各种技术,任何现代 Web 开发人员,特别是 ASP.NET 开发人员都应该熟悉:ASP.NET MVC,SignalR,HTML5 GeoLocation 以及使用 Google Maps 进行客户端地图绘制。
以下是一些您可以用来扩展此示例的想法:
-
考虑将用户的最后已知位置持久化存储在诸如 SQL Server 或 MongoDB 之类的数据库中
-
考虑如何扩展这种应用程序以支持更多用户(查看
SignalR.Scaleout
库) -
将通知的用户限制为仅在一定距离内的用户(学习如何使用 haversine 公式计算地球上两点之间的距离)
-
展示用户附近的兴趣点,可以使用 Web 上可用的各种位置数据库,如 FourSquare Venus API 或 FaceBook Places API。
第六章:跨平台开发
微软平台并不是唯一可以执行 C#代码的平台。使用 Mono 框架,您可以针对其他平台进行开发,如 Linux、Mac OS、iOS 和 Android。在本章中,我们将探讨构建 Mac 应用程序所需的工具和框架。我们将在这里看到一些工具,例如:
-
MonoDevelop:这是一个 C# IDE,可以让您在其他非 Windows 平台上编写 C#
-
MonoMac:这提供了对 Mac 库的绑定,因此您可以从 C#使用本机 API
-
Cocoa:这是用于创建 Mac 应用程序的框架
我们将在本章中构建的应用程序是一个实用程序,您可以使用它来查找网站上的文本。给定一个 URL,应用程序将查找链接,并跟随它们查找特定的触发文本。我们将使用 Mac OS 的 UI SDK,AppKit 来显示结果。
构建网络爬虫
如果您有 C#经验并且需要构建应用程序或实用程序,Mono 可以让您快速创建它,利用现有的技能。假设您需要监视一个网站,以便在包含给定文本的新帖子出现时采取行动。与其整天手动刷新页面,不如构建一个自动化系统来完成这项任务。如果网站没有提供 RSS 订阅或其他 API 来提供程序化访问,您总是可以退而求其次,使用一种可靠的方法来获取远程数据——编写一个 HTTP 爬虫。
这听起来比实际复杂,这个实用程序将允许您输入一个 URL 和一些参数,以便应用程序知道要搜索什么。然后,它将负责访问网站,请求所有相关页面,并搜索您的目标文本。
从创建项目开始。打开 MonoDevelop 并从文件 | 新建 | 解决方案菜单项创建一个新项目,这将打开新解决方案对话框。在该对话框中,从左侧面板的C# | MonoMac列表中选择MonoMac 项目。创建解决方案时,项目模板将初始化为 Mac 应用程序的基础,如下面的屏幕截图所示:
与我们在上一章中构建的 Web 应用程序一样,Mac 应用程序使用模型-视图-控制器模式来组织自己。项目模板已经创建了控制器(MainWindowControl
)和视图(MainWindow.xib
);创建模型由您来完成。
构建模型
使用类似 MonoMac 这样的工具的主要好处之一是能够在不同平台之间共享代码,特别是如果您已经熟悉 C#。因为我们正在编写 C#,所以任何通用逻辑和数据结构都可以在需要为不同平台构建相同应用程序的情况下得到重用。例如,一个名为 iCircuit 的流行应用程序(icircuitapp.com
),它是使用 Mono 框架编写的,已经发布了 iOS、Android、Mac 和 Windows Phone 版本。iCircuit 应用程序在某些平台上实现了近 90%的代码重用。
这个数字之所以不是 100%是因为 Mono 框架最近一直专注于使用本机框架和接口构建应用程序的指导原则之一。过去跨平台工具包的主要争议点之一是它们从来没有特别本地化,因为它们被迫妥协以保持兼容性的最低公分母。使用 Mono,您被鼓励通过 C#使用平台的本机 API,以便您可以利用该平台的所有优势。
模型是您可以找到最多重用的地方,只要您尽量将所有特定于平台的依赖项排除在模型之外。为了保持组织,创建一个名为models
的文件夹,用于存储所有模型类。
访问网络
与我们在第四章中构建的 Windows 8 应用程序一样,创建 Windows Store 应用程序,我们想要做的第一件事是提供连接到 URL 并从远程服务器下载数据的能力。不过,在这种情况下,我们只想要访问 HTML 文本,以便我们可以解析它并查找各种属性。在/Models
目录中添加一个名为WebHelper
的类,如下所示:
using System;
using System.IO;
using System.Net;
using System.Threading.Tasks;namespace SiteWatcher
{internal static class WebHelper{public static async Task<string> Get(string url){var tcs = new TaskCompletionSource<string>();var request = WebRequest.Create(url);request.BeginGetResponse(o => {var response = request.EndGetResponse(o);using (var reader = new StreamReader(response.GetResponseStream())){var result = reader.ReadToEnd();tcs.SetResult(result);}}, null);return await tcs.Task;}}
}
这与我们在第四章中构建的WebRequest
类非常相似,创建 Windows Store 应用程序,只是它只返回我们要解析的 HTML 字符串,而不是反序列化 JSON 对象;并且因为Get
方法将执行远程 I/O,我们使用async
关键字。作为一个经验法则,任何可能需要超过 50 毫秒才能完成的 I/O 绑定方法都应该是异步的。微软在决定哪些 OS 级 API 将是异步的时,使用了 50 毫秒的阈值。
现在,我们将为用户在用户界面中输入的数据构建后备存储模型。我们希望为用户做的一件事是保存他们的输入,这样他们下次启动应用程序时就不必重新输入。幸运的是,我们可以利用 Mac OS 上的一个内置类和 C# 5 的动态对象特性来轻松实现这一点。
NSUserDefaults
类是一个简单的键/值存储 API,它会在应用程序会话之间保留您放入其中的设置。但是,尽管针对“属性包”进行编程可以为您提供一个非常灵活的 API,但它可能会很冗长,并且难以一眼理解。为了减轻这一点,我们将在NSUserDefaults
周围构建一个很好的动态包装器,以便我们的代码至少看起来是强类型的。
首先,确保您的项目引用了Microsoft.CSharp.dll
程序集;如果没有,请添加。然后,在Models
文件夹中添加一个名为UserSettings.cs
的新类文件,并从DynamicObject
类继承。请注意,此类中使用了MonoMac.Foundation
命名空间,这是 Mono 绑定到 Mac 的 Core Foundation API 的位置。
using System;
using System.Dynamic;
using MonoMac.Foundation;namespace SiteWatcher
{public class UserSettings : DynamicObject{NSUserDefaults defaults = NSUserDefaults.StandardUserDefaults;public override bool TryGetMember(GetMemberBinder binder, out object result){result = defaults.ValueForKey(new NSString(binder.Name));if (result == null) result = string.Empty;return result != null;}public override bool TrySetMember(SetMemberBinder binder, object value){defaults.SetValueForKey(NSObject.FromObject(value), new NSString(binder.Name));return true;}}
}
我们只需要重写两个方法,TryGetMember
和TrySetMember
。在这些方法中,我们将使用NSUserDefaults
类,这是一个本地的 Mac API,来获取和设置给定的值。这是一个很好的例子,说明了我们如何在运行的本地平台上搭建桥梁,同时仍然具有一个 C#友好的 API 表面来进行编程。
当然,敏锐的读者会记得,在本章的开头,我说我们应该尽可能将特定于平台的代码从模型中分离出来。这通常是一个指导方针。如果我们想要将这个程序移植到另一个平台,我们只需将这个类的内部实现替换为适合该平台的内容,比如在 Android 上使用SharedSettings
,或者在 Windows RT 上使用ApplicationDataContainer
。
创建一个数据源
接下来,我们将构建一个类,该类将封装大部分我们的主要业务逻辑。当我们谈论跨平台开发时,这将是一个主要的候选代码,可以在所有平台上共享;并且您能够将代码抽象成这样的自包含类,它将更有可能被重复使用。
在Models
文件夹中创建一个名为WebDataSource.cs
的新文件。这个类将负责通过网络获取并解析结果。创建完类后,向类中添加以下两个成员:
private List<string> results = new List<string>();public IEnumerable<string> Results
{get { return this.results; }
}
这个字符串列表将在我们在网站源中找到匹配项时驱动用户界面。为了解析 HTML 以获得这些结果,我们可以利用一个名为HTML Agility Pack的优秀开源库,您可以在 CodePlex 网站上找到它(htmlagilitypack.codeplex.com/
)。
当您下载并解压缩包后,请在Net45
文件夹中查找名为HtmlAgilityPack.dll
的文件。这个程序集将在所有 CLR 平台上工作,因此您可以将其复制到您的项目中。通过右键单击解决方案资源管理器中的References
节点,并选择编辑引用 | .NET 程序集,将程序集添加为引用。从.NET 程序集表中浏览到HtmlAgilityPack.dll
程序集,然后单击确定。
现在我们已经添加了这个依赖项,我们可以开始编写应用程序的主要逻辑。记住,我们的目标是创建一个允许我们搜索网站特定文本的界面。将以下方法添加到WebDataSource
类中:
public async Task Retrieve()
{ dynamic settings = new UserSettings();var htmlString = await WebHelper.Get(settings.Url);HtmlDocument html = new HtmlDocument();html.LoadHtml(htmlString);foreach(var link in html.DocumentNode.SelectNodes(settings.LinkXPath)){string linkUrl = link.Attributes["href"].Value;if (!linkUrl.StartsWith("http")) {linkUrl = settings.Url + linkUrl;}// get this URLstring post = await WebHelper.Get (linkUrl);ProcessPost(settings, link, post);}
}
Retrieve
方法使用async
关键字启用您等待异步操作,首先实例化UserSettings
类作为动态对象,以便我们可以从 UI 中提取值。接下来,我们检索初始 URL 并将结果加载到HtmlDocument
类中,这样我们就可以解析出我们正在寻找的所有链接。在这里变得有趣,对于每个链接,我们异步检索该 URL 的内容并进行处理。
提示
您可能会认为,因为您在循环中等待(使用await
关键字),循环的每次迭代都会并发执行。但请记住,异步不一定意味着并发。在这种情况下,编译器将重写代码,以便主线程在等待 HTTP 调用完成时不被阻塞,但循环在等待时也不会继续迭代,因此循环的每次迭代将按正确的顺序完成。
最后,我们实现了ProcessPost
方法,该方法接收单个 URL 的内容,并使用用户提供的正则表达式进行搜索。
private void ProcessPost(dynamic settings, HtmlNode link, string postHtml)
{ // parse the doc to get the content area: settings.ContentXPathHtmlDocument postDoc = new HtmlDocument();postDoc.LoadHtml(postHtml);var contentNode = postDoc.DocumentNode.SelectSingleNode(settings.ContentXPath);if (contentNode == null) return;// apply settings.TriggerRegexstring contentText = contentNode.InnerText;if (string.IsNullOrWhiteSpace(contentText)) return;Regex regex = new Regex(settings.TriggerRegex);var match = regex.Match(contentText);// if found, add to resultsif (match.Success){results.Add(link.InnerText);}
}
完成WebDataSource
类后,我们拥有了开始工作于用户界面的一切所需。这表明了一些良好的抽象(WebHelper
和UserSettings
)以及async
和await
等新功能可以结合起来产生相对复杂的功能,同时保持良好的性能。
构建视图
接下来,我们将构建 MVC 三角形的第二和第三条腿,即视图和控制器。从视图开始是下一个逻辑步骤。在开发 Mac 应用程序时,构建 UI 的最简单方法是使用 Xcode 的界面构建器,您可以从 Mac App Store 安装该构建器。Mac 上的 MonoDevelop 专门与 Xcode 进行交互以构建 UI。
首先通过在 MonoDevelop 中双击MainWindow.xib
来打开它。它将自动在界面构建器编辑器中打开 XCode。表单最初只是一个空白窗口,但我们将开始添加视图。最初,对于任何使用过 Visual Studio 的 WinForms 或 XAML 的 WYSIWYG 编辑器的人来说,体验将非常熟悉,但这些相似之处很快就会分歧。
如果尚未显示,请通过单击屏幕右侧的按钮来显示实用程序面板,如下截图所示,您可以在 Xcode 的右上角找到该按钮。
找到对象库并浏览可用的用户界面元素列表。现在,从对象库中查找垂直分割视图,并将其拖到编辑器表面,确保将其拉伸到整个窗口,如下截图所示。
这样我们就可以构建一个简单的用户界面,让用户调整各种元素的大小,以适应他/她的需求。接下来,我们将把用户提供的选项作为文本字段元素添加到左侧面板,并附带标签。
-
URL:这是您想要抓取的网站的 URL。
-
Item Link XPath:这是在使用 URL 检索的页面上。这个 XPath 查询应该返回您感兴趣的扫描链接的列表。
-
内容 XPath:对于每个项目,我们将根据从Item Link XPath检索到的 URL 检索 HTML 内容。在新的 HTML 文档中,我们想要选择一个我们将查看的内容元素。
-
触发正则表达式:这是我们将用来指示匹配的正则表达式。
我们还希望有一种方法来显示任何匹配的结果。为此,从对象库中添加一个表视图到右侧第二个面板。这个表视图,类似于常规.NET/Windows 世界中的网格控件,将为我们提供一个以列表格式显示结果的地方。还添加一个推按钮,我们将用它来启动我们的网络调用。
完成后,您的界面应该看起来像下面的截图:
界面定义好后,我们开始查看控制器。如果您以前从未使用过 Xcode,将单独的视图元素暴露给控制器是独特的。其他平台的其他工具 tend to automatically generate code references to textboxes and buttons,但在 Xcode 中,您必须手动将它们链接到控制器中的属性。您将会接触到一些 Objective-C 代码,但只是很简短的,除了以下步骤外,您实际上不需要做任何事情。
-
显示助理编辑器,并确保
MainWindowController.h
显示在编辑器中。这是我们程序中将与视图交互的控制器的头文件。 -
您必须向控制器添加所谓的outlets并将它们与 UI 元素连接起来,这样您就可以从代码中获取对它们的引用。这是通过按住键盘上的Ctrl键,然后从控件文本框拖动到头文件中完成的。
在生成代码之前,将显示一个小对话框,如下截图所示,它让您在生成代码之前更改一些选项:
- 对于所有文本视图都这样做,并为它们赋予适当的名称,如
urlTextView
、linkXPathTextView
、contentXPathTextView
、regexTextView
和resultsTableView
。
当您添加按钮时,您会注意到您有一个选项可以将连接类型更改为Action连接,而不是Outlet连接。这是您可以连接按钮的单击事件的方法。完成后,头文件应该定义以下元素:
@property (assign) IBOutlet NSTextField *urlTextView;
@property (assign) IBOutlet NSTextField *linkXPathTextView;
@property (assign) IBOutlet NSTextField *contentXPathTextView;
@property (assign) IBOutlet NSTextField *regexTextView;
@property (assign) IBOutlet NSTableView *resultsTableView;- (IBAction)buttonClicked:(NSButton *)sender;
- 关闭 Xcode,返回到 MonoDevelop,并查看
MainWindow.designer.cs
文件。
您会注意到您添加的所有 outlets 和 actions 都将在 C#代码中表示。MonoDevelop 会监视文件系统上的文件,当 Xcode 对其进行更改时,它会相应地重新生成此代码。
请记住,我们希望用户的设置在会话之间保持。因此,当窗口加载时,我们希望用先前输入的任何值初始化文本框。我们将使用我们在本章前面创建的UserSettings
类来提供这些值。覆盖WindowDidLoad
方法(如下面的代码所示),该方法在程序首次运行时执行,并将用户设置的值设置为文本视图。
public override void WindowDidLoad ()
{base.WindowDidLoad ();dynamic settings = new UserSettings();urlTextView.StringValue = settings.Url;linkXPathTextView.StringValue = settings.LinkXPath;contentXPathTextView.StringValue = settings.ContentXPath;regexTextView.StringValue = settings.TriggerRegex;
}
- 现在,我们将注意力转向数据的显示。我们应用程序中的主要输出是
NSTableView
,我们将使用它来显示目标 URL 中的任何匹配链接。为了将数据绑定到表格,我们创建一个从NSTableViewSource
继承的自定义类。
private class TableViewSource : NSTableViewSource
{private string[] data;public TableViewSource(string[] list) { data = list; }public override int GetRowCount (NSTableView tableView){return data.Length;}public override NSObject GetObjectValue (NSTableView tableView, NSTableColumn tableColumn, int row){return new NSString(data[row]);}
}
每当表视图需要渲染给定的表格单元时,表视图将在GetObjectValue
方法中请求行数据。因此,当请求时,它只需获取一个字符串数组,并从数组中返回适当的索引。
- 现在我们定义了一个方法,它几乎可以将所有东西都整合在一起。
private async void GetData()
{// retrieve data from UIdynamic settings = new UserSettings();settings.Url = urlTextView.StringValue;settings.LinkXPath = linkXPathTextView.StringValue;settings.ContentXPath = contentXPathTextView.StringValue;settings.TriggerRegex = regexTextView.StringValue;// initiate data retrievalWebDataSource datasource = new WebDataSource();await datasource.Retrieve();// display dataTableViewSource source = new TableViewSource(datasource.Results.ToArray());resultsTableView.Source = source;
}
在GetData
方法中,我们首先要做的是从文本框中获取值,并将其存储在UserSettings
对象中。接下来,我们异步地从WebDataSource
中检索数据。现在,将结果传递给TableViewSource
以便显示。
- 最后,实现在 Xcode 中连接的
buttonClicked
操作。
partial void buttonClicked (MonoMac.AppKit.NSButton sender)
{GetData ();
}
现在运行程序,并输入一些要搜索的网页的值。您应该会看到类似于以下截图中显示的结果,您也可以尝试使用相同的值,但请注意,如果 Hacker News 已更新其 HTML 结构,则不起作用。
摘要
在本章中,我们使用 MonoMac 和 MonoDevelop 为 Mac OS 创建了一个小型实用程序应用程序。以下是一些可以用来扩展或改进此应用程序的想法:
-
跨应用程序会话保留结果(查看 Core Data)
-
通过在处理过程中向用户提供反馈来改善用户体验(查看
NSProgressIndicator
) -
通过并行化 URL 请求来提高应用程序的性能(查看
Parallel.ForEach
) -
尝试将应用程序移植到不同的平台。对于 iOS,查看 MonoTouch(
ios.xamarin.com
),对于 Android,查看 Mono for Android(android.xamarin.com
)
C#是一种非常表达力和强大的语言。能够针对每个主流计算平台,作为开发人员,您有着令人难以置信的机会,同时可以使用一种一致的编程语言,在不同平台上轻松重用代码。