在CentOS 7上安装.NET Core R2跑Hello World

前言

在上个月.NET Core出了最新版本预览版,只是在Window系统上试验了一下。原本想等发布正式版的时候在linux系统上试试,可能还需要一段时间,刚好有空可以折腾一下。

由于之前安装的Ubuntu系统是16的,目前还不支持,就没有在Ubuntu上测试。

系统环境:CentOS 7(1511) Minimal Core版本。

安装

提示:
Nodejs和Yeoman是用于自动化创建ASP.NET项目的,不是必须项。
本文是用Yeoman创建的ASP.NET项目,故添加了Nodejs和Yeoman的安装。
如果觉得不需要可以不安装。

.NET Core[必须]

1.去官网下载.NET Core:dotnet-dev-centos-x64.1.0.0-preview1-002702.tar.gz

2.移动并解压下载的安装文件。

$ # 创建dotnet文件夹
$ mkdir ~/dotnet
$ # 复制dotnet安装文件到dotnet文件夹下
$ cp dotnet-dev-centos-x64.1.0.0-preview1-002702.tar.gz ~/dotnet
$ # 解压安装文件
$ tar -xzf ~/dotnet/dotnet-dev-centos-x64.1.0.0-preview1-002702.tar.gz
$ # 删除dotnet文件夹下的原文件
$ rm ~/dotnet/dotnet-dev-centos-x64.1.0.0-preview1-002702.tar.gz

3.添加软连接,可以在全局使用。

$ ln -s ~/dotnet/dotnet /usr/local/bin

4.测试安装是否成功。

$ dotnet --version
1.0.0-preview1-002702 #输出版本号

Nodejs[可选]

不要使用系统自带的安装,版本太低。

1.去官网下载nodejs安装文件:node-v4.4.5-linux-x64.tar.xz

2.复制并解压。

$ # 创建nodejs文件夹
$ mkdir /usr/local/nodejs
$ # 复制node安装文件到nodejs文件夹下
$ cp node-v4.4.5-linux-x64.tar.xz /usr/local/nodejs
$ # 解压安装文件
$ tar -zvxf /usr/local/nodejs/node-v4.4.5-linux-x64.tar.xz
$ # 删除nodejs文件夹下的原文件
$ rm /usr/local/nodejs/node-v4.4.5-linux-x64.tar.xz

3.添加软连接,可以在全局使用。

$ sudo ln -s /usr/local/nodejs/node /usr/local/bin/node
$ sudo ln -s /usr/local/nodejs/npm /usr/local/bin/npm
$ sudo ln -s /usr/local/nodejs/node /usr/bin/node
$ sudo ln -s /usr/local/nodejs/node /usr/lib/node
$ sudo ln -s /usr/local/nodejs/npm /usr/bin/npm

4.测试安装是否成功。

$ node -v
v4.4.5 #输出版本号
$ npm -v
3.9.5 #输出版本号

Yeoman[可选]

Yeoman是用于创建asp.net的,同时需要安装相关的node插件:bower、grunt、gulp。

1.使用npm执行安装命令

$ sudo npm install -g yo bower grunt-cli gulp

等待安装完成。

2.安装asp.net生成器

$ npm install -g generator-aspnet

3.添加系统变量

将node的bin文件夹添加至系统变量中,否则会提示npm安装的插件命令不存在。

打开用户目录下的.bashrc文件:

$ vi ~/.bashrc

在尾部添加如下内容:

export PATH="/usr/local/nodejs/bin":$PATH

使用source 命令生效:

$ source ~/.bashrc

VSCode[可选]

这个可以不安装,如果不需要修改代码。

1.去官网下载安装包:vscode-x86_64.rpm

2.执行安装操作

$ rpm -ivh vscode-x86_64.rpm

3.测试安装是否成功。

$ code

如果能启动vscode则安装正确。

注:如果使用zip文件安装,跟.NET Core步骤一样。还需要安装unzip

需要安装C#扩展,在vscode中,按快捷键Ctrl + Shift + P,输入下面命令:

ext install csharp

创建控制台程序

步骤直接写在代码中。

$ #创建文件夹
$ mkdir ~/dotnetcore
$ mkdir ~/dotnetcore/ConsoleApp
$ cd ~/dotnetcore/ConsoleApp
$ #新建控制台程序
$ dotnet new
$ #还原nuget包
$ dotnet restore
$ #编译
$ dotnet build
$ #运行
$ dotnet run

实际操作图(示例):

ConsoleApp-1

ConsoleApp-2

创建ASP.NET程序

使用Yeoman脚手架创建。
如果不使用Yeoman创建,推荐阅读:ASP.NET Core 中文文档 第一章 入门

1.执行命令yo aspnet

新建asp.net项目

选择第三项,Web Application。

2.选择UI框架,这里选Bootstrap:

UI Framework

3.输入项目名称,可以默认:

输入项目名称

4.等待安装完成

等待安装完成

5.按照上面提示命令,依次执行

$ cd WebApplication
$ #还原nuget包
$ dotnet restore
$ #编译
$ dotnet build
$ #创建SQLite数据库
$ dotnet ef database update
$ #运行
$ dotnet run

运行

6.查看效果

测试效果图

 

文/seayxu(简书作者)
原文链接:http://www.jianshu.com/p/1f07d150f61e
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

.NET跨平台之旅:将示例站点升级至 .NET Core 1.1 Preview 1

今天微软发布了 .NET Core 1.1 Preview 1(详见 Announcing .NET Core 1.1 Preview 1 ),紧跟 .NET Core 前进的步伐,我们将示例站点 http://about.cnblogs.com/about/intro 升级到了 .NET Core 1.1 Preview 1 ,在这篇博文中分享一下升级过程。

部署示例站点的服务器是 Linux Ubuntu 14.04 ,已安装 .NET Core 1.0.1 ,安装位置是 /usr/share/dotnet 。首先从 https://github.com/dotnet/core/ 获取 Ubuntu 版的安装包下载地址:https://go.microsoft.com/fwlink/?LinkID=831488 ,然后运行下面的命令下载并安装:

wget -c https://go.microsoft.com/fwlink/?LinkID=831488 -O dotnet-dev-ubuntu-x64.1.0.0-preview2.1-003155.tar.gz
tar zxf dotnet-dev-ubuntu-x64.1.0.0-preview2.1-003155.tar.gz -C /usr/share/dotnet

然后运行 dotnet -v 命令确认一下安装是否成功:

# dotnet -v
.NET Command Line Tools (1.0.0-preview2-1-003155)

显示 1.0.0-preview2-1-003155 说明基于 .NET Core 1.1 Preview 1 的 SDK 安装成功。

接下来就是修改 project.json 文件,将 framework 与依赖的包包改为 .NET Core 1.1 Preview 1 对应的版本。比如,将 “frameworks” 中的 “netcoreapp1.0.1” 改为 “netcoreapp1.1” ,将 “dependencies” 中的 “Microsoft.NETCore.App” 版本号改为 “1.1.0-preview1-*”,”Microsoft.AspNetCore.Mvc” 的版本号改为 “1.1.0-preview1-*” 。。。

然后 dotnet restore 安装nuget包, dotnet run 试运行站点。

复制代码
# dotnet run
Project AboutUs (.NETCoreApp,Version=v1.1) will be compiled because expected outputs are missing
Compiling AboutUs for .NETCoreApp,Version=v1.1

EnyimMemcached Started.
Hosting environment: Production
Content root path: /data/AboutUs
Now listening on: http://*:8001
Application started. Press Ctrl+C to shut down.
复制代码

试运行成功之后,用 dotnet publish -c release 命令发布,然后将发布出来的文件复制到站点文件夹中,最后用 supervisorctl restart aboutus 命令重启站点,升级就完成了。

.NET Core全面扫盲贴

标签: .NETCore Asp.NETCore


1. 前言

.NET发行至今已经过了十四个年头。随着版本的不断迭代更新,.NET在Windows平台上的表现也是越来越好,可以说Windows平台上所有的应用类型.NET几乎都能完成。

只是成也Windows,败也Windows,这十四年来,除了部分“民间”版本,.NET一直没能在官方支持下摆脱Windows平台的局限,“开源”和“跨平台”这两个词语也是所有.NET开发者心中的痛楚。最终,.NET Core出现了,它让开发者们在官方和社区的支持走出了Windows,可以在macOS,Linux主流distributions上编写调试并部署.NET程序。

2. .NET Core 简介

2.1 .NET Core是什么

.NET Core是一个开源通用的开发框架,支持跨平台,即支持在Window,macOS,Linux等系统上的开发和部署,并且可以在硬件设备,云服务,和嵌入式/物联网方案中进行使用。.NET Core的源码放在GitHub上,由微软官方和社区共同支持。

它和传统的.NET Framework,属于“子集—超集”的关系,或者你也可以简单地认为它就是.NET Framework的跨平台版本(基于BCL的层面上看)。这是因为在当前版本中(1.0),.NET Core中的大部分核心代码都是从.NET Framework中继承重写的,包括Runtime和Libraries(如GC, JIT, 部分类型)。

吐槽:只能感谢微软《CLR via C#》不用白看呀,我之前看了得有小半年才看完

现在的.NET Core 1.0版本是一个很小的核心,APIs和工具也并不完整,但是随着.Net Core的不断完善,补充的Apis和创新也会一起整合到.NET Framework中。也就是说,.NET Core微软会同时更新.NET Framework和.NET Core,他们就像俩兄弟,共同努力致富(致谁的富?当然是.NET开发者们),实现所谓的天下大同,也就是.NET 标准2.0

这里不得不提到一个叫做.NET Standard Library概念。作为.NET平台APIs开发官方支持标准,它要求所有的.NET框架APIs都遵循向下兼容。比如说.NET Framwork 4.6支持.NET Standard Library 1.3,.NET Framwork 4.6.2框架支持.NET Standard Library 1.5,而.NET Core 1.0框架支持1.6标准。

最终展望如下:

2.2 .NET Core的组成

  • .NET Runtime
    CoreCLR。如之前所说,CoreCLR与.NET Framework的CLR并没有什么区别,进程管理,GC,JIT(RyuJIT 编译器)这些部分也都是一样的,只是针对服务器系统做了相应优化。现在CLR和CoreCLR也在进行同步更新,只是可以肯定的是,CoreCLR才是.NET的未来,CLR将作为兼容手段而存在
  • Framework Libraries,
    CoreFX。包括集合类,文件系统处理类,XML处理类,异步Task类等
  • SDK Tools 和 Language Compilers (SDK工具和编译器)
    CLI工具和Roslyn编译器。可以通过.NET Core SDK(.NET Core开发工具包)获取。
  • dotnet’app host
    用来选取并执行对应运行时,提供组件载入原则和启动.NET Core应用程序。SDK也是通过相同程序启动。

    Tips:是不是想起了MSCorEE.dll这个垫片,它同样承载着Windows平台上为.NET应用程序选择CLR版本的工作

2.3 .NET Core的特性

  • 跨平台
    可以在Windows,macOS,Linux上运行
  • 灵活的部署机制

1.Portable applications(便携式应用)
这种部署机制和传统的.NET Framework相似,只要目标平台上存在.NET Core Runtime即可。

2.Self-contained application(自宿主应用)
顾名思义,这种部署机制将应用和运行时共同打包,即便目标平台上没有安装.NET Core Runtime也能正常使用

第二种方式和.NET Native也是不一样的,仍然使用CoreCLR,而.NET Native使用CoreRT作为运行时,详细信息请见dotnet/corert

  • 命令行工具
    .NET程序所有的运行脚本都可以用命令行工具执行(cmd,bash)这里有几个常见的donnet命令
指令 帮助
dotnet new 产生新的基本 .NET 项目内容 (包含 project.json、Program.cs 以及 NuGet.config
dotnet restore 还原所参考的 NuGet 包
dotnet build 建造 .NET 项目
dotnet publish 产生可发行的 .NET 项目 (包含所属的 Runtime)
dotnet run 编译与立即运行 .NET 项目 (较适用于 exe 型项目)
dotnet repl 引导交互式的对话
dotnet pack 将项目的产出封装成 NuGet 包
  • 兼容性
    通过.NET Standard Library与.NET Framework,Xamarin,Mono兼容
  • 开源
    .NET Core从属于.NET基金会,由微软进行官方支持。使用MIT和Apache 2开源协议,文档协议遵循CC-BY

2.4 开发语言

.NET Core 1.0版本中支持的变成语言仅有C#(F#和VB暂未实现),这里还要提到一个开源的语言编译器Roslyn,它负责将代码编译成我们熟悉的IL语言,然后再借由AOT或JIT编译器编译成机器熟悉的机器语言。

3. Get Started

以下内容演示在Windows10和CentOS 7.2下的命令行生成和发布demo

3.1 Win 10

3.1.1 安装.NET Core SDK.NET Core Runtime

.NET Core SDK = 使用.NET Core开发应用.NET Core Runtime 和 SDK+CLI 工具

3.1.2 简单的运行结果

打开cmd,依次输入mkdir .project(创建目录),cd .\.project(进入目录),dotnet new(新建初始项目),dotnet restore(还原依赖),dotnet run(运行)即可运行第一个Hello World程序

3.2 CentOS 7.2(本地Hyper-V)

3.2.1 安装和运行

详情请见:.NET Core in CentOS,大致命令如下

sudo yum install libunwind libicu   #安装libunwind,libicu包

curl -sSL -o dotnet.tar.gz https://go.microsoft.com/fwlink/?LinkID=809131 #下载dotnet-dev-centos-x64.1.0.0-preview2-003121.tar文件,有时会因为网络问题下载较慢,耐心等待即可,当然也可以手动下载后放到目录下。

sudo mkdir -p /opt/dotnet && sudo tar zxf dotnet.tar.gz -C /opt/dotnet #创建目录并解压已下载文件
sudo ln -s /opt/dotnet/dotnet /usr/local/bin #将目录链接到$PATH下,否则dotnet命令无法识别

mkdir hwapp
cd hwapp

dotnet new #创建默认.NET Core应用
dotnet restore #还原依赖包
dotnet run #运行,结果将显示Hello World!

第六行命令后可使用dotnet –info查看是否链接成功,显示如下

.NET Command Line Tools (1.0.0-preview2-003121)

Product Information:
 Version:            1.0.0-preview2-003121
 Commit SHA-1 hash:  1e9d529bc5

Runtime Environment:
 OS Name:     centos
 OS Version:  7
 OS Platform: Linux
 RID:         centos.7-x64

以上步骤在.NET Core官方网站都可以找到,可以看到应用在经过简单的dotnet new, dotnet restore, dotnet run命令后就跑起来了,但是这其实是类似于开发环境中的调试运行,而且win上new的应用此时也不能直接跨平台到Linux下运行,所以我们又得提到dotnet publish命令了

3.2.2 Self-contained applications 发布

(1) 修改project.json文件

我们现在win10下按照步骤new一个新的HW控制台应用self,按照官方文档要求,我们需要用以下内容替换原来project.json文件(删除”type”: “platform”,并增加runtimes节点)

{
  "version": "1.0.0-*",
  "buildOptions": {
    "debugType": "portable",
    "emitEntryPoint": true
  },
  "dependencies": {},
  "frameworks": {
    "netcoreapp1.0": {
      "dependencies": {
        "Microsoft.NETCore.App": {
          "version": "1.0.0"
        }
      },
      "imports": "dnxcore50"
    }
  },
  "runtimes":{
   "win10-x64":{}, #win10平台
   "centos.7-x64":{} #centos7.2平台
  }
}

(2) 执行restore和publish操作

之后执行dotnet restore指令,针对平台进行还原操作。该步操作耗时较久,虽然只是两个平台,第一次也花了较长时间。紧接着进行进行dotnet publish -r centos.7-x64 -c release

dotnet publish指令详见dotnet-publish – Packs the application and all of its dependencies into a folder getting it ready for publishing

(3) 在Linux平台上运行

上述操作后,我们只需要将发布后的文件夹(bin/release/netcoreapp1.0/centos7-x64/publish,包含self.exe)上传到Linux root目录下project(新建)文件夹中, 在Shell中输入指令

修改可执行权限后,即可成功运行“Hello World”,这就是我们“Self-contained applications”方式的部署方法

4. 总结

4.1 尾声

至此,.NET Core的学习便告一段落了,以上内容简单介绍了.NET Core的组成和特性,同时也在Windows和Linux系统上通过两种不同的部署方式成功运行了示例。相比较之前的.NET Framework傻瓜式部署,.NET Core的新奇真是让我眼前一亮。接下来,我也会将.NET Core和ASP.NET Core的学习记录下来,欢迎指正。

4.2 相关学习资料

5. 参考资料

开源Asp.Net Core小型社区系统

源码地址:Github

前言

盼星星盼月亮,Asp.Net Core终于发布啦!!

Asp.Net发布时我还在上初中,没有赶上。但是Asp.Net Core我从beta版本便一直关注。最初项目名叫Asp.Net VNext,然后改名叫Asp.Net 5。最煎熬的是RC1发布后,官方继续发布了改名RC2延期的通告。这期间我已经做了一些demo项目,但是由于beta到RC2之间涉及到大量API的改动,包括dnx->dotnet cli,包括各种命名空间和工具名称的改动等等,因此这部分demo都已删掉。5月份,Github Asp.Net Core更新路线图,确定RC2于5月中旬发布,同时确定RC2会作为最终发布的版本基础。那段时间我疯狂的关注着Github,即使在国外度蜜月,也会在晚上蹭Wifi关注着动态(这里提一下,有空看一下各个项目的issue,可以积累很多知识。同时很多小道消息都可以在members的回复中看出来)。好在接下来没有再次跳票,开源、跨平台、高性能的Asp.Net Core终于来啦!

小型社区系统

首先看下项目截图:

项目布局参考了CNodeJS 前端采用了Bootstrap,数据库访问用了EntityFramework Core,同时自己用Middleware实现了一个简单的身份认证功能

目前完成的功能:注册,登录,发帖,回帖,收藏,置顶,精华等功能。

项目地址:GitHub

如何运行:

1. 首先安装基础环境

2. clone或者下载项目,先设置连接字符串,然后还原数据库,最后运行即可

详细流程请点击上方连接查看项目主页

开发感受

1. 对于初学者,Asp.Net Core的入门门槛还是挺高的。

没有了WebForm,无法再拖拖控件就完成一个Hello World Page。

MVC和WebApi合二为一,那么至少对这2种技术应该有些基础了解。

处理HTTP请求从传统的Handler、Page变成了Middleware,如果不熟悉nodejs(express)的话又是个新鲜事物。

搭建一个web项目,首先就用到依赖注入容器,又有多少初学者接触过依赖注入呢?

2. 对于.Net开发者,还有很多东西要学。

新的TagHelper和ViewComponent,看来是要培养起面向组建编程的习惯了。

前端可以方便的集成bower, gulp等,那么NodeJS, npm, bower, gulp等等都是需要学的。

project.json里面的东西涉及到编译、发布、部署等等一系列配置,再结合dotnet命令,可以很简单的实现自动化,想起来是不是很激动?

新的EntityFramework Core Migration,直接基于命令生成和更新数据库,看起来是不是很酷?

整个AspNet Core Framework都开源了,基础源码难道不想去看看?

最最最重要的是跨平台!现在我们再也没法逃避Linux啦,大家赶紧装虚拟机,从最基本的ls开始linux之旅吧!

3. 对于Asp.Net Core,还有很长的路要走

性能:从官方的性能测试看出,目前Asp.Net Core可以超过NodeJS,但是比JAVA的Netty还是差了太多(这个测试看起来还是RC1的版本)。首先我觉得大家应该培养起异步编程的好习惯,这篇文章讲述了异步编程是如何提升并发效率的;其次只能寄希望于微软继续提升性能,或者有第三方高性能web框架出现。

框架:Asp.Net Core从出生起就声明了只是.Net Framework的子集,但是部分基础框架的缺失还是带来了很大的不便。最最不方便的就是System.Drawing。

第三方库:作为一个婴儿,Asp.Net Core才刚出生,又经历跳票,因此这方面资源少得可怜。几大热门项目:Dapper,AutoMapper,Nlog等倒是很早就开始支持了。

开发人员流失:谁敢说身边没有从.Net转Java,转Android,转IOS的??

后记

昨天加班到3点,今天早上继续上班,头都是晕的。个人技术不好,见解不够,以上都是自己的想法,希望大家多多交流,一起为.Net社区出力!!

imac无法进入

你也可以开机的时候按住苹果+S键进入单用户磁盘修复命令模式,输入sh回车,再输入/sbin/fsck -fy回车(看清楚-fy前面是有个空格的),开始修复磁盘, 直到出现 The volume (name这里就是你的磁盘的名) appears to be OK 提示,表示修复完成,如果没有出现就在修复一次直到出现The volume (name) appears to be OK 提示后表示修复成功,最后输入reboot重启进入系统,就可以了,如果一直都有错误,可以考虑更换硬盘了。
sh
/sbin/fsck -fy
………………. (就开始检查修复磁盘错误 )
The volume (name) appears to be OK(表示修复成功)
如果没有出现上面的,就再输入一次
/sbin/fsck -fy
直到The volume (name) appears to be OK(表示修复成功)
reboot(重启进入系统就可以分区了)

2017 年你应该学习的编程语言、框架和工具

作者:IT程序狮
链接:https://zhuanlan.zhihu.com/p/24369470
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

在过去的一年里,软件开发行业继续大踏步地向前迈进。回顾 2016 年,我们看到了更多新兴的流行语言、框架和工具,它们改变着我们的工作方式,让我们看到更多的可能。但在这个行业,紧随潮流是很难的。所以在每年年底,我们都会给你提供一些建议,它涉及什么是最重要的,以及你在未来一年中应该学习什么。

大趋势

渐进式 Web Apps

在 2016 年里,我们见证了 Progressive Web App 概念的蓬勃兴起。它意味着 Web 应用程序可以离线工作,并能提供原生移动应用的体验。它们可以添加到你的智能设备的主屏幕上,甚至可以给你发送推送通知,从而弥补与原生移动应用程序的差距。我们认为,在 2017 年,渐进式 Web Apps 将变得更加重要,也值得我们去探究。在这里查看相关概述

聊天机器人

从运行聊天机器人的平台到构建其的框架,现在每个人都在谈论它。而社区里也正忙于此活动。(阅读我们的介绍)机器人是一款新兴的移动应用程序,它让我们感到兴奋。如果你快点的话,还可以赶得上这波浪潮。然而一旦新鲜感消失,那么它只会承担一些无聊的角色,例如自动化的客服支持。但是,相信我们可以实现梦想。

前端框架的合并

在 JavaScript 社区,随着令人难以置信的框架和工具的混合,每周都会出现新的东西。直到最近,人们希望旧工具将被新工具所取代,但这不是 2016 年我们所想看到的。相反,我们看到了流行框架交换的想法,以及纳入新诞生框架中的创新元素。所以在 2017 年,你该选择哪个 JS 框架无关紧要,因为它们的功能大多是可以比较的。

云端

就目前的形势看,众多的公司与开发者们都在积极地拥抱“云”。云是可根据不同的需求,并通过控制面板来完全配置的虚拟化计算机基础设施。目前三大云提供商为亚马逊 AWS、Google Cloud 和 微软 Azure. 由于它们的竞争价格一直在下跌,使得小公司和个人开发者也可以将云纳入其预算中,所以熟悉云工作流程将是 2017 年的一笔不错的投资。

机器学习

机器学习(ML)在去年一年中呈现爆炸式的增长。三月份 AlphaGo 与李世石的精彩对决,也让它成为了焦点。从原始数据中学习的智能计算机系统,正在改变我们与移动设备的交互方式。看样子,机器学习将在 2017 年成为更大的影响因素。

编程语言

JavaScript 继续迈着令人难以置信的创新步伐在前进。由于 Web 浏览器的快速发布计划,JS 的标准定为了每年更新。故“ES2017”预计将在 2017 年中期完成,它也将带来 JS 开发者梦寐以求的新特性——用于处理异步函数的аsync/аwait。同时要感谢 Babel ,因为你现在可以在每个浏览器中编写 ES2017 了。

TypeScript 2.1 于 2016 年年底发布,它将为旧浏览器带来Async/Await异步解决方案,并改进了类型推断。TypeScript 是一种编译为纯 JavaScript 的静态类型语言。它增强了经典的 OOP 模型和可选的静态类型,使大代码库更易于维护。同时,它也是编写 Angular 2 应用程序的首选语言,我们建议你可以尝试下。 这是关于它的快速入门指南

C#7.0 预计在 2017 年发布,作为一门优秀的编程语言,它也将得到更大的改进。当微软推出开源的 Visual Studio 代码编辑器和 .Net Core 时,这一举动让众人都感到惊讶万分。它们不仅可以在 Linux、Windows 和 macOS 操作系统中运行,而且你可以在 C# 中编写快速、高效的应用程序(在这里阅读更多)。同时,这两种工具也都形成了充满活力的社区。相信,它们将在 2017 年会给我们带来更多的惊喜。

Python 3.6 版本将于 12 月发布。它正在巩固自身在开发人员、IT 专业人员和科学家在脚本语言选择中的地位。它适用于自动化、Web开发、机器学习和科学计算。虽然 Python 2.X 与 3.X 版本的割裂,对于社区来说是一个长达数年的斗争,但是就目前而言,你可以自信地选择 Python 3 并享受完整的库支持。而对于那些需要额外性能的朋友,建议你们看看 PyPy,一个可启用 Python 运行时 JIT 的替代品。

Ruby 2.3 已在今年早些时候发布了,并带来了一些性能上的改进。同时,Ruby 也是学习通用脚本语言的一个好选择,但是只有当它和 Rails 相配合的时候才能发挥出其最大的功效。伴随 Ruby 3×3 计划的宣布,也促使了即将到来的 Ruby 3 版本比当前版本的运行速度快 3 倍。而你也可以在更多的情景中,打开使用 Ruby 的大门。

PHP 7.1 版本已在 12 月发布,并对该语言进行了小范围的增强。这个版本基于了去年 7.0 版本主要性能的改进,将 PHP 转变为构建 Web 应用程序的快速平台。如果你打算学习,我们推荐你看看 PHP 之道中的最佳实践。

Java 9 预计在 2017 年发布,它将带来一些备受开发者们所欢迎的新功能,例如评估代码的 repl、HTTP 2.0 的支持以及一些新的 API . 对于有才能的 Java 开发人员和广泛使用该语言进行项目研发的人来说,他们对这些新特性是有强烈需求的。如果 Java 不是你的“菜”,这里还有一些基于 JVM 的编程语言,像 KotlinScala,你也可以了解下。

Swift 3 已经在今年早些时候发布了。简化 iOS 和 MacOS 上应用程序的开发,是苹果公司对现代编程语言的愿景。由于 Swift 是开源的,所以也涌现了大量的社区。Swift 4 计划于 2017 年发布,此版本将会改进语言并引入服务器 API,致力使其成为编写 Web 应用程序和后端的不错选择。

如果你在寻找一些让你感到兴奋的东西,你可以尝试下 CrystalElixir。它们都拥有类似与 Ruby 的友好语法以及卓越的性能,或者你也可以看看类似于 HaskellClojure 这类函数式语言。另外两种快速编程语言,我们推荐给你 RustGo 语言。

挑一个或多个学习: JS (ES2017)、TypeScript、C#、Python、Ruby、PHP7、Java/Kotlin/Scala.

前端

近期 Web 平台取得了两个重大的进展:Web Assembly 字节码技术和 Service Workers 技术。它们打开了快速、高效的 Web 应用程序的大门,并且有效的弥补了编译本地应用上的差距。Service Workers 是针对渐进式 Web App 的启动技术,它为 Web 平台提供了通知上的支持,将来也会有更多的 API.

Angular.js 2 在今年也已经发布了。该框架由 Google 进行维护,受到了众多企业和大公司的青睐。它所具备众多的功能,也为从网络到桌面以及移动应用程序中编写任何东西成为了可能。而它的框架也是用 TypeScript 所编写的,这也是写应用程序推荐的编程语言。虽然学习它还需要阅读更多的内容,但我们认为在 2017 年学习 Angular 2 将是一个很不错的投资。

在今年我们也看到了 Vue.js2.0 版本的发布,它借鉴了 Angular,React 和 Ember 中好的想法,并且比前两个框架更轻量、更快速。我们建议你今年要试一试,你可以从我们的 Vue.js 教程开始。

Ember 是 JavaScript 框架的另一个不错的选择。它支持数据双向绑定,并能够自动更新模板、组件以及服务器端渲染。与其他竞争者相比,使用它的好处是它更加成熟与稳定,而其框架的重大更改频率之低,社区重视向后的兼容性,也使得此框架成为开发较长生命周期的应用程序的不二之选。

另外两个值得一提的框架是 AureliaReact。在过去的一年里 React 的生态系统变得越来越复杂,因此很难推荐给初学者。但经验丰富的开发者可以将库与 GraphQLRelayFluxImmutable.js 组合成一个全面完整的全栈解决方案。

没有提及 Bootstrap 的前端终归是不完整的。而 Bootstrap 4 目前也正处于 Alpha 阶段,预计在 2017 年发布。值得关注的变化是新的通用卡片组件和 Flexbox 网格(查看与常规网格的对比),这使得框架更加现代化,并且让用户使用它进行工作时更加得舒心。

SASSLESS 仍然是当前最流行的两种 CSS 预处理器。尽管 Vanilla CSS 已经实现了对变量的支持,但对 mixins、函数和代码组织上的支持,SASS 和 LESS 依然更胜一筹。如果您还没有了解它们,可以看看我们的 SASSLESS 快速入门指南。

挑一个或多个学习:Angular 2、Vue.js、Ember、Bootstrap、LESS/SASS

后端

后端有众多的选择,但所有的选择都取决于你对编程语言或特定性能需求的偏好上。Web 开发中的一个持续趋势是远离后端的业务逻辑,并将该层转换为由前端和移动应用程序使用的 API 上。但一个全栈的框架通常是能够更简单、快速的应用于开发,并且它仍然是 Web 应用程序最有效的选择。

Node.js 是在浏览器之外运行 JS 的主要方式。在今年,我们也看到了它发布了许多新的版本。除了提升了性能外,也添加了对整个 ES6 规范的覆盖。Node 具有构建快速 API、服务器、桌面应用程序甚至机器人的框架,同时它可以创建想象到的各种模块的庞大社区。这里有一些你可能想研究的框架:ExpressKoaNextNodal.

PHP 是一种拥有大量 Web 框架可供你选择的 Web 开发语言。由于其拥有出色的文档和功能,Laravel 已建成了一个活跃的社区。Zend Framework 发布了第 3 版,这标志着面向业务框架的巨大升级。在今年,我们也看到了 Symfony 发行了很多新的版本,使它成为了全栈解决方案中更好的选择。

对于 Ruby 来说,Rails 框架是首选的。Rails 5.0 版本已于今年发布,并为 Web Sockets、API 模型等方面提供了支持。对于小型应用程序而言,Sinatra 也是一个不错的选择,Sinatra 2.0版本预计在 2017 年发布。

Python 有着以 DjangoFlask 为组合的全栈/迷你型框架。Django 1.10 已在今年 8 月发布了,它为 Postgres 引入了全文搜索和一个重大修改的中间件层。

Java 的生态系统中,依旧有很多流行的 Web 框架可供你选择。PlaySpark 便是两个必备的选择,同时它们也可以与 Scala 一起使用。

对于编程爱好者来说,你还可以选择 Phoenix,它是用 Elixir 编写的,它试图成为一个具有卓越的性能,并能完整替代 Rails 功能的框架。如果 Elixir 是你想在 2017 年学习的语言之一,不妨尝试下 Phoenix .

学习其中之一:全栈后端框架、一个微框架

数据库

PostgreSQL 在今年已经发行了两个完整的版本——9.59.6.它们带来了我们从 MySQL 就开始期盼的 UPSERT (aka ON DUPLICATE KEY UPDATE)功能,以及更好的全文搜索和速度改进功能,这多亏了并行查询,更高效的复制、聚合、索引和排序。Postgres 适用于大规模、TB 级规模的数据集以及繁忙的 Web Apps,这些优化都是很受欢迎的。

MySQL 8.0 将是数据库的下一个主要版本。预计在 2017 年发布,它将给系统带来更多的改进。MySQL 仍然是最受欢迎的数据库管理系统,整个行业都受益于这些新的版本。

对于 NoSQL 的粉丝们,我们推荐 CouchDB。它是一个快速、可扩展的 JSON 存储系统,同时公开了一个 REST-ful HTTP API.此数据库易于使用,同时性能卓越。与 CouchDB 对应的是 PouchDB ,它可以完全在浏览器中工作,并且可以与 Couch 同步数据。所以你可以在离线应用程序上使用 PouchDB ,联网后它会自动同步数据。

Redis 是我们最喜欢的键-值存储型数据库。它体积小、快速并且有丰富的特性。作为 NoSQL 数据存储或进程消息和同步通道,你可以使用它作为智能分布式高速缓存系统的可替代方案。它提供了大量的数据结构可供选择,并且在即将到来的 4.0 版本中会有一个模块系统,并将改进复制功能。

学习其中之一:Postgres、MySQL、CouchDB、Redis.

工具

Yarn 是由 Facebook 开发的 Node.js 包管理器。它是对 npm 命令行工具的升级,并提供了更快速地安装,更好的安全性以及确定性的构建。它仍然使用 npm 包注册表作为其后端,因此您甚至可以访问同一个 JavaScript 模块的生态系统。Yarn 与 npm 使用的 package.json 格式是兼容的,区别在于前者能实现快速安装。

作为两个最受开发者欢迎的开源代码编辑器——Visual Studio CodeAtom ,在过去一年中,我们看到了它们进行了很多不可思议的创新。这两个项目都是使用 Web 技术构建的,社区中也吸引了大量的粉丝。编辑器具备高扩展,提供了诸如语法检查、linting 和重构工具的相关插件。

作为最流行的源代码版本控制系统,Git 当之无愧。虽然它无服务器,但你可以将计算机上的任何文件夹转换为存储库。如果你想共享代码,像 GitLabBitbucketGitHub 都是不错的选择。在 2017 年,我们建议你熟悉 git 命令行,因为它会比您想象的更加方便。

桌面应用程序依然没有消失。即使 Web App 变得越来越强大,有时你依然会需要强大的功能和 API,这是 Web 平台无法提供的。你可以使用诸如 ElectronNW.js 之类的工具,利用 Web 技术来创建桌面应用程序,同时你也可以完全访问操作系统和 npm 可用的广度模块。要了解这些工具的更多信息,请阅读有关 ElectronNW.js 的教程。

软件开发团队的最新趋势是让开发人员负责自己软件项目中的部署,也称为 DevOps.这能产生更快地发布和更迅速地修复生产中出现的问题。而具有运维经验的开发人员将得到公司的高度重视,因此从现在开始熟悉能够实现这一目标的技术,将对你来说是一个巨大的提升。我们推荐的工具是 AnsibleDocker 。同时,具备 Linux 命令行和基本系统管理技能,也将为你的职场生涯大大的加分。

尝试一个或多个学习:Yarn、Git、Visual Studio Code、Electron、Ansible、Docker.

技术

随着大型公司数据中心的关闭,并调整其整体的基础设施到云上,我们可以看到已经赢得了整个软件行业。目前三个主要的平台是 AWS, Google CloudAzure。这三大平台都有着强大的功能,同时不断地扩展其功能集,涉及虚拟机、数据库托管、机器学习服务等。由于价格的迅速下降,小公司和个人开发者也都可以接触到云。对于 2017 年,在云上部署一个业余项目将是一个很好的学习积累。

人工智能是 2016 年的流行词。语音识别和图像分类只是该技术在面向用户应用程序的两个部分,人工智能设备的性能达到甚至超越了人类的水平。当下众多的创业公司也将 AI 和机器学习应用到其新的领域,同时许多相关的开源项目也已经发布,例如谷歌的 Tensor Flow 和微软的 Cognitive Toolkit。机器学习是一个与数学非常相关的主题,对于刚刚开始的人,这里有全面的在线课程供你学习。

虚拟现实(VR)和增强现实(AR)已经存在了一段时间,而最终该技术已经成熟到足以提供引人注目的体验。Facebook(Oculus Rift),Google(Daydream)和 Microsoft(Windows Holographic)都有欢迎第三方开发者加入的虚拟现实平台。然而 VR 穿戴设备依然面临着艰巨的挑战。例如如何消除穿戴者恶心的感觉,以及脱离了游戏圈,又如何创造令人信服的使用案例。

挑一种学习:云部署、机器学习库、VR 开发

如果觉得文章不错,不妨点个赞。^_^

注:

  1. 若有翻译不当之处,还请大家多多指正,我会及时修改;
  2. 本文版权归原作者所有。如需转载译文,烦请注明出处,谢谢!

英文原文:The Languages, Frameworks and Tools You Should Learn in 2017
作者:Martin Angelov
译者:IT程序狮
译文源自:zhuanlan.zhihu.com/p/24

Delegates, Events, and Anonymous Methods 委托、事件与匿名方法

译者注:委托、事件和匿名方法等在C#编程中有广泛运用,也有很多资料和书籍对它们做过大量介绍,但在我接触的人群中仍有很多人对它们还不甚了解,甚至惧怕。我希望这篇博文能够把这些东西说清讲透,也希望有此需要的园友在阅读之后能够获得对它们的深刻理解,并在今后的编程生涯中熟练地运用它们。还希望这篇博文成为介绍委托的经典的技术文章。

本文的主体内容译自《Introducing Visual C# 2010》(Adam Freeman著,Apress出版)一书的第10章。在译文中用“译者注”对原文作了一些补充说明,以使读者对相关内容有更清醒的认识或更明确的概念。

这是一篇篇幅不短的文章,如能认真阅读,相信一定会有所收获,并从此不再惧怕!

Delegates are special types that encapsulate a method, similar to function pointers found in other programming languages. Delegates have a number of uses in C#, but you are most likely to encounter a special type of delegate—the event. Events make notifying interested parties simple and flexible, and I’ll explain the convention for their use later in the chapter.
委托是封装方法的特殊类型,它类似于其它编程语言中的函数指针。委托在C#中有大量运用,但你最可能遇到的是一种特殊的委托类型 — 事件。事件使得对相关部件的通知变得简单而灵活,本章后面将解释其使用约定。

I’ll also explain the Func and Action types that let you use delegates in a more convenient form and that are used extensively in some of the latest C# language features, such as parallel programming. We’ll finish up this chapter with a look at anonymous methods and lambda expressions, two C# features that let us implement delegates without having to define methods in our classes. Table 10-1 provides the summary for this chapter
我也会解释Func和Action类型,它们让你以更方便的形式使用委托,而且在一些最新的C#特性中也有广泛使用,如并行编程。本章最后考察匿名方法和lambda表达式,这是让我们不必在类中定义方法就可以使用委托的两个C#特性。表10-1提供了本章概要。

以上这两段文字告诉我们:委托是一种封装方法的特殊类型。事件是特殊形式的委托,用以实现对相关部件的通知。FuncAction是C#的两个特殊类型,使我们能够更方便地使用委托。匿名方法Lambda表达式是C#的两个特性,让我们不必定义方法就可以使用委托。 — 译者注

Using Delegates
使用委托

A delegate is a special C# type that represents a method signature. Methods are discussed in Chapter 9, and the signature is the combination of the return type and the type and order of the method parameters. Listing 10-1 contains an example of a delegate.
委托delegate)是表示方法签名的一种特殊的C#类型。方法在第9章讨论过,而方法签名是方法的返回类型和方法参数的类型及其顺序的组合。清单10-1是一个委托示例。

以上是委托的概念定义。很多人都知道委托是用来关联方法的,但未能强烈意识到,委托更主要的是定义和使用一种类型。既然是一种类型,委托的使用便与类的使用具有类似性。因此,关于委托的使用通常应当包含这样几个环节:定义委托、创建委托对象、委托对象实例化、执行或调用委托。

另外要特别注意的是,关于委托的名词是混淆的。委托定义、委托对象、委托实例、以及委托调用等,有时不作明确的区分,都笼统地叫做委托。因此,在委托的使用过程中,必须从概念上分清什么是委托定义、委托对象、委托实例、以及什么是执行或调用委托。 — 译者注

Listing 10-1. Defining a Delegate Type
清单10-1. 定义一个委托类型

public delegate int PerformCalc(int x, int y);

There are five parts to the delegate in Listing 10-1, and they are illustrated in Figure 10-1.
清单10-1所示的委托有五个部分,它们如图10-1所示。

IntrC10-11. Access Modifier — 访问修饰符 2. Delegate Keyword — delegate关键字 3. Result Type — 结果类型 4. Delegate Name — 委托名 5. Parameters — 参数

Figure 10-1. The anatomy of a delegate
图10-1. 一个委托的剖析

The first two parts of a delegate are simple. First, all delegates require the delegate keyword. Second, delegates, like all types, can have access modifiers. See Chapter 6 for a description of how these modifiers apply to classes; they have the same effect on delegates.
委托的前两个部分很简单。首先,所有委托都需要delegate关键字。其次,像所有类型一样,委托可以有访问修饰符。参见第6章如何把这些修饰符运用于类的描述,它们在委托上有同样的效果。

The delegate name is the name by which we will refer to the type we have created. This is equivalent to the class name. The name of the delegate type in Listing 10-1 is PerformCalc.
委托名是用来指向已创建的这个类型的名称。它等同于类名。清单10-1中的委托类型名是PerformCalc。

The remaining parts of the delegate specify the kind of method that instances of this delegate can represent. In Listing 10-1, instances of the delegate can represent methods that return an int and that have two int parameters.
该委托的其余部分指明了这个委托的实例可以代表的方法的种类。在清单10-1中,委托的实例可以代表返回一个int(整数)且有两个int参数的所有方法。

As we look at each part of the delegate in Listing 10-1, it is important to bear in mind that when we define a new delegate, we are defining a new type. What we are saying is, “Here is a new type that can be used to refer to a specific kind of method.” Delegates can be hard to understand, and if you find yourself getting lost in this chapter, you should come back to the previous sentence. You can define a new delegate type in the same places as you can create a new class—in a namespace, class, or struct. Once we have defined a new delegate type, we can create an instance of it and initialize it with a value. Listing 10-2 contains a demonstration.
在我们考察清单10-1中委托的各个部分时,重要的是记住:定义一个新委托时,实际是在定义一个新类型。就好像在说:“这是一个新类型,它可以用来指向一类特定的方法”。委托可能难以理解,但如果在本章中发现自己迷失了方向,你应该回想上面这句话。就像可以定义一个新类一样,你可以在定义类的那些地方定义一个新的委托类型 — 在命名空间、类、或结构中。一旦定义了一个新的委托类型,就可以创建它的实例,并用一个值对它初始化。清单10-2是一个演示。

Listing 10-2. Defining a Delegate Field
清单10-2. 定义一个委托字段

// 定义一个委托
public delegate int PerformCalc(int x, int y);
class Calculator {
    // 委托类型字段,用以创建委托对象
    PerformCalc perfCalc;
    // 无访问修饰符的字段意为private(私有)
    // 构造器
    public Calculator() {
        // 实例化委托对象。
        // 对委托对象进行实例化的办法是,用一个方法名对委托对象进行赋值。
        // 于是,以下语句的含义为,perfCalc委托是对CalculateProduct方法的引用
        perfCalc = CalculateProduct;
    }
    // 属性,用以暴露委托对象
    public PerformCalc CalcDelegate {
        get { return perfCalc; }
    }
    // 方法,与委托类型具有相同的方法签名,用以对委托对象实例化
    private int CalculateProduct(int num1, int num2) {
        return num1 * num2;
    }
}

The Calculator class in Listing 10-2 has a field called perfCalc that is of the type of delegate we defined in Listing 10-1. This has created a field that can be used to represent a method that matches the delegate, in other words, a method that returns an int and has two int parameters. The Calculator class contains a method called CalculateProduct that matches that description, and in the Calculator constructor, I assign a value to the delegate field by using the name of the matching method. The definition of the field and the assignment of a value are shown in bold.
清单10-2中的Calculator类有一个叫做perfCalc的字段,它的类型是清单10-1中所定义的委托类型。它创建了一个字段,可以用来表示与委托匹配的方法,即,一个返回int并有两个int参数的方法。Calculator类有一个叫做CalculateProduct的方法与这个描述相匹配,而且在Calculator构造器中,通过使用这个匹配的方法名给这个委托字段赋了一个值。这个字段的定义和赋值以黑体显示。

The Calculator class in Listing 10-2 also contains a public property that returns an instance of the delegate type. The accessor in the property returns the value assigned to the delegate field.
清单10-2中的Calculator类还有一个public属性,它返回委托类型的一个实例。这个属性的访问器(指属性的getter块 — 译者注)返回赋给委托字段的值。

Now we have a new delegate type, PerformCalc, instances of it can be used to represent methods that return an int and that have two int parameters. We have a Calculator class that has a private field of the PerformCalc type and that has been assigned the CalculateMethod and a public property that returns the value of the delegate field. Listing 10-3 demonstrates how to use the delegate.
现在,我们有了一个新的委托类型PerformCalc。它的实例可以用来表示返回一个int并有两个int参数的方法。有一个Calculator类,它有一个PerformCalc类型的private(私有)字段,并把CalculateMethod(应当是CalculateProduct — 译者注)赋给了它。还有一个public属性,它返回该委托字段的值。清单10-3演示了如何使用这个委托。

Listing 10-3. Using a Delegate Obtained Through a Property
清单10-3. 使用通过属性获得的委托

class Listing_03 {
    static void Main(string[] args) {
        Calculator calc = new Calculator();
        // get the delegate
        // 获取委托
        PerformCalc del = calc.CalcDelegate; 
        // invoke the delegate to get a result
        // 调用该委托以获得结果
        int result = del(10, 20); 
        // print out the result
        // 打印结果
        Console.WriteLine("Result: {0}", result); 
        // wait for input before exiting
        // 退出之前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

A new instance of the Calculator class is created, and the CalcDelegate property is used to assign a value to a local variable of the PerformCalc delegate type; this means that the del variable contains a reference to the CalculateProduct method in the Calculator object. I invoke the delegate with the following statement:
上述代码创建了Calculator类的一个新实例,用CalcDelegate属性把一个值赋给了PerformCalc委托类型的一个局部变量,这意味着del变量是对Calculator对象中CalculateProduct方法的引用。用以下语句调用这个委托:

int result = del(10, 20);

This statement passes the parameters 10 and 20 to the method assigned to the del variable, which means that the CalculateProduct method in the Calculator object is called. The result from the delegated method is assigned to the local result variable, just as would happen with a regular method call.
这条语句把参数10和20传递给赋给del变量的方法,这意味着调用Calculator对象中的CalculateProduct方法。从这个委托方法而来的结果被赋值给局部变量result,这就像调用一个常规方法所发生的情况一样。(可见,调用委托就像执行常规的方法一样 — 译者注)

总结上述委托编程过程,可以从概念上形成以下几个名词:

  1. 委托定义:委托定义的作用是创建一个能够封装一类方法的类型。
  2. 委托字段:在委托编程中需要有一个委托字段,该字段可以用来创建委托对象。也可以把这个委托字段看成为是一个委托类型的变量(简称为委托变量)。于是,随时可以用一个方法对这个委托变量进行赋值。
  3. 委托实例:这是对委托字段的实例化,以形成委托对象。实例化的办法是用一个具有相同签名的方法名对委托变量进行赋值。实例化的作用是把委托字段与实例化方法关联在一起,形成委托对象。因此,无论何时,调用委托就是执行其实例化方法。
  4. 委托属性:可以用委托属性对外暴露委托对象。
  5. 执行/调用委托:执行或调用委托实际上是执行委托对象的实例化方法,执行/调用委托与执行规则的方法一样(送入方法参数、接收返回结果)。 — 译者注

The reason that I created a new Calculator object is that I wanted to delegate an instance method, and you can do that only once you have an instance to work with. If you want to delegate a static method, then you can do so by using the class name; you can see an example of delegating a static method in Listing 10-4 later in the chapter.
创建一个新的Calculator对象的原因是想委托一个实例方法,而且你只要这样做一次,就有了一个用来进行工作的实例。如果想委托一个静态方法,那么可以用这个类名来做,本章稍后可以看到委托一个静态方法的示例。

You can also use generic types with delegates. If we wanted a generic version of the delegate type defined in Listing 10-1, we could define the following:
也可以使用委托的泛型类型。如果想定义类似于清单10-1所示的委托类型的泛型类型,可以这样定义:

public delegate T PerformCalc<T>(T x, T y);

Then to create the delegate field in the Calculator class, we would use the following:
然后创建Calculator类中的委托字段,像这样:

class Calculator {
    PerformCalc<int> perfCalc;
    ...

In this way, we can define a general-purpose delegate and provide the type parameters as needed when we define the delegate fields or variables. Generic types are described in Chapter 15.
通过这种方式,我们可以定义一个通用目的的委托,并在定义委托字段或变量时,提供必要的类型参数。泛型类型在第15章描述。

There are a couple of points to note about all the examples so far. The first is that we passed around a method as we would a regular variable, invoking it only when we needed. The other is that the class that called the delegated method had no direct relationship to the method being invoked. We delegated a private method hidden away inside the Calculator class that the Listing_03 class wouldn’t otherwise be able to access.
对于上述这些例子,有两点需要注意。第一点是我们像传递常规的变量一样来传递一个方法,只在需要的时候调用它(可见,我们可以像对变量赋值一样,把一个方法赋给一个委托,还可以像变量一样对委托进行传递。于是,通过委托使得对方法的传递变得十分灵活 — 译者注)。另一点是调用委托方法的类与被请求的方法没有直接的关系(从而实现了方法的使用与方法的具体实现之间的分离。于是,方法体可以被重构或被替换 — 译者注)。我们委托了在Calculator类中被隐藏起来的一个私有方法,这个方法因而在Listing_03类中是不能访问的。

The examples have shown how to use delegates but didn’t really explain why you might find them useful. In the following sections, I’ll show you ways to use delegates that simplify common coding patterns and demonstrate some useful C# features.
这些例子演示了如何使用委托,但并未解释为什么它们是有用的。在以下小节中,将演示使用委托的方式,以达到简化常规的编码模式,并演示一些有用的C#特性。

Using Delegates for Callbacks
用委托进行回调

You can use delegates to create callbacks, where one object is notified when something of interest happens in another object. Listing 10-4 contains an example of a simple callback to notify an interested class when a calculation is performed.
可以用委托来创建回调callback),即,当在一个对象中发生某个关心的事情时,通知另一个对象。清单10-4是一个简单回调的示例,以便在计算完成时通知一个感兴趣的类。

Listing 10-4. Using a Delegate for a Callback
清单10-4. 使用委托进行回调

using System; 
// 委托字义
delegate void NotifyCalculation(int x, int y, int result); 
class Calculator {
    // 委托字段
    NotifyCalculation calcListener; 
    // 构造器,用参数给委托字段赋值(委托实例化) — 译者注
    public Calculator(NotifyCalculation listener) {
        calcListener = listener;
    }
    // 方法,计算两数乘积,在其中通过委托向另一对象发送通知 — 译者注
    public int CalculateProduct(int num1, int num2) {
        // perform the calculation
        // 执行计算
        int result = num1 * num2; 
        // notify the delegate that we have performed a calc
        // 向委托发送通知,说明已经执行了一个计算
        calcListener(num1, num2, result); 
        // return the result
        // 返回结果
        return result;
    }
}
// 接收通知的类 — 译者注
class CalculationListener {
    // 与委托的方法签名对应的方法。
    // 注意,这是一个静态方法,因此不需要在这个类中创建委托字段并实例化,
    // 可以直接通过类名返回该方法的一个实例,如Calculation.CalculationPrinter — 译者注
    public static void CalculationPrinter(int x, int y, int result) {
        Console.WriteLine("Calculation Notification: {0} x {1} = {2}",
                        x, y, result);
    }
}
// 主程序
class Listing_04 {
    static void Main(string[] args) {
        // create a new Calculator, passing in the printer method
        // 创建一个新的Calculator,在其中传递printer方法
        // 于是把一个方法对象注入到了Calculator对象的calc之中形成了一个委托实例 — 译者注
        Calculator calc = new Calculator(CalculationListener.CalculationPrinter);
        // perform some calculations
        // 执行一些计算
        // 注意,每执行一次计算,在计算体里都会回调委托向CalculationListener发送一个通知
        // 通知的作用导致执行CalculationListener中的printer方法 — 译者注
        calc.CalculateProduct(10, 20);
        calc.CalculateProduct(2, 3);
        calc.CalculateProduct(20, 1); 
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

回调是事件的基础,请通过上述清单真正理解委托回调:首先把一个接收通知的方法注入到一个对象(通知源对象,也即发出通知的对象)之中(上例中的黑体语句),以形成通知源中的委托实例。然后在通知源对象的某个方法中,通过执行这个委托实例,以便回过头来调用(回调)这个被委托方法,从而实现向目标对象(或目标方法)发送通知的目的。因此,所谓委托回调是指通过委托实现回调 — 译者注

The delegate type in this example is called NotifyCalculation and has parameters for the two numbers that have been used for the calculation and the result that was computed. The Calculator class in this example has a constructor argument that takes an instance of the delegate type, which is then invoked inside the CalculateProduct method.
此例中的委托类型叫做NotifyCalculation,其参数是用于计算的两个数字和计算所得的结果。这个例子中的Calculator类有一个构造器参数,它是委托类型的一个实例,在CalculatreProduct方法中对这个实例进行调用。

The Listing_04 class creates a new instance of Calculator and passes a reference to the static CalculationListener.CalculationPrinter method as the constructor parameter. The delegate is called each time the Calculator.CalculateProduct method is invoked, printing out a notification of the calculation that has been performed. Compiling and running the code in Listing 10-4 produces the following result:
Listing_04类创建了一个新的Calculator实例,并把对静态方法CalculationListener.CalculationPrinter的引用作为构造器参数进行传递。每次请求Calculator.CalculateProduct方法都会调用这个委托,打印出一个计算已被执行的通知,编译并运行清单10-4中的代码产生以下结果:

Calculation Notification: 10 x 20 = 200
Calculation Notification: 2 x 3 = 6
Calculation Notification: 20 x 1 = 20
Press enter to finish(按回车键结束)

Using delegates in callbacks means that the source of the notifications doesn’t need to know anything about the class that receives them, allowing the notification receiver to be refactored or replaced without the source having to be modified at all. As you’ll see in the “Delegating Selectively” section later in the chapter, we can select a delegate at runtime, which provides us with even greater flexibility.
在回调中使用委托,意味着通知源不需要知道通知接收者的任何信息,这允许通知接受者被重构或被替换,而源根本不需要做任何修改。正如你将在稍后的“选择性委托”小节中所看到的,可以在运行时选择一个委托,这为我们提供了更大的灵活性。

Multicasting with Delegates
委托多播

When performing callbacks, you will often need to cater for multiple listeners, rather than the single listener shown in Listing 10-4. The delegate type uses custom + and – operators that let you combine several method references together into a single delegate and invoke them in one go, known as multicasting. Custom operators are discussed in Chapter 8. Listing 10-5 contains an example of a multicasting delegate callback.
在执行回调时,通常需要迎合多个侦听器(通知接收者 — 译者注),而不是如清单10-4所示的一个单一的侦听器。委托类型采用 + 和 – 操作符,允许你把对几个方法的引用结合在一起,形成一个单一的委托,并一次性地调用它们,这称为多播multicasting)(因此,多播是一次性调用委托,达到向多个目标发送通知的目的 — 译者注)。自定义操作符在第8章讨论过。清单10-5是一个多播委托回调的例子。

Listing 10-5. Using Delegate Multicasting
清单10-5. 使用委托多播

using System; 
// 委托定义
delegate void NotifyCalculation(int x, int y, int result); 
class Calculator {
    // 委托字段
    NotifyCalculation calcListener; 
    // 方法,增加委托
    public void AddListener(NotifyCalculation listener) {
        calcListener += listener;
    }
    // 方法,去除委托
    public void RemoveListener(NotifyCalculation listener) {
        calcListener -= listener;
    }
    // 方法,执行乘积计算
    public int CalculateProduct(int num1, int num2) {
        // perform the calculation
        // 执行计算
        int result = num1 * num2; 
        // notify the delegate that we have performed a calc
        // 通知委托,告知已执行了一个计算
        calcListener(num1, num2, result); 
        // return the result
        // 返回结果
        return result;
    }
}
// 侦听器类
class CalculationListener {
    // 字段,表示侦听器id
    private string idString; 
    // 构造器,设置侦听器id
    public CalculationListener(string id) {
        idString = id;
    }
    // 与委托签名对应的方法,调用委托时将执行此方法
    public void CalculationPrinter(int x, int y, int result) {
        Console.WriteLine("{0}: Notification: {1} x {2} = {3}",
                    idString, x, y, result);
    }
}
// 另一个侦听器类
class AlternateListener {
    public static void CalculationCallback(int x, int y, int result) {
        Console.WriteLine("Callback: {0} x {1} = {2}",
                    x, y, result);
    }
}
// 主程序
class Listing_05 {
    static void Main(string[] args) {
        // create a new Calculator
        // 创建一个新计算器
        Calculator calc = new Calculator();
        // create and add listeners
        // 创建并添加侦听器,这称为订阅通知 — 译者注
        calc.AddListener(new CalculationListener("List1").CalculationPrinter);
        calc.AddListener(new CalculationListener("List2").CalculationPrinter);
        calc.AddListener(AlternateListener.CalculationCallback); 
        // perform a calculation
        // 执行一个计算
        calc.CalculateProduct(10, 20); 
        // remove a listener
        // 移去一个侦听器,这称为退订通知 — 译者注
        calc.RemoveListener(AlternateListener.CalculationCallback); 
        // perform a calculation
        // 执行一个计算
        calc.CalculateProduct(10, 30); 
        // wait for input before exiting
        // 退出之前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

The Calculator class in this example defines two methods that register and unregister callback delegates using the += and -= operators. There are two classes that contain methods that match the delegate signature, CalculationListener and AlternateListener, and the Listing_05 class registers and unregisters the methods as delegates with the Calculator object. You can see that you use a multicast delegate just as you would a single delegate. Compiling and running the code in Listing 10-5 produces the following results:
此例中的Calculator类定义了两个方法,用 += 和 -= 操作符注册和注销回调委托。有两个类含有与委托签名匹配的方法,CalculationListener和AlternateListener,而Listing_05类以Calculator对象来注册和注销委托方法。可以看出,使用多播委托就像使用单个委托一样。编译并运行清单10-5代码会产生如下结果:

List1: Notification: 10 x 20 = 200
List2: Notification: 10 x 20 = 200
Callback: 10 x 20 = 200
List1: Notification: 10 x 30 = 300
List2: Notification: 10 x 30 = 300
Press enter to finish

可以通过以下描述体会委托多播的价值:假设我们要做一个多国语言翻译机,把一国文字翻译成多个国家的文字。于是可以把英-汉、英-日、英-俄、英-德等多个翻译方法组合成一个委托多播,然后通过一次性委托调用得到全部翻译结果 — 译者注

Delegating Selectively
选择性委托

One of the benefits of being able to pass delegates around as variables is to apply delegates selectively, such that we create a delegate that is tailored to a given situation. Listing 10-6 contains a simple example.
能够把委托像变量一样传递的一个好处是可以有选择地运用委托,这样,我们可以针对一个给定的情况来创建委托。清单10-6是一个简单的例子。

Listing 10-6. Creating Anonymous Delegates Based on Parameter Value
清单10-6. 创建基于参数值的匿名委托

using System; 
// 委托定义
delegate int PerformCalc(int x, int y); 
// 计算器类
class Calculator {
    public enum Modes {
        Normal,
        Iterative
    };
    public PerformCalc GetDelegate(Modes mode) {
        if (mode == Modes.Normal) {
            return CalculateNormally;
        } else {
            return CalculateIteratively;
        }
    }
    // 与委托签名一致的方法,乘积
    private int CalculateNormally(int x, int y) {
        return x * y;
    }
    // 与委托签名一致的方法,累加
    private int CalculateIteratively(int x, int y) {
        int result = 0;
        for (int i = 0; i < x; i++) {
            result += y;
        }
        return result;
    }
}
// 主程序
class Listing_06 {
    static void Main(string[] args) {
        // create a new Calculator
        // 创建一个新Calculator
        Calculator calc = new Calculator();
        // get a delegate
        // 获取一个委托
        PerformCalc del = calc.GetDelegate(Calculator.Modes.Normal); 
        // use the delegate
        // 使用该委托
        Console.WriteLine("Normal product: {0}", del(10, 20)); 
        // get a delegate
        // 获取一个委托
        del = calc.GetDelegate(Calculator.Modes.Iterative); 
        // use the delegate
        // 使用该委托
        Console.WriteLine("Iterative product: {0}", del(10, 20)); 
        // wait for input before exiting
        // 退出之前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

The Calculator class in Listing 10-6 has a GetDelegate method that returns an delegate based on the parameter value, selected from an enum. If the parameter is the Normal enum value, the delegate returned by the method uses the standard C# multiplication operator, but if the value is Iterative, then the method returns a delegate that performs multiplication as an iterative series of additions.
清单10-6中的Calculator类有一个GetDelegate方法,它返回一个基于参数值的、从一个enum(枚举)选择的委托。如果参数是Normal枚举值,由方法返回的委托使用标准的C#乘法运算符,但如果该值是Iterative,那么该方法返回另一种委托,它像反复累加那样执行乘法。

Interrogating Delegates
质询委托

The base type for all delegates is System.Delegate, and we can use the members of this class to find out which methods a delegate will invoke on our behalf. Listing 10-7 contains an example.
所有委托的基类型都是System.Delegate,而且可以使用该类的成员为我们找出一个委托所调用的方法。清单10-7是一个示例。

Listing 10-7. Interrogating Delegate Types
清单10-7. 质询委托类型

using System; 
delegate int PerformCalc(int x, int y); 
class Calculator {
    public int CalculateSum(int x, int y) {
        return x + y;
    }
}
class AlternateCalculator {
    public int CalculateProduct(int x, int y) {
        return x * y;
    }
}
class Listing_07 {
    static void Main(string[] args) {
        // create a delegate variable
        // 创建委托变量
        PerformCalc del = new Calculator().CalculateSum; 
        // combine with another method
        // 组合另一个方法
        del += new AlternateCalculator().CalculateProduct; 
        // Interrogate the delegate
        // 质询委托
        Delegate[] inlist = del.GetInvocationList();
        foreach (Delegate d in inlist) {
            Console.WriteLine("Target: {0}", d.Target);
            Console.WriteLine("Method: {0}", d.Method);
        }
        // wait for input before exiting
        // 退出之前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

The Listing_07 class creates a new delegate variable and uses it to combine methods from the Calculator and AlternateCalculator classes. I use the GetInvocationList method on the delegate variable, which returns an array of System.Delegate objects. I enumerate the contents of the array with a foreach loop and print out the value of the Target and Method properties for each Delegate object. (C# arrays are described in Chapter 13.) Table 10-2 describes the Target and Method properties.
Listing_07类创建了一个新的委托变量,并用它把Calculator和AlternateCalculator的方法组合起来。这里使用了委托变量的GetInvocationList方法,它返回一个System.Delegate对象的数组。用foreach循环枚举了该数组的内容,并打印出每个Delegate对象的Target和Method属性(C#数组在第13章描述)。表10-2描述了Target和Method属性。

Table 10-2. The System.Delegate Properties
表10-2. System.Delegate属性
Property
属性
Description
描述
Target Returns the object that the delegate will use to invoke the method or null if the delegate method is static.
返回委托用来调用方法的对象(目标方法的父类 — 译者注),若委托方法是static(静态的),则返回null。
Method Returns a System.Reflection.MethodInfo that describes the method that will be invoked by the delegate.
返回一个System.Reflection.MethodInfo(方法信息),它描述由委托调用的方法(目标方法本身的信息 — 译者注)。

Compiling and running the code in Listing 10-7 produces the following results:
编译并运行清单10-7代码将产生以下结果:

Target: Calculator
Method: Int32 CalculateSum(Int32, Int32)
Target: AlternateCalculator
Method: Int32 CalculateProduct(Int32, Int32)
Press enter to finish

从程序设计角度讲,选择性委托与质询委托属于两个相反方向的委托处理。利用选择性委托,我们可以根据某些条件有选择地创建委托;而通过质询委托,我们可以根据委托的一些信息有选择地去做某些事情。 — 译者注

Using Events
使用事件

Events are specialized delegates designed to simplify the callback model we saw earlier in the chapter. There can be a problem when you use a delegate type as a field, where one object interferes with another. Listing 10-8 contains a demonstration.
事件是设计用来简化前述回调模型的特殊委托(这是事件的定义,即,事件是执行回调(或发送通知)的一种特殊形式的委托 — 译者注)。在将委托用作为字段时,可能存在一个问题:一个对象会干扰另一个对象。清单10-8是一个演示。

Listing 10-8. One Type Modifying a Delegate Supplied by Another Type
清单10-8. 一个类型修改了由另一个类型提供的委托

using System; 
// 委托
delegate void NotifyCalculation(int x, int y, int result); 
// 类,计算器
class Calculator {
    // 委托字段
    public static NotifyCalculation CalculationPerformed; 
    // 方法,计算乘积
    public static int CalculateProduct(int num1, int num2) {
        // perform the calculation
        // 执行计算,计算乘积
        int result = num1 * num2; 
        // notify any listeners
        // 通知各个侦听器
        CalculationPerformed(num1, num2, result); 
        // return the result
        // 返回结果
        return result;
    }
}
// 类,一个邪恶的类
class NefariousClass {
    // 委托字段
    private NotifyCalculation orig; 
    // 构造器
    public NefariousClass() {
        // get a reference to the existing listener
        // 获取对现有侦听器的一个引用
        orig = Calculator.CalculationPerformed; 
        // set a new listener for Calculator
        // 对Calculator设置一个新的侦听器(将其指向了下面的那个扭曲的方法 — 译者注)
        Calculator.CalculationPerformed = HandleNotifyCalculation;
    }
    // 方法,一个扭曲的方法。在这个方法中做了两件事:
    // (1)调用原委托谎报结果;(2)自己打印出正确结果 — 译者注
    public void HandleNotifyCalculation(int x, int y, int result) {
        // lie to the original listener
        // 谎报原侦听器
        // 把不良信息送入了原委托方法,这里送入的是加法结果 — 译者注
        orig(x, y, x + y); 
        // print out the details of the real calculation
        // 打印实际计算的细节
        Console.WriteLine("NefariousClass: {0} x {1} = {2}",
                x, y, result);
    }
}
class Listing_08 {
    static void Main(string[] args) {
        // set a listener for the Calculator class
        // 设置对Calculator类的一个侦听器
        Calculator.CalculationPerformed = StandardHandleResult; 
        // create an instance of the Nefarious class
        // 创建Nefarious类的一个实例
        // 注意,在创建这个实例时,原侦听器已经被修改,
        // 而指向了NefariousClass类的HandleNotifyCalculation方法 — 译者注
        NefariousClass nc = new NefariousClass();
        // perform a calculation
        // 执行一个计算。
        // 此时在CalculateProduct方法中执行委托回调时,
        // 实际执行的已经是HandleNotifyCalculation方法了 — 译者注
        Calculator.CalculateProduct(20, 72); 
        // wait for input before exiting
        // 退出之前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
    private static void StandardHandleResult(int x, int y, int result) {
        Console.WriteLine("Good Class: {0} x {1} = {2}", x, y, result);
    }
}

In this example, the Listing_08 class contains a method that matches the delegate type used for the Calculator.CalculationPerformed field. This method is used to process callbacks from the Calculator class. The idea is that the anonymous method will be called each time a calculation is performed by the Calculator class, just as in some of the earlier examples.
在这个例子中,Listing_08类含有一个用于Calculator.CalculationPerformed字段的委托类型相匹配的方法。该方法用于处理Calculator类的回调。其思想是Calculator类每执行一次计算,都调用这个方法(原文中说这是一个匿名方法,其实不是 — 译者注),就像前面那些例子一样。

The Listing_08 class also creates a new NefariousClass object, and the fun begins. The NefariousClass constructor assigns a new method to the Calculator delegate field, displacing the original. This method then feeds bad information to the original value of the delegate field. If we compile and run the code in Listing 10-9, we get the following results:
Listing_08类也创建了一个新的NefariousClass对象,于是开玩笑的事便开始了。NefariousrClass构造器把一个新方法赋给了Calcualtor的委托字段,替换了原有方法。然后这个方法(指已接管的方法HandleNotifyCalculation — 译者注)把不良信息送入了原委托字段。如果编译并运行清单10-9,会得到以下结果:

Good Class: 20 x 72 = 92
NefariousClass: 20 x 72 = 1440
Press enter to finish

The original method is invoked each time a calculation is performed, but NefariousClass has inserted itself in the way and changes the details of the calculation that is reported. And the problems don’t stop there—because the delegate field is public, any object can invoke the delegate as it wants, simulating callbacks even though no calculation has been performed.
每次执行一个计算时都会请求原方法,但NefariousClass阻挡了这一过程,并修改了所报告的计算细节。问题不仅于此 — 由于委托字段是public的,因此任何对象都可以在需要时请求这个委托,甚至在计算尚未执行时就模拟回调。

This example demonstrates deliberate interference, but most of the problems with public delegate fields arise because of sloppy programming, where an object makes an explicit assignment using = to set the value of the delegate field instead of using += to combine delegates. You could take steps to avoid this problem—make the delegate field private and implement methods that enforce checks to ensure that objects are not interfering with each other—but the C# event feature takes care of this for you. Defining an event is just like defining a delegate field, with the addition of the event keyword. So, here’s our delegate field from Listing 10-9:
这个示例演示了有意的干扰,但public委托字段的大多数问题都是由于草率编程所引发的(所以,在委托编程中,委托字段的访问修饰是重要的 — 译者注),在这些场合中,对象都是用 = 来设置委托字段的值,以形成一个明确的赋值,而不是用 += 来组合委托。可以采取一些步骤来避免这类问题 — 让委托字段为private的,并实现强制检查,以确保对象不会相互干扰 — 不过,C#的事件可以帮你照顾这些事。定义事件就像定义委托字段一样,只要附加一个event关键字即可。因此,在以下是清单10-9的委托字段:

class Calculator {
    public static NotifyCalculation CalculationPerformed;

becomes the following:
把它改成下面这样:

class Calculator {
    public static event NotifyCalculation CalculationPerformed;

注意,event关键字是附加在委托字段上的,因此,可以把事件看成是一种特殊形式的委托对象,是经过event修饰的一种委托对象,它让委托对象的使用和操作受到一定的限制和约束。通过使用事件,便不会出现上述一个对象干扰另一个对象之类的问题 — 译者注

When you make a delegate into an event, the class that contains the field can still invoke the delegate and make full use of the type members and operators. Every other object can use only the += and -= operators to add and subtract methods. It is no longer possible to interrogate the delegate or replace the methods assigned to the delegate as I did in Listing 10-8. Although you can make any delegate into an event, there is strong convention in C# to use a certain pattern for events known as the EventHandler pattern, which makes it easier for others to use your events in their code. I describe the pattern in the following sections.
当你把一个委托变成一个事件时,包含该字段的类仍然能够调用委托,并充分利用类型成员和操作符。每一个其它对象都只能用 += 和 -= 操作符来添加和移去方法。这就不再能像清单10-8所做的那样去质询委托,或替换赋给委托的方法。虽然,你可以让任何委托成为事件,但C#有强行的约定,它要求使用事件的特定模式,这称为EventHandler模式,它使其他人更易于在他们的代码中使用你的事件(可见,EventHandler模式是一种编写事件的约定模式,该模式使大家能以统一的方式编写和使用事件,便于事件编程,也便于事件互用 — 译者注)。以下几小节描述这个模式。

注:由于事件是用来实现委托回调的,为了更好地理解以下的事件编程模式,这里对委托回调作以下回顾和总结:

  1. 委托回调可以实现由一个对象(通知源)向外部发送通知的目的。
  2. 通知源需要有一个委托字段和一个发出通知的方法。委托字段用以接收外部方法的注入,形成一个委托实例。发出通知的方法通过执行委托实例,实现向外发送通知的目的。
  3. 在通知源的外部,需要有一个可以注入通知源的方法(接收通知的方法),以便把它注入到通知源形成委托实例,并在通知源发出通知时接收通知,而且在接收到通知时作相应的处理工作。接收通知的方法有时也叫侦听器,意即在侦听到(接收到)通知时,对通知做出响应。
  4. 在实现通知的编程中(主程序中),需要将接收通知的方法注入到通知源中,这称为通知的订阅。

如果把上述委托回调对应到事件场景,应当有以下环节:

  1. 事件是用来实现委托回调的。因此,事件的目的是为了从一个对象(事件源)向外发送事件通知,以便外部对象在接收到通知时进行事件处理。
  2. 事件源需要定义一个事件句柄(事件字段)和一个发送事件通知的方法。事件句柄用以接收事件侦听器(处理事件的方法)的注入,以形成事件委托对象。发送事件通知的方法通过执行事件委托,向外部发送事件通知,就好像向外部通告:“某个事件已经发生啦”。
  3. 事件侦听器是另一个实体中可以作为侦听器注入到事件源中的事件处理方法,以便在接收到事件通知时,对所发生的事件做出响应。
  4. 在实现事件的编程中(主程序中),需要将侦听器注入到事件源中,这通常称为事件订阅

由上可见,事件的本质与委托回调是一致的,但为了克服前述委托回调所具有的干扰,以及让事件有统一的编程和使用方式,对事件的编程需采用特定的编程模式,这个模式称为EventHandler事件编程模式。 — 译者注

Defining and Publishing EventHandler Pattern Events
定义并发布EventHandler模式的事件

The first step in defining an event is to derive a class from the System.EventArgs class, which contains properties and fields to contain any custom data that you want to pass when you invoke the event delegate; the name of this class should end with EventArgs. Listing 10-9 contains an example for the calculation notification from earlier examples.
定义事件的第一步是从System.EventArgs类(事件参数类)派生一个类,这个类需要包含在调用事件委托时希望传递的所有自定义数据的各种字段和属性,这个类的名称应当以EventArgs结尾。清单10-9是针对前面例子中计算通知的一个示例。

Listing 10-9. A Custom EventArgs Implementation
清单10-9. 一个自定义EventArgs实现

class CalculationEventArgs : EventArgs {
    // 定义事件委托的所有参数字段。
    // 注意,这些字段都是private的 — 译者注
    private int x, y, result; 
    // 构造器
    public CalculationEventArgs(int num1, int num2, int resultVal) {
        x = num1;
        y = num2;
        result = resultVal;
    }
    // 以下是与字段对应的属性。注意,这些属性都只有getter块,即都是只读的 — 译者注
    public int X {
        get { return x; }
    }
    public int Y {
        get { return y; }
    }
    public int Result {
        get { return result; }
    }
}

You can include any fields and properties that you need to express information about your event, but you should make sure that the fields cannot be modified. This is because the same EventArgs object will be passed to each of the subscribers of your event, and a poorly or maliciously coded recipient could change the information that subsequent listeners receive. The CalculationEventArgs class derives from EventArgs and defines fields for the details of our calculation and a set of read-only properties to provide access to the field values. You can get more information about properties in Chapter 8 and more information about fields in Chapter 7.
你可以包括需要表示事件信息的任何字段和属性,但要确保字段不能被修改(即,是只读的 — 译者注)。这是因为,同样的EventArgs对象(你所定义的这些字段和属性 — 译者注)将被传递给该事件的每个订户,无知或恶意的代码接收者可能会修改随后的侦听器所接收的信息。这个CalculationEventArgs类派生于EventArgs,且为计算细节定义了一组字段和只读属性,以提供对字段值的访问。你可以从第8章了解更多关于属性、从第7章了解更多关于字段的信息。

以上表明,事件编程的第一步需要定义一个派生于System.EventArgs类的事件参数类。这个类的名称按约定要以EventArgs结尾。在这个类中,应当为事件的所有参数定义相应的字段和属性 — 译者注

The next step is to define an event in your class. You don’t have to define a delegate for events (although as I showed earlier you certainly can), because you can use the generic EventHandler delegate, which is part of the System namespace. Listing 10-10 demonstrates the definition of an event.
下一步是在你的类(事件源类 — 译者注)中定义一个事件。你不必为事件定义一个委托(虽然我在前面已表明这是可以的),因为你可以使用泛型的EventHandler委托,它位于System命名空间中。清单10-10是一个事件定义。

Listing 10-10. Defining an Event Using the Genetic EventHandler Delegate
清单10-10. 用泛型EventHandler委托定义一个事件

class Calculator {
    public event EventHandler<CalculationEventArgs> CalculationPerformedEvent;
    ...
}

To define the event using the generic EventHandler delegate, you use your custom EventArgs class as the type parameter, as shown in the listing. The convention is that the name of the event should end with the word Event. You can learn more about generic types and generic type parameters in Chapter 15.
为了用泛型EventHandler委托来定义事件,你要以自定义的EventArgs类作为其类型参数,如清单所示。其约定是,事件的名称应当以单词Event结尾。第15章可以了解更多关于泛型类型及泛型类型参数的内容。

请把这个EventHandler理解为事件句柄,它是一种泛型委托类型,是用来定义事件(字段)的。用这个事件句柄定义事件可以将委托的定义和创建事件委托字段这两步工作合并成一步,即,此时不需要再定义事件的委托了。定义这个事件委托的作用是用它来接收外部注入的事件侦听器(事件处理方法) — 译者注

The convention dictates that you put the code to invoke your event delegate in a method whose name starts with On concatenated with the event name, less the word event. So, for example, since we have defined an event called CalculationPerformedEvent, the method would be called OnCalculationPerformed. This method should make a copy of the event to avoid a race condition (race conditions arise in parallel programming and are explained in later in this book) and ensure that the event has subscribers by ensuring that the event field is not null. Listing 10-11 shows the Calculator class updated to use the event pattern fully.
该约定指明,为了调用这个事件委托,你会把代码放到一个方法中,该方法的名称以On开头,后跟去掉单词event的事件名称。举例来说,由于我们定义了一个名称为CalculationPerformedEvent的事件,该方法要叫做OnCalculationPerformed。这个方法将形成该事件的一份拷贝,以避免竞争条件(竞争条件会出现在并行编程中,本书的后面会加以解释),并通过保证事件字段非空的办法来确保该事件已有订户。清单10-11展示了经过修改的Calculator类,以完全使用这种事件模式。

Listing 10-11. Implementing the EventArgs Pattern
清单10-11. 实现EventArgs模式

class Calculator {
    // 定义事件
    public event EventHandler<CalculationEventArgs> CalculationPerformedEvent; 
    public int CalculateProduct(int num1, int num2) {
        // perform the calculation
        // 执行计算
        int result = num1 * num2; 
        // publish the event
        // 发布事件,意即执行事件委托,向外部发出事件通知 — 译者注
        OnCalculationPerformed(new CalculationEventArgs(num1, num2, result)); 
        // return the result
        // 返回结果
        return result;
    }
    // 在发布事件时调用的方法
    private void OnCalculationPerformed(CalculationEventArgs args) {
        // make a copy of the event
        // 形成事件的一份拷贝
        EventHandler<CalculationEventArgs> handler = CalculationPerformedEvent; 
        // check to see we have subscribers
        // 查看事件订户
        if (handler != null) {
            handler(this, args);
        }
    }
}

You can see that the CalculateProduct method creates a new instance of the CalculationEventArgs class and uses it to call the OnCalculationPerformed method, which then copies the event, checks to see that it isn’t null, and invokes it.
可以看出,CalculateProduct方法创建了CalculationEventArgs类的一个新实例,并用它来调用OnCalculationPerformed方法,该方法然后拷贝事件,检查如果不为null,便调用它。

事件编程的第二步是创建事件源类,这需要做三件事:

  1. 用EventHandler定义事件委托。EventHandler的参数是事件编程第一步中创建的事件参数类。该事件委托的名称按约定要以Event结尾。该事件委托的作用是用来接收外部注入的事件处理方法,以形成一个实例化的事件委托对象。
  2. 编写发送事件通知的方法。在这个方法中,通过执行OnXXX方法实现向外发送通知的目的。按约定,OnXXX方法名中的XXX是上一步的事件名称去掉单词Event的部分。送给OnXXX方法的参数是事件参数类的一个实例。
  3. 编写OnXXX方法。在这个方法中需形成事件的一份拷贝,并判断该事件有无订户,若有则执行事件。

以上三步工作如上述清单代码的黑体所示 — 译者注

Subscribing to events is just like using a delegate, with the exception that the subscriber is limited to using the += and -= operators. Listing 10-12 shows a class that uses the events defined in the previous examples.
订阅事件就像使用委托一样,只是把订阅者限制到只能使用 += 和 -= 操作符。清单10-12展示了使用前述示例所定义的事件的一个类。

Listing 10-12. Subscribing to Events
清单10-12. 订阅事件

class Listing_12 {
    static void Main(string[] args) {
        // create a new instance of the Calculator class
        // 创建Calculator类的新实例
        Calculator calc = new Calculator();
        // subscribe to the event in the Calculator class
        // 订阅Calculator类中的事件(将一个外部方法注入到事件源 — 译者注)
        calc.CalculationPerformedEvent += HandleEvent; 
        // perform a calculation
        // 执行计算
        calc.CalculateProduct(20, 72); 
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
    // 处理事件方法
    // 作为侦听器可以被注入到事件源中去,
    // 并在接收到事件源发出的通知时,对该事件进行处理(响应) — 译者注
    static void HandleEvent(object sender, CalculationEventArgs e) {
        Console.WriteLine("Good Class: {0} x {1} = {2}", e.X, e.Y, e.Result);
    }
}

由上可见,要实现事件的订阅与退订,需要做两件事:

  1. 编写事件处理方法:事件处理方法也叫侦听器。事件处理方法是用来注入到事件源、以接收事件通知、并对通知做出响应的方法。注意,事件处理方法的参数有特殊的要求。
  2. 订阅与退订事件:在应用程序(主程序)中,将事件处理方法注入到事件源对象中(订阅)。注意,只能以 += 或 -= 操作符进行事件的订阅或退订,否则会抛出异常。

事件处理方法的编程,以及事件的订阅与退订如上述清单的黑体所示 — 译者注

You must ensure that the method you are going to use to handle events is not publically accessible; otherwise, you are still liable to encounter problems with other classes, as demonstrated by Listing 10-13.
必须确保打算用来处理事件的方法不是公开可访问的(即,事件处理方法即上述说明中的事件侦听器方法,该方法的访问修饰符应当不是public的 — 译者注),否则,与其它类一起使用时,仍会遇到问题,如清单10-13所示。

Listing 10-13. Removing Another Delegate from an Event
清单10-13. 从一个事件中移去另一个委托

using System; 
class CalculationEventArgs : EventArgs {
    private int x, y, result; 
    public CalculationEventArgs(int num1, int num2, int resultVal) {
        x = num1;
        y = num2;
        result = resultVal;
    }
    public int X {
        get { return x; }
    }
    public int Y {
        get { return y; }
    }
    public int Result {
        get { return result; }
    }
}
class Calculator {
    public event EventHandler<CalculationEventArgs> CalculationPerformedEvent; 
    public int CalculateProduct(int num1, int num2) {
        // perform the calculation
        // 执行计算
        int result = num1 * num2; 
        // publish the event
        // 公布事件
        OnCalculationPerformed(new CalculationEventArgs(num1, num2, result)); 
        // return the result
        // 返回结果
        return result;
    }
    private void OnCalculationPerformed(CalculationEventArgs args) {
        // make a copy of the event
        // 形成事件的一份拷贝
        EventHandler<CalculationEventArgs> handler = CalculationPerformedEvent; 
        // check to see we have subscribers
        // 检查是否有订户
        if (handler != null) {
            handler(this, args);
        }
    }
}
class NefariousClass {
    public NefariousClass(Calculator calc) {
        // add a new listener for Calculator
        // 添加一个新的Calculator侦听器
        calc.CalculationPerformedEvent += HandleNotifyCalculation; 
        // unsubscribe someone else's event handler
        // 退订其他人的事件处理器
        calc.CalculationPerformedEvent -= Listing_13.HandleEvent;
    }
    public void HandleNotifyCalculation(object sender, CalculationEventArgs e) {
        // print out the details of the real calculation
        // 打印出实际计算的细节
        Console.WriteLine("NefariousClass: {0} x {1} = {2}",
                e.X, e.Y, e.Result);
    }
}
class Listing_13 {
    static void Main(string[] args) {
        // create a new instance of the Calculator class
        // 创建Calculator类的新实例
        Calculator calc = new Calculator();
        // subscribe to the event in the calaculator class
        // 订阅Calculator类中的事件
        calc.CalculationPerformedEvent += HandleEvent; 
        // create an instance of NefariousClass
        // 创建NefariousClass实例
        NefariousClass nef = new NefariousClass(calc); 
        // perform a calculation
        // 执行计算
        calc.CalculateProduct(20, 72); 
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
    public static void HandleEvent(object sender, CalculationEventArgs e) {
        Console.WriteLine("Good Class: {0} x {1} = {2}", e.X, e.Y, e.Result);
    }
}

The NefariousClass constructor in the example adds a new listener to the Calculator event, but it also removes the listener added by the Listing_13 class. It can do this because the Listing_13.HandleEvent method is public and therefore can be accessed outside of its containing class. Changing the access modifier on the HandleEvent class to a more restrictive setting would prevent this from happening.
上例中NefariousClass的构造器把一个新的侦听器加到Calculator事件,但它也移去了Listing_13类加入的侦听器。这确实是能够做到的,因为Listing_13.HandleEvent方法是public的,因而可以在它的容器类的外部访问它。把HandleEvent类的访问修饰符改为更严格的设置将可以阻止这一情况的发生。

Creating Nongeneric Events
创建非泛型事件

Another approach to events is to use the nongeneric version of EventHandler. This is more like using a delegate in that you have to define the event/delegate type. This approach predates the introduction of generic types in C#, but I have included it because it is still widely used. Listing 10-14 shows the Calculator example implemented without generic support.
事件的另一种办法是使用EventHandler的非泛型版本(这是事件的另一种编程模式 — 译者注)。这更像以事件/委托类型的方式来使用委托。这种办法在C#中要早于泛型类型的引入,我在这里包含此内容是因为它仍有广泛的运用。清单10-14演示了不用泛型支持的Calculator示例实现。

Listing 10-14. Implementing Events Without Generic Types
清单10-14. 不用泛型类型实现事件

using System; 
delegate void CalculationPerformedEventHandler(object sender, CalculationEventArgs args); 
class CalculationEventArgs : EventArgs {
    private int x, y, result; 
    public CalculationEventArgs(int num1, int num2, int resultVal) {
        x = num1;
        y = num2;
        result = resultVal;
    }
    public int X {
        get { return x; }
    }
    public int Y {
        get { return y; }
    }
    public int Result {
        get { return result; }
    }
}
class Calculator {
    public event CalculationPerformedEventHandler CalculationPerformedEvent; 
    public int CalculateProduct(int num1, int num2) {
        // perform the calculation
        // 执行计算
        int result = num1 * num2; 
        // publish the event
        // 公布事件
        OnCalculationPerformed(new CalculationEventArgs(num1, num2, result)); 
        // return the result
        // 返回结果
        return result;
    }
    private void OnCalculationPerformed(CalculationEventArgs args) {
        // make a copy of the event
        // 形成事件拷贝
        CalculationPerformedEventHandler handler = CalculationPerformedEvent; 
        // check to see we have subscribers
        // 检查是否已有订户
        if (handler != null) {
            handler(this, args);
        }
    }
}
class Listing_14 {
    static void Main(string[] args) {
        // create a new instance of the Calculator class
        // 创建Calculator类的新实例
        Calculator calc = new Calculator();
        // subscribe to the event in the calaculator class
        // 订阅Calculator类中的事件
        calc.CalculationPerformedEvent += HandleEvent; 
        // perform a calculation
        // 执行计算
        calc.CalculateProduct(20, 72); 
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
    static void HandleEvent(object sender, CalculationEventArgs e) {
        Console.WriteLine("Good Class: {0} x {1} = {2}", e.X, e.Y, e.Result);
    }
}

There isn’t much to say about this example; it is very similar to the generic event listings but has an additional delegate definition.
这个例子无需多说,除了有一个附加的委托定义之外,它与前述泛型事件完全类似。

Creating Events Without Custom Data
创建无自定义数据的事件

If you don’t need to pass custom data as part of your event, then you can use EventArgs directly, as shown in Listing 10-15.
如果不需要把自定义数据作为事件的一部分进行传递,那么可以直接使用EventArgs,如果清单10-15所示。

Listing 10-15. Creating and Using Events with No Custom Data
清单10-15. 创建和使用无自定义数据的事件

using System; 
class Calculator {
    public event EventHandler CalculationPerformedEvent;
    public int CalculateProduct(int num1, int num2) {
        // perform the calculation
        // 执行计算
        int result = num1 * num2; 
        // publish the event
        // 公布事件
        OnCalculationPerformed();
        // return the result
        // 返回结果
        return result;
    }
    private void OnCalculationPerformed() {
        // make a copy of the event
        // 形成事件拷贝
        EventHandler handler = CalculationPerformedEvent; 
        // check to see we have subscribers
        // 检查是否已有订户
        if (handler != null) {
            handler(this, EventArgs.Empty); 
        }
    }
}
class Listing_15 {
    static void Main(string[] args) {
        // create a new instance of the Calculator class
        // 创建Calculator类新实例
        Calculator calc = new Calculator();
        // subscribe to the event in the calaculator class
        // 订阅Calculator类中的事件
        calc.CalculationPerformedEvent += HandleEvent; 
        // perform a calculation
        // 执行计算
        calc.CalculateProduct(20, 72); 
        // wait for input before exiting
        // 退出之前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
    static void HandleEvent(object sender, EventArgs e) {
        Console.WriteLine("Event Received");
    }
}

There is no need to define a custom delegate if you don’t need custom data. When defining the event, you simply use the EventHandler type, as follows:
如果不需要自定义数据,没必要定义一个自定义委托。当定义事件时,你只要简单地使用EventHandler类型,如下所示:

public event EventHandler CalculationPerformedEvent;

The event still requires two arguments to invoke it, but you can use the static EventArgs.Empty property to get a reference to a ready-made EventArgs instance that has no custom data. You can see this in the OnCalculationPerformed method of the Calculator class, which has been updated to remove the method parameters.
该事件仍然需要两个参数去调用它,但你可以使用静态的EventArgs.Empty属性,以获得对已形成的无自定义数据的EventArgs实例的引用。你可以在Calculator类的OnCalculationPerformed方法中看到这种情况,已对其作了修改,去除了方法参数。

由上可见,创建无自定义数据的事件,不需要创建事件参数类 — 译者注

Applying Modifiers to Events
对事件运用修饰符

You can control access to your events using the public, protected, internal, and private keywords. See Chapter 7 for details of these keywords and the effect they have on fields.
你可以用public、protected、internal和private关键字来控制对事件的访问。参阅第7章关于这些关键字的细节,以及它们运用于字段所具有的效应。

You should not use the virtual modifier on events. If you do, it is possible that your events will not be delivered properly. If you want to override an event from a base class, then you should mark the OnXXX method as virtual and override the method in the derived class. Listing 10-16 provides a demonstration. You can find more details and examples of overriding methods in Chapter 9.
你不应该在事件上使用virtual修饰符。如果这么做,你的事件可能不会被适当地投递。如果你想重写基类的事件,那么你应当将OnXXX方法标记为virtual,并在派生类中重写该方法。清单101-6提供了一个演示。在第9章中你会看到重写方法的更多细节和示例。

Listing 10-16. Deriving Event Implementations
清单10-16. 派生事件实现

// 基类
class Calculator {
    public event EventHandler<CalculationEventArgs> CalculationPerformedEvent; 
    public int CalculateProduct(int num1, int num2) {
        // perform the calculation
        // 执行计算
        int result = num1 * num2; 
        // publish the event
        // 公布事件
        OnCalculationPerformed(new CalculationEventArgs(num1, num2, result)); 
        // return the result
        // 返回结果
        return result;
    }
    protected virtual void OnCalculationPerformed(CalculationEventArgs args) {
        // make a copy of the event
        // 形成事件拷贝
        EventHandler<CalculationEventArgs> handler = CalculationPerformedEvent; 
        // check to see we have subscribers
        // 确认已有订户
        if (handler != null) {
            handler(this, args);
        }
    }
}
// 派生类
class DerivedCalc : Calculator {
    protected override void OnCalculationPerformed(CalculationEventArgs args) {
        // perform custom logic here
        // 以下执行自定义逻辑
        // call the base method
        // 调用基方法
        base.OnCalculationPerformed(args);
    }
}

In this example, the DerivedCalc class overrides the OnCalculationPerformed method, which has been marked as virtual in the base Calculator class. The overridden method can perform modifications to the custom EventArgs implementation (or perform any other required task) before calling the base OnCalculationPerformed method to publish the event.
在这个例子中,DerivedCalc类重写了OnCalculationPerformed方法,它在基Calculator类中已被标记为virtual了。重写的方法可以在调用基OnCalculationPerformed方法来公布事件之前,对自定义的EventArgs实现进行修改(或执行任何其它所需的任务)。

Using Action and Func Delegates
使用Action和Func委托

The Func and Action classes are special types that allow you to use delegates without having to specify a custom delegate type. They are used throughout the .NET Framework. For example, you will see examples of both when we look at parallel programming.
Func和Action类是特殊的类型,它们允许你在不必指定自定义委托类型的情况下,去使用委托。在整个.NET框架中都可以使用它们。例如,在我们考察并行计算时,你也会看到这两个类的示例。

Func和Action是.NET的两个特殊类型,在整个.NET平台中都可以使用它们。这两个类型实际上是委托类型,可以用来创建委托对象。因此,由于它们是类型,我们便可以创建Func和Action类型的变量;又由于它们可以用来创建委托对象,于是直接用方法名对这些变量进行赋值,便可以形成实例化的委托对象,而不必进行委托定义 — 译者注

Using Action Delegates
使用Action委托

Action delegates encapsulate methods that do not return results. In other words, they can be used only with methods that are defined with the void keyword. The simplest Action delegate is the System.Action class, which is used for methods that have no parameters (and no results). Listing 10-17 contains an example.
Action委托封装不返回结果的方法(Action是可以用来委托无返回类型的方法,意即,可以用无返回类型的方法名对Action类型的变量进行赋值 — 译者注)。换句话说,这种委托只能用于以void关键字定义的那些方法。最简单的void委托是System.Action类,它用于无参数(且无结果)的方法。清单10-17含有一个例子。

Listing 10-17. Using the Basic Action Delegate
清单10-17. 使用基本的Action委托

using System; 
class Calculator {
    public void CalculateProduct() {
        // perform the calculation
        // 执行计算
        int result = 10 * 20; 
        // print out a message with the result
        // 用结果印出消息
        Console.WriteLine("Result: {0}", result);
    }
}
class Listing_17 {
    static void Main(string[] args) {
        // create a new instance of Calculator
        // 创建Calculator新实例
        Calculator calc = new Calculator();
        // create an action and assign a method
        // 创建一个Action并赋予一个方法
        // 以下语句创建了一个Action类型的变量,并用一个方法名对其实例化,形成了一个委托对象 — 译者注
        Action act = calc.CalculateProduct; 
        // invoke the method via the Action
        // 通过这个Action调用该方法
        act();
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

The key statement in Listing 10-17 is shown in bold; it creates a new local Action variable and assigns the CalculateProduct method from an instance of Calculator as the value. The Action is then invoked, just as a regular delegate would be. Compiling and running the code in Listing 10-17 products the following results:
清单10-17中的关键语句以黑体显示,它创建了一个新的Action局部变量,并把Calculator实例的CalculateProduct方法作为值赋给了它。然后这个Action被调用,就像规则的委托那样。编译并运行清单10-17会产生以下结果:

Result: 200
Press enter to finish

We didn’t have to define a custom delegate in this example. The System.Action type handled everything for us. There are 17 different Action implementations available. Starting with the one used in Listing 10-17, each adds a new generic parameter. This is not as confusing as it may sound; you just create the generic implementation that matches the number of parameters the target method required. Listing 10-18 contains an example that uses two parameters.
在这个例子中,我们不必定义一个自定义委托(即,不必进行委托定义 — 译者注)。System.Action类型为我们处理所有事情。有17个不同的Action实现可用。从清单10-17所用的一个开始,每一个都添加了一个新的泛型参数。实际情况并不像听上去这样让人困扰,你只要创建与目标方法所需要的参数数目匹配的泛型实现。清单1-18包含了一个使用两个参数的示例。

Listing 10-18. Using a Generic Action Delegate
清单10-18. 使用泛型的Action委托

using System; 
class Calculator {
    public void CalculateProduct(int x, int y) {
        // perform the calculation
        // 执行计算
        int result = x * y; 
        // print out a message with the result
        // 用result打印出消息
        Console.WriteLine("Result: {0}", result);
    }
}
class Listing_18 {
    static void Main(string[] args) {
        // create a new instance of Calculator
        // 创建Calculator新实例
        Calculator calc = new Calculator();
        // create an action and assign a method
        // 创建一个action,并赋予一个方法
        Action<int, int> act = calc.CalculateProduct; 
        // invoke the method via the Action
        // 通过Action调用该方法
        act(10, 20);
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

In this example, the method I want to delegate has two int parameters, so I used the Action<int, int> delegate (the parameter types for an Action need not all be the same). If I had wanted to delegate a method with five parameters, then I would have used the Action<T1, T2, T3, T4, T5> type and filled in the type parameters of the Action type to match the parameter types of the delegated method.
在这个例子中,我想委托的方法有两个int参数,因此,我使用了Action<int, int>委托(Action的参数类型并不需要是相同的)。如果我想委托一个具有五个参数的方法,那么,我会使用Action<T1, T2, T3, T4, T5>类型,并填充这个Action类型的类型参数,以匹配被委托方法的参数类型。

Using Func Delegates
使用Func委托

System.Func delegates are just like Action delegates, except that they can return results. The simplest Func implementation has no parameters. Listing 10-19 contains an example; you can see the similarities to the Action examples.
System.Func委托除了可以返回结果以外,它与Action委托完全相同。最简单的Func实现没有参数。清单10-19包含一个示例,你可以看出它与Action示例的类似性。

Listing 10-19. A Simple Func Example
清单10-19. 一个简单的Func示例

using System; 
class Calculator {
    public int CalculateProduct() {
        // perform the calculation
        // 执行计算
        return 10 * 20;
    }
}
class Listing_19 {
    static void Main(string[] args) {
        // create a new instance of Calculator
        // 创建Calculator新实例
        Calculator calc = new Calculator();
        // create a Func and assign a method
        // 创建一个Func,并赋予一个方法
        Func<int> act = calc.CalculateProduct; 
        // invoke the method via the Action(应当是Func)
        // 通过Func调用方法
        int result = act();
        // print out the result
        // 印出结果
        Console.WriteLine("Result: {0}", result);
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

The last generic type for System.Func is the result type for the delegate. So, in Listing 10-19, the Func<int> I used had no parameters but returned an int. Just like Action, there are 17 Func implementations with an increasing number of parameters, each of which can be of a different type. Listing 10-20 demonstrates using Func with two int parameters.
System.Func的最后一个泛型类型是委托的结果类型。因此,在清单10-19中,我所使用的Func<int>没有参数,但返回int。就像Action一样,也有17个带有不同参数数目的Func实现,每个参数都可以是不同的类型。清单10-20演示了使用两个int参数的Func。

Listing 10-20. Using a Func with Parameters
清单10-20. 使用带有参数的Func

using System; 
class Calculator {
    public int CalculateProduct(int x, int y) {
        // perform the calculation
        // 执行计算
        return x * y;
    }
}
class Listing_20 {
    static void Main(string[] args) {
        // create a new instance of Calculator
        // 创建Calculation新实例
        Calculator calc = new Calculator();
        // create a Func and assign a method
        // 创建Func并赋予一个方法
        Func<int, int, int> act = calc.CalculateProduct;
        // invoke the method via the Action Func
        // 通过这个Func调用该方法
        int result = act(10, 20);
        // print out the result
        // 印出结果
        Console.WriteLine("Result: {0}", result); 
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

感谢微软工程师,为我们提供了这么好用的Action和Func。有了它们,我们使用委托简直太方便了 — 译者注

Anonymous Methods
匿名方法

All of the examples so far in this chapter have used named methods, that is, methods that exist in classes and have a method identifier. C# also supports anonymous methods, which allow you to implement a delegate without defining a method. Listing 10-21 contains an anonymous method.
本章目前为止所演示的示例都使用了命名的方法,即,在类中存在且有方法修饰符的方法。C#也支持匿名方法,它让你能够不必定义方法就可以实现委托。清单10-21含有一个匿名方法。

Listing 10-21. An Anonymous Method
清单10-21. 匿名方法

using System; 
class Listing_21 {
    static void Main(string[] args) {
        // create a new Calculator
        // 创建一个新的Calculator
        Calculator calc = new Calculator();
        // create a delegate with an anonymous method
        // 用匿名方法创建一个委托
        calc.CalculationPerformedEvent += delegate(object sender, CalculationEventArgs e) {
            Console.WriteLine("Anonymous Calc: {0} x {1} = {2}", e.X, e.Y, e.Result);
        };
        // perform a calculation
        // 执行计算
        calc.CalculateProduct(20, 40); 
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

The class in Listing 10-21 works with the Calculator and CalculationEventArgs classes defining in Listing 10-9. I have omitted them from this listing for brevity. You can see the anonymous method in bold; it is also illustrated in Figure 10-2.
清单10-21中的类使用了清单10-9中定义的Calculator和CalculationEventArgs类。出于简化,我忽略了它们。你可以看到以黑体表示的匿名方法,也如图10-2所示。

IntrC10-2图中:Delegate Keyword: 委托关键字;Parameters:参数;Code Statements:代码语句

Figure 10-2. The anatomy of an anonymous method
图10-2. 一个匿名方法的剖析

An anonymous method has a number of similarities to a named method. You can see the parameters and code block in Figure 10-2 are just like those you would get in a regular method. There is no method identifier (because there is no method name), and you must use the delegate keyword when defining an anonymous method.
匿名方法与命名方法有许多相似性。你可以看出,图10-2中的参数和代码块与你要在规则方法中得到的一样。没有方法修饰符(因为没有方法名),而且,在定义匿名方法时,你必须使用delegate关键字。

Using an anonymous method as a delegate is just like using a named method, as you can see from Listing 10-21. I used the += operator to add the anonymous method to the event in the Calculator class.
将匿名方法用作为委托与使用命名方法一样,正如你从清单10-21所看到的,我使用了+=操作符把匿名方法加到了Calculator类中的事件。

When the event invokes the delegate, the statements in the anonymous method body are executed, just as would happen for a regular method.
当事件调用委托时,匿名方法体中的语句被执行,就像规则方法所发生的情况一样。

If we compile and run the code in Listing 10-21, we get the following results:
如果编译并运行清单10-21代码,会得到以下结果:

Anonymous Calc: 20 x 40 = 800
Press enter to finish

Anonymous methods work very well with Func and Action delegates, allowing you to define a delegate without needing to create a delegate type or implement a named method. When implementing a delegate that returns a result, simply use the return keyword as you would for a named method, as demonstrated by Listing 10-22.
匿名方法可以与Func和Action委托很好地一起使用,这允许你定义一个委托,而不需要创建委托类型或实现命名方法。当实现返回一个结果的委托时,简单地使用return关键字,就像使用命名方法一样,如清单10-22所示。

Listing 10-22. An Anonymous Method That Returns a Result
清单10-22. 返回一个结果的匿名方法

using System; 
class Listing_22 {
    static void Main(string[] args) {
        // define a func and implement it with an anonymous method
        // 定义一个func,并用一个匿名方法实现它
        Func<int, int, int> productFunction = delegate(int x, int y) {
            return x * y;
        };
        // invoke the func and get a result
        // 调用这个func,并得到一个结果
        int result = productFunction(10, 20); 
        // print out the result
        // 印出结果
        Console.WriteLine("Result: {0}", result); 
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

The bold statement in Listing 10-22 defines a Func that returns an int result and that has two int parameters. The implementation of this delegate is provided by an anonymous method that returns the product of the two parameters values. You invoke the delegate in the normal way, as though it were a method. Compiling and running the code in Listing 10-22 produces the following results:
清单10-22中的黑体语句定义了一个返回int结果且有两个int参数的Func。这个委托的实现是由一个匿名方法实现的,它返回两个参数值的积。用常规的方式调用这个委托,就像它是一个方法一样。编译并运行清单10-22代码产生以下结果:

Result: 200
Press enter to finish

Capturing Outer Variables
捕捉外部变量

Anonymous methods do more than reduce the number of methods in your class; they are also able to access the local variables that are defined in the containing methods, known as outer variables. Listing 10-23 provides a demonstration.
匿名方法的作用还不止能减少类中的方法数,它们也能访问容纳方法(指容纳该匿名方法的容器 — 译者注)中定义的局部变量,这称为外部变量(outer variables)。清单10-23提供了一个演示。

Listing 10-23. Accessing Outer Variables
清单10-23. 访问外部变量

using System; 
class Calculator {
    Func<int, int, int> calcFunction; 
    public Calculator(Func<int, int, int> function) {
        calcFunction = function;
    }
    public int PerformCalculation(int x, int y) {
        return calcFunction(x, y);
    }
}
class Listing_23 {
    static void Main(string[] args) {
        // define a local variable
        // 定义一个局部变量
        int calculationCount = 0;
        // define and implement a Func
        // 定义并实现一个Func
        Func<int, int, int> productFunc = delegate(int x, int y) {
            // increment the outer variables
            // 递增外部变量
            calculationCount++;
            // calculate and return the result
        // 计算并返回结果
            return x * y;
        };
        // create a new instance of Calculator
        // 创建Calculator新实例
        Calculator calc = new Calculator(productFunc);
        // perform several calculations
        // 执行一些计算
        for (int i = 0; i < 5; i++) {
            Console.WriteLine("Result {0} = {1}", i, calc.PerformCalculation(i, i));
        }
        // print out the value of the outer variable
        // 印出外部变量的值
        Console.WriteLine("calculationCount: {0}", calculationCount); 
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

The Calculator class in this example takes a Func<int, int, int> as a constructor parameter and invokes the delegate when the PerformCalculation method is called. The Listing_23 class defines a matching Func using an anonymous method. This is passed to the constructor of the Calculator instance.
这个例子中的Calculator类以Func<int, int, int>作为构造器参数,并在调用PerformCalculation方法时调用该委托。Listing_23类用一个匿名方法定义了一个匹配的Func。它被传递给Calculator实例的构造器。

The anonymous method increments the calculationCount variable each time that it is invoked, even though the variable is defined outside of the anonymous method and even though the Func is invoked by an entirely different method in an entirely different object. Compiling and running the code in Listing 10-23 produces the following result:
匿名方法每次被调用时都递增了calculationCount变量,即使这个变量是在匿名方法的外部定义的,甚至Func也是由完全不同的对象中完全不同的方法来调用的。编译并运行清单10-23代码产生以下结果:

Result 0 = 0
Result 1 = 1
Result 2 = 4
Result 3 = 9
Result 4 = 16
calculationCount: 5
Press enter to finish

Variables that are accessed outside the anonymous method are called captured variables. The calculationCount variable in Listing 10-23 was captured by the anonymous method. An anonymous method that captures variables is called a closure.
匿名方法外部被访问的变量称为被捕捉captured)变量(因此,匿名方法体外部的那些变量称为外部变量,在匿名方法体中所访问的那个外部变量称为被捕捉变量 — 译者注)。清单10-23中的calculationCount变量是由匿名方法来捕捉的。一个要捕捉变量的匿名方法称为是一个闭包closure)。

Captured variables are evaluated when the delegate is invoked, which means that you should take care when making assumptions about the value of a variable. Listing 10-24 provides a demonstration.
在委托被调用时,会求取被捕捉变量的值,这意味着,在假设一个变量的值时要小心。清单10-24提供了一个演示。

Listing 10-24. Evaluating Captured Variables
清单10-24. 求取被捕捉变量的值

using System; 
class Listing_24 {
    static void Main(string[] args) {
        // define a variable that will be captured
        // 定义被捕捉变量
        string message = "Hello World";
        // define an anonymous method that will capture
        // the local variables
        // 定义一个将捕捉这个局部变量的匿名方法
        Action printMessage = delegate() {
            Console.WriteLine("Message: {0}", message);
        };
        // modify one of the local vaiables
        // 修改局部变量
        message = "Howdy!"; 
        // invoke the delegate
        // 调用委托
        printMessage();
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

When the anonymous method is defined, the value of the message variable is Hello World. But defining an anonymous method doesn’t capture the variables it references—that doesn’t happen until the delegate is invoked, by which time the value of the message variable has changed. Compiling and running the code in Listing 10-24 produces the following result:
在定义匿名方法时,message变量的值是Hello World。但在定义匿名方法时并不捕捉它所引用的变量 — 直到委托被调用时才会发生,此时message变量的值已经变化了。编译并运行清单10-24代码产生以下结果:

Message: Howdy!
Press enter to finish

Lambda Expressions
Lambda表达式

Lambda expressions have largely replaced anonymous methods since they were introduced in C# 3.0. They have much the same functionality as an anonymous method but are slightly more convenient to use. Listing 10-25 contains an anonymous method and an equivalent lambda expression.
Lambda表达式已经广泛代替了匿名方法,因为它是从C# 3.0开始引入的。Lambda表达式与匿名方法有很多同样的功能,但更便于使用。清单10-25包含了一个匿名方法和一个等效的Lambda表达式。

Listing 10-25. Comparing an Anonymous Method with a Lambda Expression
清单10-25. 匿名方法与Lambda表达式比较

using System; 
class Listing_25 {
    static void Main(string[] args) {
        // implement an anonymous method that multiplies ints
        // 实现一个int数相乘的匿名方法
        Func<int, int, int> anonFunc = delegate(int x, int y) {
            return x * y;
        };
        // do the same thing with a lambda expression
        // lambda表达式做同样的事情
        Func<int, int, int> lambaFunc = (x, y) => {
            return x * y;
        };
        // invoke the delegates
        // 调用委托
        Console.WriteLine("Anonymous Method Result: {0}", anonFunc(10, 10));
        Console.WriteLine("Lambda Expression Result: {0}", lambaFunc(10, 10)); 
        // wait for input before exiting
        // 退出前等待输入
        Console.WriteLine("Press enter to finish");
        Console.ReadLine();
    }
}

The lambda expression in Listing 10-25 is shown in bold. There are only three parts to a lambda expression, and they are illustrated in Figure 10-3.
清单10-25中的lambda表达式以黑体显示。一个lambda表达式只有三个部分,它们如图10-3所示。

IntrC10-3图中:Parameters:参数,Lambda Operator:Lambda操作符,Code Statements:代码语句

Figure 10-3. The anatomy of a lambda expression
图10-3. Lambda表达式剖析

Although the format of a lambda expression looks a little odd, the basic premise is the same as for anonymous methods. Parameters in a lambda expression are specified without their types—the types are inferred.
虽然lambda表达式看上去有点古怪,但基本前提与匿名方法是相同的。Lambda表达式的参数不指定类型 — 其类型通过推断。

The code statements are just like for a method, named or anonymous. You access the parameters by the names they have been specified by. In the case of Listing 10-25, the parameters x and y are multiplied together, and the result is returned.
其代码语句与命名或匿名方法同样。通过被指定的参数名称来访问参数。在清单10-25的示例中,参数x和y相乘,并返回结果。

You can omit the braces and the return keyword if there is a single operation in a lambda expression so that the lambda expression in Listing 10-25 can also be written as follows:
如果在lambda表达式中只有一个单一的操作,你可以忽略花括号和return关键字,因此,清单10-25的lambda表达式也可以写成如下形式:

(x, y) => x * y;

The lambda operator (=>) is, as you might expect, required for lambda operators. It is often described as the goes to operator. For the expression in Listing 10-25, we can say that the parameters x and y go to the product of x and y.
lambda操作符(=>),正如你可能想到的,是必需的。通常把它说成是“进入go to)”操作符(感觉把 => 操作符说成“送入”更恰当些,意即将参数送入方法体 — 译者注)。对于清单10-25中的表达式,我们可以说成,x和y参数进入x和y的乘积。

Compiling and running the code in Listing 10-25 produces the following results:
编译并运行清单10-25代码产生以下结果:

Anonymous Method Result: 100
Lambda Expression Result: 100
Press enter to finish

The C# compiler is pretty good at inferring the types of parameters for lambda expressions, but on occasion the compiler won’t be able to work it out, and you will you need to explicitly tell the compiler what they are. Here is an example:
C#编译器十分擅长推断lambda表达式的参数类型,但偶尔也会有编译器不能推断的情况,此时你需要明确地告诉编译器,它们是什么。以下是一个示例:

(int x, int y) => x * y;

I have used lambda expressions extensively in software projects and for other C# books, and I have found only a small number of instances where the compiler couldn’t figure things out implicitly, but when you do encounter one of those situations (or if you just prefer explicit typing), then it is good to know how to do it.
我在软件项目以及其它C#书籍中广泛使用了lambda表达式。而且我发现,只有很少情况下才会出现编译器不能推断的情况。当你遇到这种情况时(或者你更喜欢明确指定类型),那么,最好知道如何做这种事。

Summary
小结

In this chapter, you have how delegates can be used to encapsulate references to static and instance methods and passed around like regular types. You have seen the limitations of delegates when they are shared between objects and how events address this shortcoming.
在本章中,你知道了如何把委托用于封装对静态和实例化方法的引用,并像规则类型一样进行传递。你也看到了,对象之间共享委托所具有的局限性,以及事件是如何解决这一缺陷的。

The convention for using events is widely adopted, and I recommend that you follow it in your own code. Not only does it make your code easier for other programmers to use, but it also helps you create events that can be overridden reliably in derived classes.
使用事件的约定被广泛采纳,而且我建议你在代码中遵循它。不仅它使你的代码更易于被其他程序员使用,而且也有助于你创建能够在派生类中可靠地进行重写的事件。

We looked briefly at the System.Func and System.Action types, both of which allow us to work with delegates without creating custom delegate types, and we looked at anonymous methods and lambda expressions, which allow us to implement delegates without having to define methods. We learned how anonymous methods and lambda expressions capture local variables and when those variables are evaluated. We’ll see a lot more of Func, Action, and lambda expressions when we explore some of the advanced C# language features such as LINQ and parallel programming.
我们简要地考查了System.Func和System.Action类型,它们都能让我们不必创建自定义委托类型就可以使用委托。我们也考查了匿名方法和lambda表达式,它们让我们能够不必定义方法就可以实现委托。我们了解了匿名方法和lambda表达式如何捕捉局部变量,以及评估这些变量的时间。在我们考察C#的高级语言特性,如LINQ和并行编程时,会看到更多Func、Action和lambda表达式。

这是一篇有技术含量、甚至可以说是有学术价值的文章!请大家多给推荐

日交易额百亿级交易系统的超轻量日志实现

首先来聊聊往事吧~~两年前就职于一家传统金融软件公司,为某交易所开发一套大型交易系统,交易标的的价格为流式数据,采用价格触发成交方式,T+0交易制度(类似炒股,只是炒的不是股票而是其他标的物,但可以随时开平仓)。鉴于系统需要记录大量价格数据、交易信息及订单流水,且系统对性能要求极高(敏感度达毫秒级),因此需要避免日志服务成为系统性能瓶颈。通过对几个通用型日志(如log4j、logback)的性能压测,以及考虑到它们作为通用型日志相对比较臃肿,就决定自个儿写个日志工具以支撑系统功能和性能所需。当时的做法只是简单的将日志的实现作为一个 util 类写在项目中,只有几百行的代码量。

系统上线两个月后日均成交额200亿RMB,最高达440亿RMB,峰值成交4000笔/秒。系统非常庞大,但几百行的代码却完美支撑住了重要的日志服务!

鉴于其优秀的表现,就花了一点点时间把它抽取出来作为一个独立的日志组件,取名叫 FLogger,代码几乎没有改动,现已托管到GitHub(FLogger),有兴趣的童鞋可以clone下来了解并改进,目前它的实现是非常简(纯)单(粹)的。

以上就是 FLogger 的诞生背景。好吧,下面进入正题。

特性

虽然 FLogger 只有几百行的代码,但是麻雀虽小五脏俱全,它可是拥有非常丰富的特性呢:

  • 双缓冲队列
  • 多种刷盘机制,支持时间触发、缓存大小触发、服务关闭强制触发等刷盘方式
  • 多种 RollingFile 机制,支持文件大小触发、按天触发等 Rolling 方式
  • 多日志级别,支持 debug、info、warn、error和 fatal 等日志级别
  • 热加载,由日志事件触发热加载
  • 超轻量,不依赖任何第三方库
  • 性能保证,成功用于日交易额百亿级交易系统

使用

既然是个超轻量级日志,使用肯定要很简单。为最大程度保持用户的使用习惯,Flogger 提供了与 log4j 几乎一样的日志 API。你只需要先获取一个实例,接下来的使用方式就非常简单了:

1
2
3
4
5
6
7
8
//获取单例
FLogger logger = FLogger.getInstance();
//简便api,只需指定内容
logger.info("Here is your message...");
//指定日志级别和内容,文件名自动映射
logger.writeLog(Constant.INFO, "Here is your customized level message...");
//指定日志输出文件名、日志级别和内容
logger.writeLog("error", Constant.ERROR, "Here is your customized log file and level message...");

使用前你需要在项目根路径下创建 log.properties 文件,配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
########## 公共环境配置 ##########
# 字符集
CHARSET_NAME = UTF-8
########## 日志信息配置 ##########
# 日志级别   0:调试信息  1:普通信息   2:警告信息  3:错误信息  4:严重错误信息
LOG_LEVEL = 0,1,2,3,4
# 日志文件存放路径
LOG_PATH =./log
# 日志写入文件的间隔时间(默认为1000毫秒)
WRITE_LOG_INV_TIME = 1000
# 单个日志文件的大小(默认为10M)
SINGLE_LOG_FILE_SIZE = 10485760
# 单个日志文件缓存的大小(默认为10KB)
SINGLE_LOG_CACHE_SIZE = 10240

当然,为了提供最大程度的便捷性,日志内部针对所有配置项都提供了默认值,你大可不必担心缺少配置文件会抛出异常。

至此,你可能很好奇使用 FLogger 打印出来的日志格式到底是怎样的,会不会杂乱无章无法理解,还是信息不全根本无法判断上下文呢?好吧,你多虑了,FLogger 提供了非常规范且实用的日志格式,能使让你很容易理解且找到相关上下文。

先来看看上面的 demo 代码打印出来的结果:

info.log

1
[INFO] 2016-12-06 21:07:32:840 [main] Here is your message...

warn.log

1
[WARN] 2016-12-06 21:07:32:842 [main] Here is your customized level message...

error.log

1
[ERROR] 2016-12-06 21:07:32:842 [main] Here is your customized log file and level message...

从上面可以看到,你可以很清楚的分辨出日志的级别、时间和内容等信息。到这其实很明了了,日志由以下几个元素组成:

1
[日志级别] 精确到毫秒的时间 [当前线程名] 日志内容

当然,处于便捷性的考虑,FLogger 目前并不支持用户定义日志格式,毕竟它的目的也不是要做成一个通用性或者可定制性非常高的日志来使用。

源码解析

上面这么多都是围绕如何使用进行说明,下面就针对 FLogger 的特性进行实现逻辑的源码解析。

双缓冲队列

FLogger 在内部采用双缓冲队列,那何为双缓冲队列呢?它的作用又是什么呢?

FLogger 为每个日志文件维护了一个内部对象 LogFileItem ,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class LogFileItem {
    /** 不包括路径,不带扩展名的日志文件名称 如:MsgInner */
    public String logFileName = "";
    
    /** 包括路径的完整日志名称 */
    public String fullLogFileName = "";
    
    /** 当前日志文件大小 */
    public long currLogSize = 0;
    
    /** 当前正在使用的日志缓存 */
    public char currLogBuff = 'A';
    
    /** 日志缓冲列表A */
    public ArrayList<StringBuffer> alLogBufA = new ArrayList<StringBuffer>();
    
    /** 日志缓冲列表B */
    public ArrayList<StringBuffer> alLogBufB = new ArrayList<StringBuffer>();
        
    /** 下次日志输出到文件时间 */
    public long nextWriteTime = 0 ;
    
    /** 上次写入时的日期 */
    public String lastPCDate = "";
    
    /** 当前已缓存大小 */
    public long currCacheSize = 0;
}

在每次写日志时,日志内容作为一个 StringBuffer 添加到当前正在使用的 ArrayList<StringBuffer> 中,另一个则空闲。当内存中的日志输出到磁盘文件时,会将当前使用的 ArrayList<StringBuffer> 与空闲的 ArrayList<StringBuffer> 进行角色交换,交换后之前空闲的 ArrayList<StringBuffer> 将接收日志内容,而之前拥有日志内容的 ArrayList<StringBuffer> 则用来输出日志到磁盘文件。这样就可以避免每次刷盘时影响日志内容的接收(即所谓的 stop-the-world 效应)及多线程问题。流程如下:

关键代码如下:

日志接收代码

1
2
3
4
5
6
7
8
9
//同步单个文件的日志
synchronized(lfi){
    if(lfi.currLogBuff == 'A'){
        lfi.alLogBufA.add(logMsg);
    }else{
        lfi.alLogBufB.add(logMsg);
    }
    lfi.currCacheSize += CommUtil.StringToBytes(logMsg.toString()).length;
}

日志刷盘代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//获得需要进行输出的缓存列表
ArrayList<StringBuffer> alWrtLog = null;
synchronized(lfi){
    if(lfi.currLogBuff == 'A'){
        alWrtLog = lfi.alLogBufA;
        lfi.currLogBuff = 'B';
    }else{
        alWrtLog = lfi.alLogBufB;
        lfi.currLogBuff = 'A';
    }
    lfi.currCacheSize = 0;
}
//创建日志文件
createLogFile(lfi);
//输出日志
int iWriteSize = writeToFile(lfi.fullLogFileName,alWrtLog);
lfi.currLogSize += iWriteSize;

多刷盘机制

FLogger 支持多种刷盘机制:

  • 刷盘时间间隔触发
  • 内存缓冲大小触发
  • 退出强制触发

下面就来一一分析。

刷盘时间间隔触发

配置项如下:

1
2
# 日志写入文件的间隔时间(默认为1000毫秒)
WRITE_LOG_INV_TIME = 1000

当距上次刷盘时间超过间隔时间,将执行内存日志刷盘。

内存缓冲大小触发

配置项如下:

1
2
# 单个日志文件缓存的大小(默认为10KB)
SINGLE_LOG_CACHE_SIZE = 10240

当内存缓冲队列的大小超过配置大小时,将执行内存日志刷盘。

退出强制触发

FLogger 内部注册了 JVM 关闭钩子 ShutdownHook ,当 JVM 正常关闭时,由钩子触发强制刷盘,避免内存日志丢失。相关代码如下:

1
2
3
4
5
6
7
8
public FLogger(){
    Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
        @Override
        public void run() {
            close();
        }
    }));
}

当 JVM 异常退出时无法保证内存中的日志全部落盘,但可以通过一种妥协的方式来提高日志刷盘的实时度:设置 SINGLE_LOG_CACHE_SIZE = 0 或者 WRITE_LOG_INV_TIME = 0 。

刷盘代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/** 线程方法 */
public void run(){
    int i = 0 ;
    while(bIsRun){
        try{
            //输出到文件
            flush(false);
            //重新获取日志级别
            if(i++ % 100 == 0){
                Constant.CFG_LOG_LEVEL = CommUtil.getConfigByString("LOG_LEVEL","0,1,2,3,4");
                i = 1;
            }
        }catch(Exception e){
            System.out.println("开启日志服务错误...");
            e.printStackTrace();
        }
    }
}
/** 关闭方法 */
public void close(){
    bIsRun = false;
    try{
        flush(true);
    }catch(Exception e){
        System.out.println("关闭日志服务错误...");
        e.printStackTrace();
    }
}
    
/**
* 输出缓存的日志到文件
* @param bIsForce 是否强制将缓存中的日志输出到文件
*/
private void flush(boolean bIsForce) throws IOException{
    long currTime = System.currentTimeMillis();
    Iterator<String> iter = logFileMap.keySet().iterator();
    while(iter.hasNext()){
        LogFileItem lfi = logFileMap.get(iter.next());
        if(currTime >= lfi.nextWriteTime || SINGLE_LOG_CACHE_SIZE <= lfi.currCacheSize || bIsForce == true){
            //获得需要进行输出的缓存列表
            ArrayList<StringBuffer> alWrtLog = null;
            synchronized(lfi){
                if(lfi.currLogBuff == 'A'){
                    alWrtLog = lfi.alLogBufA;
                    lfi.currLogBuff = 'B';
                }else{
                    alWrtLog = lfi.alLogBufB;
                    lfi.currLogBuff = 'A';
                }
                lfi.currCacheSize = 0;
            }
            //创建日志文件
            createLogFile(lfi);
            //输出日志
            int iWriteSize = writeToFile(lfi.fullLogFileName,alWrtLog);
            lfi.currLogSize += iWriteSize;
        }
    }      
}

多 RollingFile 机制

同 log4j/logback,FLogger 也支持多种 RollingFile 机制:

  • 按文件大小 Rolling
  • 按天 Rolling

其中按文件大小 Rolling,配置项为:

1
2
# 单个日志文件的大小(默认为10M)
SINGLE_LOG_FILE_SIZE = 10485760

即当文件大小超过配置大小时,将创建新的文件记录日志,同时重命名旧文件为”日志文件名_日期_时间.log”(如 info_20161208_011105.log)。

按天 Rolling 即每天产生不同的文件。

产生的日志文件列表可参考如下:

1
2
3
4
5
info_20161207_101105.log
info_20161207_122010.log
info_20161208_011110.log
info_20161208_015010.log
info.log

 当前正在写入的日志文件为 info.log。

关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 创建日志文件
* @param lfi
*/
private void createLogFile(LogFileItem lfi){
    //当前系统日期
    String currPCDate = TimeUtil.getPCDate('-');
        
    //如果超过单个文件大小,则拆分文件
    if(lfi.fullLogFileName != null && lfi.fullLogFileName.length() > 0 && lfi.currLogSize >= LogManager.SINGLE_LOG_FILE_SIZE ){
        File oldFile = new File(lfi.fullLogFileName);
        if(oldFile.exists()){
            String newFileName = Constant.CFG_LOG_PATH + "/" + lfi.lastPCDate + "/" + lfi.logFileName + "_" + TimeUtil.getPCDate() + "_"+ TimeUtil.getCurrTime() + ".log";
            File newFile = new File(newFileName);
            boolean flag = oldFile.renameTo(newFile);
            System.out.println("日志已自动备份为 " + newFile.getName() + ( flag ? "成功!" : "失败!" ) );
            lfi.fullLogFileName = "";
            lfi.currLogSize = 0;
        }
    }
    //创建文件
    if ( lfi.fullLogFileName == null || lfi.fullLogFileName.length() <= 0 || lfi.lastPCDate.equals(currPCDate) == false ){
        String sDir = Constant.CFG_LOG_PATH + "/" + currPCDate ;
        File file = new File(sDir);
        if(file.exists() == false){
            file.mkdir();
        }
        lfi.fullLogFileName = sDir + "/" + lfi.logFileName + ".log";
        lfi.lastPCDate = currPCDate;
            
        file = new File(lfi.fullLogFileName);
        if(file.exists()){
            lfi.currLogSize = file.length();
        }else{
            lfi.currLogSize = 0;
        }
    }
}

多日志级别

FLogger 支持多种日志级别:

  • DEBUG
  • INFO
  • WARN
  • ERROR
  • FATAL

FLogger 为每个日志级别都提供了简易 API,在此就不再赘述了。

打印 error 和 fatal 级别日志时,FLogger 默认会将日志内容输出到控制台。

热加载

FLogger 支持热加载,FLogger 内部并没有采用事件驱动方式(即新增、修改和删除配置文件时产生相关事件通知 FLogger 实时热加载),而是以固定频率的方式进行热加载,具体实现就是每执行完100次刷盘后才进行热加载(频率可调),关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int i = 0;
while(bIsRun){
    try{
        //等待一定时间
        Thread.sleep(200);
        //输出到文件
        flush(false);
        //重新获取日志级别
        if(i++ % 100 == 0){
            Constant.CFG_LOG_LEVEL = CommUtil.getConfigByString("LOG_LEVEL","0,1,2,3,4");
            //其他配置项热加载......
            i = 1;
        }
    }catch(Exception e){
        System.out.println("开启日志服务错误...");
        e.printStackTrace();
    }
}

这么做完全是为了保持代码的精简和功能的纯粹性。事件驱动热加载无疑是更好的热加载方式,但需要新建额外的线程并启动对配置文件的事件监听,有兴趣的童鞋可自行实现。

性能保证

FLogger 成功支撑了日交易额百亿级交易系统的日志服务,它的性能是经历过考验的。下面我们就来拿 FLogger 跟 log4j 做个简单的性能对比。

测试环境:Intel(R) Core(TM) i5-3470 CPU @ 3.20GHz  3.20 GHz   4.00 GB Memory   64位操作系统

测试场景:单条记录72byte   共1000000条   写单个日志文件

FLogger 配置如下:

1
2
3
4
5
6
# 日志写入文件的间隔时间
WRITE_LOG_INV_TIME = 0
# 单个日志文件的大小
SINGLE_LOG_FILE_SIZE = 104857600
# 单个日志文件缓存的大小
SINGLE_LOG_CACHE_SIZE = 0

以上配置保证所有日志写入到单个文件,且尽量保证每一条记录不在内存中缓存,减少测试误差。

测试代码:

1
2
3
4
5
6
7
8
9
FLogger logger = FLogger.getInstance();    //FLogger
//Logger logger = Logger.getLogger(Log4jTest.class);   //log4j
String record = "Performance Testing about log4j and cyfonly customized java project log.";   //72字节
long st = System.currentTimeMillis();
for(int i=0; i<1000000; i++){
   logger.info(record);
}
long et = System.currentTimeMillis();
System.out.println("FLogger/log4j write 1000000 records with each record 72 bytes, cost :" + (et - st) + " millseconds");

日志内容:

1
2
3
4
5
FLogger:
[INFO] 2016-12-06 21:40:06:842 [main] Performance Testing about log4j and cyfonly customized java project log.
log4j:
[INFO ]2016-12-06 21:41:12,852, [main]Log4jTest:12, Performance Testing about log4j and cyfonly customized java project log.

测试结果(执行10次取平均值):

1
2
FLogger write 1000000 records with each record 72 bytes, cost :2144 millseconds
log4j write 1000000 records with each record 72 bytes, cost :cost :12691 millseconds

 说明:测试结果为日志全部刷盘成功的修正时间,加上各种环境的影响,有少许误差,在此仅做简单测试,并不是最严格最公平的测试对比。有兴趣的童鞋可进行精确度更高的测试。欢迎私下探讨,本人QQ:869827095。

 

如果觉得文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
打赏支持
作者:cyfonly
本文版权归作者和博客园共有,欢迎转载,未经同意须保留此段声明,且在文章页面明显位置给出原文连接。欢迎指正与交流。

YAML 语言教程

作者: 阮一峰

日期: 2016年7月 4日

编程免不了要写配置文件,怎么写配置也是一门学问。

YAML 是专门用来写配置文件的语言,非常简洁和强大,远比 JSON 格式方便。

本文介绍 YAML 的语法,以 JS-YAML 的实现为例。你可以去在线 Demo 验证下面的例子。

一、简介

YAML 语言(发音 /ˈjæməl/ )的设计目标,就是方便人类读写。它实质上是一种通用的数据串行化格式。

它的基本语法规则如下。

  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进时不允许使用Tab键,只允许使用空格。
  • 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可

# 表示注释,从这个字符一直到行尾,都会被解析器忽略。

YAML 支持的数据结构有三种。

  • 对象:键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary)
  • 数组:一组按次序排列的值,又称为序列(sequence) / 列表(list)
  • 纯量(scalars):单个的、不可再分的值

以下分别介绍这三种数据结构。

二、对象

对象的一组键值对,使用冒号结构表示。


animal: pets

转为 JavaScript 如下。


{ animal: 'pets' }

Yaml 也允许另一种写法,将所有键值对写成一个行内对象。


hash: { name: Steve, foo: bar } 

转为 JavaScript 如下。


{ hash: { name: 'Steve', foo: 'bar' } }

三、数组

一组连词线开头的行,构成一个数组。


- Cat
- Dog
- Goldfish

转为 JavaScript 如下。


[ 'Cat', 'Dog', 'Goldfish' ]

数据结构的子成员是一个数组,则可以在该项下面缩进一个空格。


-
 - Cat
 - Dog
 - Goldfish

转为 JavaScript 如下。


[ [ 'Cat', 'Dog', 'Goldfish' ] ]

数组也可以采用行内表示法。


animal: [Cat, Dog]

转为 JavaScript 如下。


{ animal: [ 'Cat', 'Dog' ] }

四、复合结构

对象和数组可以结合使用,形成复合结构。


languages:
 - Ruby
 - Perl
 - Python 
websites:
 YAML: yaml.org 
 Ruby: ruby-lang.org 
 Python: python.org 
 Perl: use.perl.org 

转为 JavaScript 如下。


{ languages: [ 'Ruby', 'Perl', 'Python' ],
  websites: 
   { YAML: 'yaml.org',
     Ruby: 'ruby-lang.org',
     Python: 'python.org',
     Perl: 'use.perl.org' } }

五、纯量

纯量是最基本的、不可再分的值。以下数据类型都属于 JavaScript 的纯量。

  • 字符串
  • 布尔值
  • 整数
  • 浮点数
  • Null
  • 时间
  • 日期

数值直接以字面量的形式表示。


number: 12.30

转为 JavaScript 如下。


{ number: 12.30 }

布尔值用truefalse表示。


isSet: true

转为 JavaScript 如下。


{ isSet: true }

null~表示。


parent: ~ 

转为 JavaScript 如下。


{ parent: null }

时间采用 ISO8601 格式。


iso8601: 2001-12-14t21:59:43.10-05:00 

转为 JavaScript 如下。


{ iso8601: new Date('2001-12-14t21:59:43.10-05:00') }

日期采用复合 iso8601 格式的年、月、日表示。


date: 1976-07-31

转为 JavaScript 如下。


{ date: new Date('1976-07-31') }

YAML 允许使用两个感叹号,强制转换数据类型。


e: !!str 123
f: !!str true

转为 JavaScript 如下。


{ e: '123', f: 'true' }

六、字符串

字符串是最常见,也是最复杂的一种数据类型。

字符串默认不使用引号表示。


str: 这是一行字符串

转为 JavaScript 如下。


{ str: '这是一行字符串' }

如果字符串之中包含空格或特殊字符,需要放在引号之中。


str: '内容: 字符串'

转为 JavaScript 如下。


{ str: '内容: 字符串' }

单引号和双引号都可以使用,双引号不会对特殊字符转义。


s1: '内容\n字符串'
s2: "内容\n字符串"

转为 JavaScript 如下。


{ s1: '内容\\n字符串', s2: '内容\n字符串' }

单引号之中如果还有单引号,必须连续使用两个单引号转义。


str: 'labor''s day' 

转为 JavaScript 如下。


{ str: 'labor\'s day' }

字符串可以写成多行,从第二行开始,必须有一个单空格缩进。换行符会被转为空格。


str: 这是一段
  多行
  字符串

转为 JavaScript 如下。


{ str: '这是一段 多行 字符串' }

多行字符串可以使用|保留换行符,也可以使用>折叠换行。


this: |
  Foo
  Bar
that: >
  Foo
  Bar

转为 JavaScript 代码如下。


{ this: 'Foo\nBar\n', that: 'Foo Bar\n' }

+表示保留文字块末尾的换行,-表示删除字符串末尾的换行。


s1: |
  Foo

s2: |+
  Foo


s3: |-
  Foo

转为 JavaScript 代码如下。


{ s1: 'Foo\n', s2: 'Foo\n\n\n', s3: 'Foo' }

字符串之中可以插入 HTML 标记。


message: |

  <p style="color: red">
    段落
  </p>

转为 JavaScript 如下。


{ message: '\n<p style="color: red">\n  段落\n</p>\n' }

七、引用

锚点&和别名*,可以用来引用。


defaults: &defaults
  adapter:  postgres
  host:     localhost

development:
  database: myapp_development
  <<: *defaults

test:
  database: myapp_test
  <<: *defaults

等同于下面的代码。


defaults:
  adapter:  postgres
  host:     localhost

development:
  database: myapp_development
  adapter:  postgres
  host:     localhost

test:
  database: myapp_test
  adapter:  postgres
  host:     localhost

&用来建立锚点(defaults),<<表示合并到当前数据,*用来引用锚点。

下面是另一个例子。


- &showell Steve 
- Clark 
- Brian 
- Oren 
- *showell 

转为 JavaScript 代码如下。


[ 'Steve', 'Clark', 'Brian', 'Oren', 'Steve' ]

八、函数和正则表达式的转换

这是 JS-YAML 库特有的功能,可以把函数和正则表达式转为字符串。


# example.yml
fn: function () { return 1 }
reg: /test/

解析上面的 yml 文件的代码如下。


var yaml = require('js-yaml');
var fs   = require('fs');

try {
  var doc = yaml.load(
    fs.readFileSync('./example.yml', 'utf8')
  );
  console.log(doc);
} catch (e) {
  console.log(e);
}

从 JavaScript 对象还原到 yaml 文件的代码如下。


var yaml = require('js-yaml');
var fs   = require('fs');

var obj = {
  fn: function () { return 1 },
  reg: /test/
};

try {
  fs.writeFileSync(
    './example.yml',
    yaml.dump(obj),
    'utf8'
  );
} catch (e) {
  console.log(e);
}

九、参考链接

(完)