C--2012-说明指南-全-

C# 2012 说明指南(全)

原文:Illustrated C# 2012

协议:CC BY-NC-SA 4.0

零、简介

这本书的目的是以尽可能清晰的方式教你 C# 编程语言的语法和语义。C# 是一门很棒的编程语言!我喜欢在里面编码。我不知道这些年我学了多少编程语言,但 C# 是我迄今为止最喜欢的。我希望通过使用这本书,你可以欣赏 C# 的美丽和优雅。

大多数书籍主要使用文本来教授编程。这对小说来说很棒,但是编程语言的许多重要概念可以通过文字、图形和表格的组合来最好地理解。

我们中的许多人都是视觉思维,数字和表格可以帮助我们澄清和明确对一个概念的理解。在几年的编程语言教学中,我发现我在白板上画的图片是最能快速帮助学生理解我试图传达的概念的东西。然而,仅有插图不足以解释编程语言和平台。这本书的目标是找到文字和插图的最佳组合,让你彻底理解这门语言,并让这本书作为参考资源。

这本书是为任何想了解 C# 编程语言的人写的——从新手到经验丰富的程序员。对于那些刚刚开始编程的人,我已经包括了基础知识。对于经验丰富的程序员来说,内容简洁明了,并且以一种允许您直接找到所需信息的形式,而不必费力地阅读大量文字。对于这两组程序员来说,内容本身是以图形化的方式呈现的,这种形式应该使语言易于学习。

好好享受!

受众、源代码和联系信息

这本书是为初级和中级程序员以及来自另一种语言如 Visual Basic 或 Java 的程序员编写的。我试图将重点放在 C# 语言本身,并对该语言及其所有部分进行深入的描述,而不是偏离主题去讨论 .NET 或编程实践。我想让这本书尽可能简洁,同时仍然全面地涵盖语言——还有其他一些涵盖这些其他主题的好书。

您可以从 Apress 网站或本书的网站(www.illustratedcsharp.com)下载所有示例程序的源代码。虽然我不能回答关于你的代码的具体问题,但是你可以通过dansolis@sbcglobal.net.联系我,给我关于这本书的建议和反馈

我希望这本书能让你觉得学习 C# 是一种享受!保重。

丹·索利斯

一、C# 和 .NET 框架

以前。网

C# 编程语言是为微软开发程序而设计的 .NET 框架。本章简要介绍了 .NET 的起源及其基本架构。首先,让我们把名字弄对:C# 读作“see sharp” 1

20 世纪 90 年代末的 Windows 编程

在 20 世纪 90 年代后期,使用微软平台的 Windows 编程已经分裂成许多分支。大多数程序员使用 Visual Basic、C 或 C++。一些 C 和 C++程序员使用原始的 Win32 API,但大多数使用微软基础类(MFC)。其他人已经转向组件对象模型(COM)。

所有这些技术都有自己的问题。原始的 Win32 API 不是面向对象的,使用它比 MFC 需要更多的工作。MFC 是面向对象的,但是不一致而且越来越老了。虽然概念上很简单,但实际编码很复杂,需要大量丑陋、不优雅的管道。

所有这些编程技术的另一个缺点是,它们的主要目标是为桌面而不是互联网开发代码。当时,网络编程是事后的想法,似乎与桌面编程非常不同。

下一代平台服务的目标

我们真正需要的是一个新的开始——一个集成的、面向对象的开发框架,它将把一致性和优雅带回编程。为了满足这种需求,微软着手开发代码执行环境和满足这些目标的代码开发环境。图 1-1 列出了这些目标。

Image

***图 1-1。*下一代平台的目标

进入微软。网

2002 年,微软发布了 .NET 框架,它承诺解决老问题并满足下一代系统的目标。那个。与 MFC 或 COM 编程技术相比,NET Framework 是一个更加一致和面向对象的环境。它的一些特性包括:


有一次,我参加了一个 C# 合同职位的面试,人力资源面试官问我在“see pound”(而不是“see sharp”)有多少编程经验!我过了一会儿才意识到他在说什么。

  • Multi-platform: This system runs on a wide range of computers, from servers and desktops to PDAs and mobile phones.
  • Industry standard: The system uses industry standard communication protocols, such as XML, HTTP, SOAP, JSON, WSDL, etc.
  • Security : Even if there is code obtained from suspicious sources, the system can provide a safer execution environment.

的组成部分 .NET 框架

那个 .NET 框架由三个组件组成,如图图 1-2 所示。执行环境称为公共语言运行时(CLR)。CLR 在运行时管理程序执行,包括以下内容:

  • Memory management and garbage collection
  • Code security verification
  • Code execution, thread management and exception handling

编程工具包括编码和调试所需的一切,包括:

  • Visual Studio integrated development environment. NET compatible compiler (for example, C#, Visual Basic. NET, F#, IronRuby, managed C++)
  • debugger
  • Web development server-side technologies, such as ASP.NET and WCF

基类库(BCL)是一个大型类库,由 .NET Framework,也可供您在自己的程序中使用。

Image

***图 1-2。*的组成部分 .NET 框架

一个改进的编程环境

那个。与以前的 Windows 编程环境相比,NET Framework 为程序员提供了相当大的改进。以下部分简要概述了它的功能和优点。

面向对象的开发环境

CLR、BCL 和 C# 被设计成完全面向对象的,并作为一个良好集成的环境。

该系统为本地程序和分布式系统提供了一致的、面向对象的编程模型。它还为桌面应用编程、移动应用编程和 web 开发提供了软件开发接口,在从服务器到手机的广泛目标上保持一致。

自动垃圾收集

CLR 有一个称为垃圾收集器(GC)的服务,它会自动为您管理内存。

GC 自动从内存中移除你的程序不再访问的对象。

GC 将程序员从传统上必须执行的任务中解放出来,例如释放内存和寻找内存泄漏。这是一个巨大的改进,因为寻找内存泄漏可能很困难且耗时。

互用性

那个 .NET Framework 是为不同的 .NET 语言、操作系统或 Win32 DLLs 以及 COM。

  • This. NET language interoperability allows different. NET language seamless interaction.
    • A program written by one person. NET language can use or even inherit classes written in another language. Internet, as long as you follow certain rules.
    • Because it can easily integrate modules generated by different programming languages. The. NET Framework is sometimes described as language agnostic.
  • This. The. NET Framework provides a feature named platform call (p/invoke) , which allows for It can use the original C functions imported from standard Win32 DLLs, such as Windows APIs.
  • This. The. NET Framework also allows interoperability with COM. . NET framework software COMponents can call COM components, and com components can call. NET components as if they were COM components themselves.
不需要 COM

那个 .NET Framework 将程序员从 COM 传统中解放出来。如果您来自 COM 编程环境,您会很高兴知道,作为一名 C# 程序员,您不需要使用以下任何一项:

  • IUnknown Interface : In COM, all objects must implement interface IUnknown. By contrast, all. NET object comes from a class named object. The interface is still programmed. NET, but this is no longer the central theme.
    ** Type library : In COM, type information is stored in the type library as a .tlb file, which is separate from executable code. NET, the type information of the program is bundled with the code in the program file.* Manual reference counting : In COM, programmers must record the number of references to an object to ensure that it is not deleted at the wrong time. NET, GC tracks references and removes objects only when appropriate.* HRESULT: com used the HRESULT data type to return the runtime error code. . NET does not use HRESULT s, on the contrary, all unexpected runtime errors will generate exceptions.* Registry : COM applications must be registered in the system registry, which stores the configuration information of the operating system and applications. . NET applications do not need to use the registry. This simplifies the installation and deletion of programs. (However, there is a similar thing called Global Assembly Cache , which I will introduce in Chapter 21 of . )*

*尽管目前正在编写的 COM 代码数量相当少,但在目前正在使用的系统中仍然有相当数量的 COM 组件,C# 程序员有时需要编写与这些组件接口的代码。C# 4.0 引入了几个新特性,使得这项任务变得更加容易。这些功能在第二十五章中有所介绍。

简化部署

部署为 .NET Framework 可以比以前容易得多,原因如下:

  • Facts. NET programs don't need to be registered in the registry, which means that in the simplest case, a program only needs to be copied to the target machine to run.
  • . NET provides a parallel execution feature named , which allows different versions of a DLL to exist on the same machine. This means that every executable file can access the version of the DLL built for it.
类型安全

CLR 检查并确保参数和其他数据对象的类型安全,即使是在用不同编程语言编写的组件之间。

基类库

的 .NET 框架提供了一个广泛的基础类库,毫不奇怪,叫做基础 类库(BCL) 。(它有时也被称为框架类库[FCL])。在编写自己的程序时,您可以使用这些丰富的可用代码。一些类别如下:

  • Universal base class : A class that provides you with a set of extremely powerful tools for various programming tasks, such as file operation, string operation, security and encryption.
  • Set class : A class that implements lists, dictionaries, hash tables and bit arrays.
  • Thread and synchronization class : a class used to build multithreaded programs.
  • XML class : a class used to create, read and manipulate XML documents.

编译成通用的中间语言

. NET 语言的编译器获取一个源代码文件,并生成一个名为程序集的输出文件。图 1-3 说明了这个过程。

  • The assembly is either an executable file or a DLL.
  • The code in the assembly is not native code, but an intermediate language called Universal Intermediate Language (CIL) .
  • An assembly contains, among other things, the following items:
    • CIL of program
    • Metadata about the types used in the program
    • Metadata about references to other assemblies

Image

***图 1-3。*编译过程

Image 注意中间语言的首字母缩略词随着时间的推移发生了变化,不同的引用使用不同的术语。您可能会遇到的 CIL 的另外两个术语是中间语言(IL)和微软中间语言(MSIL)。这些术语在 .NET 的最初开发和早期文档,尽管它们现在用得不那么频繁了。

编译为本机代码并执行

在被调用运行之前,程序的 CIL 不会被编译成本机代码。在运行时,CLR 执行以下步骤,如图 1-4 所示:

  • Check the safety features of components.
  • It allocates space in memory.
  • It sends the compiled executable code to a just-in-time (JIT) compiler, which compiles part of the code into native code.

JIT 编译器只在需要时才编译程序集中的可执行代码。然后,它被缓存起来,以备程序稍后再次执行时使用。使用这个过程意味着在执行过程中没有被调用的代码不会被编译成本机代码,而被调用的代码只需要编译一次。

Image

***图 1-4。*编译成本机代码发生在运行时。

一旦 CIL 编译成本机代码,CLR 就在它运行时管理它,执行诸如释放孤立内存、检查数组边界、检查参数类型和管理异常之类的任务。这引出了两个重要术语:

  • Managed code : Code written for managed code. NET framework is called managed code and needs CLR.
  • unmanaged code : code that does not run under the control of CLR, such as Win32 C and c++ dll, is called unmanaged code.

微软还提供了一个名为原生图像生成器Ngen 的工具,它获取一个程序集并为当前处理器生成原生代码。通过 Ngen 运行的代码避免了运行时的 JIT 编译过程。

编译和执行概述

不管原始源文件的语言是什么,都遵循相同的编译和执行过程。图 1-5 展示了用不同语言编写的三个程序的整个编译和运行过程。

Image

***图 1-5。*编译时和运行时进程概述

公共语言运行时

的核心组件 .NET Framework 就是 CLR,它位于操作系统之上,管理程序执行,如图图 1-6 所示。CLR 还提供以下服务:

  • Automatic garbage collection
  • And security authentication.
  • Access BCL to realize a wide range of programming functions, including web services and data services.

Image

***图 1-6。*CLR 概述

通用语言基础设施

每种编程语言都有一组内在的类型,用来表示整数、浮点数、字符等对象。从历史上看,这些类型的特征因编程语言和平台的不同而不同。例如,根据语言和平台的不同,构成整数的位数有很大的不同。

然而,如果我们想让程序与用不同语言编写的其他程序和库很好地配合,这种缺乏统一性的情况会很困难。要有秩序和合作,就要有一套标准。

公共语言基础结构(CLI)是一组标准,它将 .NET 框架整合成一个紧密的、一致的系统。它展示了系统的概念和架构,并指定了所有软件必须遵守的规则和约定。图 1-7 显示了 CLI 的组成部分。

Image

***图 1-7。*CLI 的组件

CLI 和 C# 都已被 Ecma International 批准为开放的国际标准规范。(“Ecma”这个名字曾经是欧洲计算机制造商协会的首字母缩写,但现在它本身只是一个词。Ecma 成员包括微软、IBM、惠普、Adobe 和许多其他与计算机和消费电子产品相关的公司。

CLI 的重要部分

虽然大多数程序员不需要了解 CLI 规范的细节,但您至少应该熟悉通用类型系统(CTS)和通用语言规范(CLS)的含义和用途。

通用类型系统

CTS 定义了必须在托管代码中使用的类型的特征。cts 的一些重要方面如下:

  • CTS defines a group of rich intrinsic types, each of which has fixed and specific characteristics.
  • Types provided by. NET-compatible programming languages usually map to a specific subset of this defined set of internal types.
  • One of the most important features of CTS is that all types of and are derived from a common base class called object.
  • Using CTS ensures that system types and user-defined types can be used by anyone. Comply with the language of. NET.
公共语言规范

CLS 指定了符合. NET 的编程语言的规则、属性和行为。主题包括数据类型、类构造和参数传递。

复习缩略词

这一章已经讲了很多 .NET 首字母缩略词,所以图 1-8 会帮助你把它们弄清楚。

Image

***图 1-8。*美国。网络缩略语

c# 的进化

该语言的当前版本是 5.0 版。这种语言的每一个新版本都有一个特别关注的新特性。版本 5.0 中新特性的重点是异步编程。我将在第二十章中详细介绍这些特性。

图 1-9 显示了该语言每个版本的主要特性焦点,以及包含该材料的章节。

Image

***图 1-9。C # 版本特性集的焦点

二、C# 编程概述

一个简单的 C# 程序

这一章是学习 C# 的基础。由于我将在本文中广泛使用代码示例,我首先需要向您展示 C# 程序的样子及其各个部分的含义。

我将首先演示一个简单的程序,并逐一解释它的组件。这将介绍一系列主题,从 C# 程序的结构到将程序输出到屏幕的方法。

有了这些源代码预备知识,我就可以在本文的其余部分自由地使用代码示例了。所以,不像下面的章节,其中一个或两个主题被详细讨论,这一章涉及许多主题,只有最低限度的解释。

让我们先看一个简单的 C# 程序。该程序的完整源代码显示在图 2-1 左上角的阴影区域。如图所示,代码包含在一个名为SimpleProgram.cs的文本文件中。当你通读它的时候,不要担心理解所有的细节。表 2-1 给出了代码的逐行描述。图左下方的阴影区域显示了程序的输出。图的右边部分是程序各部分的图形描述。

  • When code compilation is executed, the string "Hi there!" is displayed. In a window on the screen.
  • Line 5 contains two consecutive slash characters. The compiler ignores these characters and everything that follows them in the line. This is called single-line annotation .

Image

***图 2-1。*简单程序程序

关于简单程序的更多信息

C# 程序由一个或多个类型声明组成。本书的大部分内容是解释你可以在程序中创建和使用的不同类型。程序中的类型可以以任何顺序声明。在SimpleProgram示例中,只声明了一个class类型。

名称空间是一组与名称相关的类型声明。SimpleProgram使用两个名称空间。它创建了一个名为Simple的新名称空间,在这个名称空间中它声明了自己的类型(类Program,并使用了在名为System的名称空间中定义的Console类。

要编译程序,可以使用 Visual Studio 或命令行编译器。要以最简单的形式使用命令行编译器,请在命令窗口中使用以下命令:

   csc SimpleProgram.cs

在这个命令中,csc是命令行编译器的名称,SimpleProgram.cs是源文件的名称。CSC 代表“C-Sharp 编译器”

标识符

标识符是字符串,用于命名变量、方法、参数和一系列其他编程结构,这些将在后面介绍。

您可以通过使用大小写字母将有意义的单词连接成一个描述性名称来创建自文档化的标识符(例如,CardDeckPlayersHandFirstNameSocialSecurityNum)。标识符中的某些位置允许或不允许使用某些字符。图 2-2 说明了这些规则。

  • And underscores (a to z, A to Z and _) are allowed to appear in any position.
  • Numbers are not allowed in the first position, and all other positions are allowed.
  • The @ character is allowed to appear in the first position of the identifier, but not in any other position. Although it is allowed, it is usually discouraged.

Image

***图 2-2。*标识符中允许的字符

标识符区分大小写。例如,变量名myVarMyVar是不同的标识符。

例如,在下面的代码片段中,变量声明都是有效的,并且声明了不同的整数变量。但是使用这样相似的名字会使编码更容易出错,调试更困难。那些稍后调试你的代码的人会不高兴的。

   // Valid syntactically, but very confusing! int totalCycleCount; int TotalCycleCount; int TotalcycleCount;

我将在第七章中描述推荐的 C# 命名约定。

关键词

关键字是用来定义 C# 语言的字符串记号。表 2-2 给出了 C# 关键字的完整列表。

关于关键词,需要了解的一些重要事项如下:

  • Keyword cannot be used as variable name or any other form of identifier unless it begins with @ character.
  • All C# keywords are all composed of lowercase letters. (.However, NET type names use Pascal case. )

Image

上下文关键字是仅在特定语言结构中充当关键字的标识符。在这些位置上,它们有特殊的意义;但是与不能用作标识符的关键字不同,上下文关键字可以在代码的其他部分用作标识符。表 2-3 包含上下文关键词列表。

Image

Main:程序的起点

每个 C# 程序都必须有一个包含名为Main的方法的类。在前面显示的SimpleProgram程序中,它在一个名为Program的类中声明。

  • The execution starting point of every C# program is at the first instruction in Main.

Main

Main最简单的形式如下:

    static void Main( ) { *Statements* }

空白

程序中的空白是指没有可见输出字符的字符。编译器会忽略源代码中的空白,但程序员会用它来使代码更加清晰易读。一些空白字符包括以下内容:

  • blank
  • tabs
  • line feed
  • return

例如,尽管以下代码片段在外观上有所不同,但它们被编译器完全相同地对待。

`   // Nicely formatted
Main()
{
Console.WriteLine("Hi, there!");
}

// Just concatenated
Main(){Console.WriteLine("Hi, there!");}`

报表

C# 中的语句与 C 和 C++中的语句非常相似。本节介绍了报表的一般形式;具体报表见第九章。

语句是描述一个类型或者告诉程序执行一个动作的源代码指令。

  • A simple sentence is terminated by a semicolon by .

例如,下面的代码是两个简单语句的序列。第一条语句定义了一个名为var1的整数变量,并将其值初始化为5。第二条语句将变量var1的值打印到屏幕上的一个窗口中。

   int var1 = 5; System.Console.WriteLine("The value of var1 is {0}", var1);

街区

是由一组匹配的花括号括起来的零个或多个语句的序列;它作为一个单一的句法陈述。

通过将前面示例中的两个语句用匹配的大括号括起来,可以从这组语句中创建一个块,如下面的代码所示:

   { int var1 = 5; System.Console.WriteLine("The value of var1 is {0}", var1); }

关于块,需要了解的一些重要信息如下:

只要语法需要一个语句,但是你需要的动作需要不止一个简单的语句,你就可以使用一个块。* Some program constructs require blocks. In these constructions, simple statements cannot replace blocks.* Although a simple statement ends with a semicolon, there is no semicolon after a block. (Actually, the compiler will allow it because it is parsed as an empty statement-but this is not a good style. )

    {        Terminating semicolon ↓                                    Terminating semicolon int var2 = 5;                                           ↓ System.Console.WriteLine("The value of var1 is {0}", var1); } ↑  No terminating semicolon

从程序中输出文本

一个控制台窗口是一个简单的命令提示窗口,允许程序显示文本并从键盘接收输入。BCL 提供了一个名为Console(在System名称空间中)的类,它包含向控制台窗口输入和输出数据的方法。

WriteConsole类的成员。它向程序的控制台窗口发送一个文本字符串。最简单的形式是,Write向窗口发送一个文本字符串。字符串必须用引号括起来——双引号,而不是单引号

下面一行代码展示了一个使用Write成员的例子:

   Console.Write("<ins>This is trivial text.</ins>"); ↑ Output string

此代码在控制台窗口中产生以下输出:


This is trivial text.


另一个例子是下面的代码,它向程序的控制台窗口发送三个文本字符串:

   System.Console.Write ("This is text1\. "); System.Console.Write ("This is text2\. "); System.Console.Write ("This is text3\. ");

这段代码产生了下面的输出。请注意,Write没有在字符串后面添加换行符,所以三个语句的输出在一行中一起运行。

<ins>This is text1.</ins>  <ins>This is text2.</ins>  <ins>This is text3.</ins> ↑               ↑               ↑ First           Second           Third statement          statement          statement

WriteLine

WriteLineConsole的另一个成员;它执行与Write相同的功能,但是在每个输出字符串的末尾附加一个换行符。

例如,如果您使用前面的代码,用WriteLine替换Write,则输出在单独的行上:

   System.Console.WriteLine("This is text1."); System.Console.WriteLine("This is text2."); System.Console.WriteLine("This is text3.");

这段代码在控制台窗口中产生以下输出:


This is text1. This is text2. This is text3.


格式字符串

WriteWriteLine语句的一般形式可以接受多个参数。

  • If there are multiple parameters, separate them with commas.
  • The first parameter must always be a string, which is called format string . The format of the string can contain the substitution mark .
    • Substitution marks the position in the format string where a value should be substituted in the output string.
    • The replacement tag consists of an integer contained in a set of matching curly braces. Integer is the numeric position of the replacement value to be used. The parameter after the format string is called substitute value . These replacement values are numbered from 0.

语法如下:

   Console.WriteLine( FormatString, SubVal0, SubVal1, SubVal2, ... );

例如,下面的语句有两个替换标记,编号为01,还有两个替换值,它们的值分别是36

                                            Substitution  markers ↓       ↓     Console.WriteLine(<ins>"Two sample integers are {0} and {1}."</ins>, <ins>3, 6</ins>); ↑                       ↑ Format string             Substitution values

这段代码在屏幕上产生以下输出:


Two sample integers are 3 and 6.


多个标记和值

在 C# 中,可以使用任意数量的标记和任意数量的值。

  • These values can be used in any order.
  • The values in the format string can be replaced any number of times.

例如,下面的语句使用三个标记和两个值。注意,值1用在值0之前,值1用了两次。

   Console.WriteLine("Three integers are {1}, {0} and {1}.", 3, 6);

该代码产生以下输出:


   Three integers are 6, 3 and 6.


标记不得试图引用超出替换值列表长度的位置的值。如果是这样,它将不会产生编译错误,而是产生运行时错误(称为异常)。

例如,在下面的语句中,有两个替换值,位置分别为01。然而,第二个标记引用了不存在的位置2。这将产生运行时错误。

                                                Position 0    Position 1 ↓          ↓ Console.WriteLine("Two integers are {0} and {2}.", 3   6);    // Error! ↑ There is no position 2 value.

格式化数字字符串

在本文中,示例代码将使用WriteLine方法来显示值。通常,它将使用简单的替换标记,仅由整数周围的花括号组成。

然而,很多时候,在您自己的代码中,您会希望用一种比普通数字更合适的格式来表示文本字符串的输出。例如,您可能希望将一个值显示为货币或具有特定小数位数的定点值。您可以通过使用格式字符串来完成这些事情。

例如,下面的代码由两条打印出值500的语句组成。第一行打印出没有任何附加格式的数字。在第二行中,格式字符串指定应将数字格式化为货币。

   Console.WriteLine("The value: {0}."  , 500);        // Print out number Console.WriteLine("The value: {0:C}.", 500);        // Format as currency ↑ Format as currency

该代码产生以下输出:


The value: 500. The value: $500.00.


这两个语句的区别在于格式化项在格式说明符中包含附加信息。格式说明符的语法由花括号内的三个字段组成:索引、对齐说明符和格式字段。图 2-3 显示了语法。

Image

***图 2-3。*格式说明符的语法

格式说明符中的第一件事是索引。如您所知,索引指定了格式字符串后面的列表中的哪一项应该被格式化。索引是必需的,列表项的编号从 0 开始。

对齐说明符

对齐说明符用字符表示字段的最小宽度。对齐说明符具有以下特征:

  • The alignment specifier is optional, separated from the index by commas.

  • Integer indicates the minimum number of characters used in this field.

  • Symbols represent right or left alignment. A positive number indicates right alignment; Negative numbers indicate left alignment.

          Index—use 0th item in the list ↓ Console.WriteLine("{0, 10}", 500); ↑ Alignment specifier—right-align in a field of ten characters

例如,下面的代码格式化了变量myInt的值,显示了两个格式项。在第一种情况下,myInt的值显示为右对齐的十个字符的字符串。在第二种情况下,它是左对齐的。格式项位于两个竖线之间,这样在输出中您可以看到字符串每一边的限制。

   int myInt = 500; Console.WriteLine("|{0, 10}|", myInt);                 // Aligned right Console.WriteLine("|{0,-10}|", myInt);                 // Aligned left

这段代码产生以下输出:竖线之间有十个字符:


|       500| |500       |


该值的实际表示形式可能比对齐说明符中指定的字符更多或更少:

  • If there are fewer characters than those specified in the alignment specifier, the remaining characters are filled with spaces.
  • If the notation uses more characters than specified, the alignment specifier is ignored and the notation uses the required number of characters.
格式字段

格式字段指定数字表示应该采用的形式。比如应该用货币表示,十进制格式,十六进制格式,还是定点记数法?

格式字段由三部分组成,如图图 2-4 所示:

  • The colon character must follow the format specifier with no spaces in the middle.
  • Format specifier is a single-letter character from a set of nine built-in character formats. Characters can be uppercase or lowercase. This situation is important for some descriptors, but not for others.
  • Precision specifier is optional and consists of one or two digits. Its actual meaning depends on the format specifier.

Image

***图 2-4。*标准格式字段字符串

下面的代码显示了格式字符串组件的语法示例:

          Index—use 0th item in the list ↓ Console.WriteLine("{0:F4}", 12.345678); ↑ Format component—fixed-point, four decimal places

以下代码显示了不同格式字符串的示例:

   double myDouble = 12.345678; Console.WriteLine("{0,-10:G} -- General",                      myDouble); Console.WriteLine("{0,-10} -- Default, same as General",       myDouble); Console.WriteLine("{0,-10:F4} -- Fixed Point, 4 dec places",   myDouble); Console.WriteLine("{0,-10:C} -- Currency",                     myDouble); Console.WriteLine("{0,-10:E3} -- Sci. Notation, 3 dec places", myDouble); Console.WriteLine("{0,-10:x} -- Hexadecimal integer",          1194719 );

该代码产生以下输出:


12.345678  -- General 12.345678  -- Default, same as General 12.3457    -- Fixed Point, 4 dec places $12.35     -- Currency 1.235E+001 -- Sci. Notation, 3 dec places 123adf     -- Hexadecimal integer


标准数字格式说明符

表 2-4 总结了九种标准数字格式说明符。第一列列出了说明符的名称,后跟说明符字符。如果说明符字符根据大小写有不同的输出,它们被标记为区分大小写。

注释:注释代码

你已经看到了单行注释,所以在这里我将讨论第二种类型的行内注释——用分隔的注释——并提到第三种类型,称为文档注释

  • The separated comment has a two-character start tag (/*) and a two-character end tag (*/).
  • Text between matching tags is ignored by the compiler.
  • Delimited comments can span any number of lines.

   ↑ Beginning of comment spanning multiple lines /* This text is ignored by the compiler. Unlike single-line comments, delimited comments like this one can span multiple lines. */ ↑ End of comment

分隔注释也可以只跨越一行的一部分。例如,下面的语句显示了行中间被注释掉的文本。结果是声明了一个变量var2

   Beginning of comment ↓ int /*var 1,*/ var2; ↑ End of comment

Image 注意单行和分隔注释在 C# 中的行为就像在 C 和 C++中一样。

更多关于评论

关于注释,您还需要了解一些其他重要的事情:

  • Cannot nest delimited comments. Only one comment can take effect at a time. If you try to nest comments, the first comment will remain valid until the end of its scope.
  • The range of annotation types is as follows:
    • Single line comment, the comment will remain valid until the end of the current line.
    • For comments with separators, the comments will remain valid until the first ending separator is met.

下列评论是不正确的:

`      ↓Opens the comment
/* This is an attempt at a nested comment.
/*  ← Ignored because it’s inside a comment
Inner comment
*/ ← Closes the comment because it’s the first end delimiter encountered
*/   ← Syntax error because it has no opening delimiter

↓ Opens the comment                ↓ Ignored because it’s inside a comment
// Single-line comment   /* Nested comment?
*/  ← Incorrect because it has no opening delimiter`

文档注释

C# 还提供了第三种类型的注释:文档注释。文档注释包含可用于生成程序文档的 XML 文本。这种类型的注释看起来像单行注释,除了它们有三个连续的斜线而不是两个。我将在第二十五章中讨论文档注释。

以下代码显示了文档注释的形式:

   /// <summary> /// This class does... /// </summary> class Program { ...

评论类型汇总

行内注释是被编译器忽略的文本部分,但是包含在代码中以记录它。程序员在代码中插入注释来解释和记录代码。表 2-5 总结了注释类型。

Image

三、类型、存储和变量

一个 C# 程序是一组类型声明

如果你要概括 C 和 C程序的源代码,你可能会说 C 程序是一组函数和数据类型,而 C程序是一组函数和类。然而,C# 程序是一组类型声明。

  • The source code of a c# program or DLL is a collection of one or more type declarations.
  • For an executable file, the type of one of the declarations must be a class containing a method named Main.
  • Namespace is a way to group a group of related type declarations and name the group. Because your program is a set of related type declarations, you usually declare the program type in the namespace you create.

例如,下面的代码显示了一个由三个类型声明组成的程序。这三种类型在名为MyProgram的名称空间中声明。

`   namespace MyProgram                         // Declare a namespace.
{
DeclarationOfTypeA                       // Declare a type.

DeclarationOfTypeB                       // Declare a type.

class C                                  // Declare a type.
{
static void Main()

}
}`

名称空间在第二十一章中有详细解释。

一个类型就是一个模板

由于 C# 程序只是一组类型声明,所以学习 C# 包括学习如何创建和使用类型。所以,我们需要做的第一件事是看看什么是类型。

您可以从将类型视为用于创建数据结构的模板开始。它不是数据结构本身,但是它指定了从模板构造的对象的特征。

类型由以下元素定义:

  • A name
  • A data structure contains its data members.
  • Behavior and constraint

例如,图 3-1 说明了两种类型的部件:shortint

Image

***图 3-1。*一个类型就是一个模板。

实例化一个类型

从类型的模板中创建一个实际的对象叫做实例化类型。

  • An object created by instantiating a type is called the object of that type or the instance of that type. These terms are interchangeable. Every data item in C# program is provided by language, BCL or other libraries, or some kind of instance defined by the programmer.

图 3-2 说明了两种预定义类型对象的实例化。

Image

***图 3-2。*实例化一个类型创建一个实例。

数据成员和函数成员

有些类型,如shortintlong,被称为简单类型*,只能存储一个数据项。*

其他类型可以存储多个数据项。例如,数组是一种可以存储多个相同类型项目的类型。单个项目被称为元素,并由一个编号引用,称为索引。第十二章详细描述了数组。

成员类型

然而,还有其他类型可以包含许多不同类型的数据项。这些类型中的单个元素被称为成员,与数组不同,数组中的每个成员都用一个数字来表示,这些成员有不同的名称。

有两种成员:数据成员和函数成员。

  • The data member stores data related to the object of the class or the whole class.
  • Function member executes code. Function members define the behavior of types.

例如,图 3-3 展示了XYZ类型的一些数据成员和函数成员。它包含两个数据成员和两个函数成员。

Image

***图 3-3。*类型指定数据成员和函数成员。

预定义类型

C# 提供了 16 种预定义类型,如图 3-4 所示,列于表 3-1 和表 3-2。它们包括 13 个简单类型和 3 个非简单类型。

所有预定义类型的名称都由全部小写的字符组成。预定义的简单类型包括以下几种:

  • Eleven numerical types, including the following:
    • Signed and unsigned integer types of various lengths.
    • Floating-point type-float and double.
    • A high-precision decimal type called decimal. Unlike float and double, Type decimal can accurately represent decimal places. It is usually used for currency calculation.
  • A Unicode character type called char.
  • A Boolean type called bool. Type bool represents a Boolean value and must be one of two values-either true or false.

Image 注意与 C 和 C++不同,在 C# 中数值没有布尔解释。

三种非简单类型如下:

  • Type string, which is Unicode character.
  • The array of type object is the base type on which all other types are based.
  • Type dynamic, using dynamic language.

编写的程序集时使用

Image

***图 3-4。*预定义的类型

关于预定义类型的更多信息

所有预定义的类型都直接映射到基础。网络类型。C# 类型名只是 .NET 类型,所以使用。网名在语法上很好,尽管不鼓励这样做。在 C# 程序中,应该使用 C# 名称,而不是。网名。

预定义的简单类型表示单个数据项。它们在表 3-1 中列出,以及它们可以表示的值的范围和基础。它们映射到的. NET 类型。

非简单的预定义类型稍微复杂一些。表 3-2 显示了预定义的非简单类型。

Image

用户自定义类型

除了 C# 提供的 16 种预定义类型,您还可以创建自己的用户定义类型。您可以创建六种类型。它们是:

  • class type
  • struct type
  • array type
  • enum type
  • delegate type
  • interface type

您使用一个类型声明创建一个类型,它包含以下信息:

  • The kind of type you are creating.
  • The name of the new type
  • The declaration (name and specification) of each member of the type-they have no named members except for the array and delegate types.

一旦声明了类型,就可以创建和使用该类型的对象,就像它们是预定义的类型一样。图 3-5 总结了预定义和用户定义类型的使用。使用预定义类型是一个单步过程,只需实例化该类型的对象。使用用户定义的类型是一个两步过程。您必须首先声明该类型,然后实例化该类型的对象。

Image

***图 3-5。*预定义类型只需要实例化。用户定义的类型需要两步:声明和实例化。

栈和堆

当一个程序运行时,它的数据必须存储在内存中。一个项目需要多少内存,它存储在哪里以及如何存储,取决于它的类型。

一个正在运行的程序使用两个内存区域来存储数据:堆栈和 ?? 堆

堆栈

堆栈是一个内存数组,充当后进先出(LIFO)数据结构。它存储几种类型的数据:

  • The value of a certain variable
  • The current execution environment of the program
  • Parameters are passed to methods.

系统负责所有的堆栈操作。作为程序员,你不需要明确地对它做任何事情。但是理解它的基本功能会让你更好地理解你的程序在运行时在做什么,并且让你更好地理解 C# 文档和文献。

关于书库的事实

堆栈的一般特征如下:

  • Data can only be added and deleted from the top of the stack.
  • Putting a data item on the top of the stack is called pushing the item onto the stack.
  • Deleting an item from the top of the stack is called Ejecting the item from the top of the stack.

图 3-6 说明了堆栈的功能和术语。

Image

***图 3-6。*在堆栈上推动和弹出

堆是内存中的一个区域,其中分配了块来存储某些类型的数据对象。与堆栈不同,数据可以以任何顺序存储在堆中或从堆中移除。图 3-7 显示了一个在堆中存储了四个项目的程序。

Image

***图 3-7。*内存堆

虽然您的程序可以在堆中存储项,但它不能显式删除它们。相反,当 CLR 的垃圾收集器(GC)确定您的代码无法再访问孤立的堆对象时,它会自动清理这些对象。这将您从其他编程语言中容易出错的任务中解放出来。图 3-8 说明了垃圾收集过程。

Image

***图 3-8。*自动收集垃圾堆里的垃圾

值类型和引用类型

数据项的类型定义了需要多少内存来存储它以及组成它的数据成员。类型也决定了对象在内存中的存储位置——堆栈还是堆。

类型分为两类:值类型和引用类型。这些类型的对象以不同的方式存储在内存中。

  • The value type only needs a memory to store the actual data.
  • Reference type requires two pieces of memory:
    • The first segment contains the actual data—and is always in the heap. The second is a reference that points to where the data is stored in the heap.

图 3-9 显示了每种类型的单个数据项是如何存储的。对于值类型,数据存储在堆栈上。对于引用类型,实际数据存储在堆中,引用存储在堆栈中。

Image

***图 3-9。*存储不属于另一种类型的数据

存储引用类型对象的成员

虽然图 3-9 显示了当数据不是另一个对象的成员时,它是如何存储的,但是当它是另一个对象的成员时,数据的存储可能会有一点不同。

  • The data part of the reference type object is always stored in the heap, as shown in Figure and Figure 3-9 .
  • A value type object, or the reference part of a reference type, can be stored in the stack or in the heap, as the case may be.

例如,假设您有一个名为MyType的引用类型实例,它有两个成员——值类型成员和引用类型成员。它是如何储存的?值类型成员是否存储在堆栈上,引用类型是否在堆栈和堆之间拆分,如图图 3-9 所示?答案是否定的。

记住,对于引用类型,实例的数据总是存储在堆中。因为两个成员都是对象数据的一部分,所以它们都存储在堆中,不管它们是值类型还是引用类型。图 3-10 说明了MyType型的情况。

  • Although the member A is a value type, it is a part of the MyType instance data, so it is stored in the heap together with the object data.
  • Member B is a reference type, so its data part will always be stored in the heap, as shown by the small box marked "data". The difference is that its reference is also stored in the heap, in the data part of the closed MyType object.

Image

***图 3-10。*作为参考类型一部分的数据存储

Image 注意对于任何一个引用类型的对象,其所有的数据成员都存储在堆中,不管是值类型还是引用类型。

对 C# 类型进行分类

表 3-3 显示了 C# 中所有可用的类型以及它们是什么类型——值类型还是引用类型。每种引用类型将在后面的正文中介绍。

Image

变量

通用编程语言必须允许程序存储和检索数据。

  • A variable is a name that represents the data stored in memory during program execution.
  • C# provides four variables, each of which will be discussed in detail. These are listed in Table 3-4 .

Image

变量声明

变量必须在使用前声明。变量声明定义变量并完成两件事:

  • Give the variable a name and associate it with a type.

简单的变量声明至少需要一个类型和一个名称。下面的声明定义了一个名为var2,类型为int的变量:

   Type ↓ int var2; ↑ Name

例如,图 3-11 表示四个变量的声明以及它们在堆栈中的位置。

Image

***图 3-11。*值类型和引用类型变量声明

变量初始值设定项

除了声明变量的名称和类型,您还可以选择使用声明将其内存初始化为一个特定的值。

一个变量初始化器由一个等号和其后的初始值组成,如下所示:

           Initializer <ins>   ↓   </ins> int var2 = 17;

没有初始值设定项的局部变量有一个未定义的值,在被赋值之前不能使用。试图使用未定义的局部变量会导致编译器产生错误信息。

图 3-12 左侧显示了一些局部变量声明,右侧显示了最终的堆栈配置。有些变量有初始值设定项,有些没有。

Image

***图 3-12。*变量初始值设定项

自动初始化

一些类型的变量如果在没有初始化器的情况下被声明,它们会被自动设置为默认值,而另一些则不会。没有自动初始化为默认值的变量包含未定义的值,直到程序给它们赋值。表 3-5 显示了哪些变量会自动初始化,哪些不会。我将在后面的文章中讨论这五种变量。

多变量声明

可以在一个声明语句中声明多个变量。

  • Variables in a multivariable declaration must all be of the same type.
  • Variables must be separated by commas. Initializers can be included in variable names.

例如,下面的代码显示了两个包含多个变量的有效声明语句。请注意,只要用逗号分隔,初始化的变量可以与未初始化的变量混合使用。显示的最后一条声明语句无效,因为它试图在一条语句中声明不同类型的变量。

`   // Variable declarations--some with initializers, some without
int    var3 = 7, var4, var5 = 3;
double var6, var7 = 6.52;

Type     Different type
↓          ↓
int var8, float var9;       // Error! Can't mix types (int and float)`

使用变量值

变量名表示变量存储的值。您可以通过使用变量名来使用该值。

例如,在下面的语句中,变量名var2代表变量存储的。执行语句时,将从内存中检索该值。

   Console.WriteLine("{0}", var2);

静态键入和动态关键字

你会注意到的一件事是,每个变量声明都包括变量的类型。这使得编译器可以确定运行时需要的内存量,以及哪些部分应该存储在堆栈中,哪些部分应该存储在堆中。变量的类型在编译时确定,在运行时不能更改。这叫做静态打字。

然而,并不是所有的语言都是静态类型的。许多语言,包括脚本语言如 IronPython 和 IronRuby,都是动态类型化的。也就是说,变量的类型可能要到运行时才能解析。既然这些也是 .NET 语言,C# 程序需要能够使用用这些语言编写的程序集。那么,问题是 C# 需要能够在编译时从直到运行时才解析其类型的程序集中解析类型。

为了解决这个问题,C# 提供了dynamic关键字来表示特定的 C# 类型,该类型知道如何在运行时自我解析。

在编译时,编译器不会对类型为dynamic的变量进行类型检查。相反,它将有关变量操作的任何信息打包,并将这些信息包含在变量中。在运行时,检查该信息以确保它与变量被解析成的实际类型一致。如果没有,运行时将抛出一个异常。

可空类型

有些情况下,尤其是在处理数据库时,您希望指明某个变量当前不包含有效值。对于引用类型,通过将变量设置为null,可以很容易地做到这一点。然而,当你定义一个值类型的变量时,不管它的内容是否有任何有效的意义,它的内存都是被分配的。

在这种情况下,您希望有一个与变量相关联的布尔指示符,这样,当值有效时,指示符为true,当值无效时,指示符为false

可空类型允许你创建一个可以被标记为有效或无效的值类型变量,这样你就可以在使用变量之前确保它是有效的。常规值类型被称为非空类型。当你对 C# 有更好的理解时,我会在第二十五章中解释可空类型的细节。

四、类:基础

类别概述

在前一章中,你看到了 C# 提供了六种用户定义的类型。其中最重要的,也是我将首先介绍的,是。由于 C# 中的类是一个很大的主题,我将在接下来的几章中展开讨论。

一个类是一个活跃的数据结构

在面向对象的分析和设计出现之前,程序员认为程序只是一系列指令。当时的重点是构建和优化这些指令。随着面向对象范例的出现,焦点从优化指令转变为将程序的数据和功能组织成逻辑上相关的数据项和功能的封装集,称为类。

类是可以存储数据和执行代码的数据结构。它包含数据成员和函数成员:

  • The data member stores data related to a class or an instance of a class. Members usually model the attributes of real objects represented by this class.
  • Member execution code. These usually model the functions and actions of the real-world objects represented by this class.

C# 类可以有任意数量的数据和函数成员。成员可以是九种可能的成员类型的任意组合。表 4-1 显示了这些成员类型。在这一章中,我将介绍字段和方法。

Image

Image 注意类是逻辑上相关的数据项和函数的封装集合,通常代表现实世界或概念世界中的对象。

程序和类:快速示例

一个正在运行的 C# 程序是一组交互的类型对象,其中大部分是类的实例。例如,假设你有一个模拟扑克游戏的程序。当它运行时,它可能有一个名为Dealer的类的实例,它的工作是运行游戏,还有几个名为Player的类的实例,它们代表游戏的玩家。

Dealer对象存储卡片组的当前状态和玩家数量等信息。它的行动包括洗牌和发牌。

Player类很不一样。它存储玩家姓名和剩余下注金额等信息,并执行分析玩家当前手牌和下注等操作。图 4-1 说明了运行程序。类名显示在盒子外面,实例名在盒子里面。

Image

***图 4-1。*运行程序中的对象

除了DealerPlayer之外,一个真正的程序无疑会包含许多其他类。这些将包括像CardDeck这样的职业。每一个类都模拟了一些事物,这是扑克游戏的一个组成部分。

Image 一个正在运行的程序是一组相互交互的对象。

声明一个类

虽然类型intdoublechar是在 C# 语言中定义的,但是像DealerPlayer这样的类,正如你可能猜到的,并不是由语言定义的。如果你想在程序中使用它们,你必须自己定义它们。您可以通过编写一个类声明来实现。

一个类声明定义了一个新类的特征和成员。它不创建类的实例,而是创建模板,从该模板中将创建类实例。类声明提供了以下内容:

  • Class name
  • Class members
  • Class characteristics

下面是类声明的最小语法的一个示例。花括号包含组成类主体的成员声明。类成员可以在类体内以任何顺序声明。这意味着一个成员的声明完全可以引用另一个还没有定义的成员,直到在类声明的更下面。

    Keyword     Class name ↓          ↓ class MyExcellentClass { *MemberDeclarations* }

下面的代码显示了两个类声明的概要:

`   class Dealer          // Class declaration

class Player          // Class declaration
`

Image 注意由于一个类声明“定义”了一个新的类,你会经常看到一个被称为类定义的类声明,无论是在文献中还是在程序员的日常使用中。

类成员

两种最重要的类成员类型是字段和方法。字段是数据成员,方法是函数成员。

字段

一个字段是属于一个类的变量。

  • It can be of any type, predefined or user-defined.
  • Like all variables, fields store data and have the following characteristics:
    • They can be written.
    • They can learn from.

声明字段的最低语法如下:

   Type ↓ *Type Identifier*; ↑ Field name

例如,下面的类包含字段MyField的声明,它可以存储一个int值:

   class MyClass {  Type ↓ int MyField; ↑ }       Field name

Image 注意与 C 和 C++不同,在 C# 中没有在类型之外声明全局变量(即变量或字段)。所有字段都属于一个类型,并且必须在其类型声明中声明。

显式和隐式字段初始化

因为字段是一种变量,所以字段初始化器的语法与前一章中显示的变量初始化器的语法相同。

字段初始化器

  • The initialization value must be determinable at compile time.    class MyClass { int F1 <ins>= 17</ins>; }           ↑ Field initializer
  • If the initializer is not used, the compiler will set the value of the field to the default value, which is determined by the type of the field. Table 3-1 (in of Chapter 3) gives the default values of simple types. To sum up: the default value of each type is 0, and false means bool. The default value of the reference type is null.

例如,下面的代码声明了四个字段。前两个字段是隐式初始化的。后两个字段用初始化器显式初始化。

`   class MyClass
{
int    F1;                     // Initialized to 0    - value type
string F2;                     // Initialized to null - reference type

int    F3 = 25;                // Initialized to 25
string F4 = "abcd";            // Initialized to "abcd"
}`

具有多个字段的声明

通过用逗号分隔名称,可以在同一个语句中声明相同类型的多个字段*。不能在一个声明中混合不同的类型。例如,您可以将前面的四个字段声明合并成两个语句,得到完全相同的语义结果:*

   int    F1, F3 = 25; string F2, F4 = "abcd";

方法

方法是一个命名的可执行代码块,可以从程序的许多不同部分执行,甚至可以从其他程序执行。(也有匿名方法,没有被命名——但是我会在第十三章中介绍这些方法。)

当一个方法被调用,或者被调用时,它执行该方法的代码,然后返回到调用它的代码,继续执行调用代码。一些方法将值返回到调用它们的位置。方法对应于 C++中的成员函数。

声明方法的最低语法包括以下组件:

  • Return Type : the type of return value of the declaration method. If a method does not return a value, the return type is specified as void.
  • Name: This is the name of the method.
  • Parameter list: consists of at least one set of empty matching brackets. If there are parameters (which I will introduce in the next chapter), they are listed in brackets.
  • Method: It consists of a set of matching curly braces and contains executable code.

例如,下面的代码用一个叫做PrintNums的简单方法声明了一个类。从声明中,您可以了解到关于PrintNums的以下信息:

  • The return type is specified as void, so it does not return a value.
  • It has an empty parameter list.
  • Its method body contains two lines of code. The first output number 1 and the second output number 2.

   class SimpleClass { Return type    Parameter list ↓            ↓ void PrintNums( ) { Console.WriteLine("1"); Console.WriteLine("2"); } }

Image 注意与 C 和 C不同,在 C# 中没有在类型声明之外声明全局函数(即方法或函数)。与 C 和 C不同的是,在 C# 中,方法没有“默认”返回类型。所有方法都必须包含一个返回类型,或者将其列为void

创建类的变量和实例

类声明只是创建类实例的蓝图。一旦声明了一个类,就可以创建该类的实例。

  • Classes are reference types, as you remember in the last chapter, which means that they need memory to store references to data and actual data.
  • The referenced data is stored in variables of class type. Therefore, to create an instance of a class, you need to start by declaring variables of the class type. If the variable is not initialized, its value is undefined.

图 4-2 说明了如何定义变量来保存引用。左边代码的顶部是对类Dealer的声明。下面是对类Program的声明,它包含方法MainMain声明了Dealer类型的变量theDealer。因为变量未初始化,所以它的值未定义,如图右侧所示。

Image

***图 4-2。*为类变量的引用分配内存

为数据分配内存

声明类类型的变量会分配内存来保存引用,但不会分配内存来保存类对象的实际数据。要为实际数据分配内存,可以使用new操作符。

  • The new operator allocates and initializes memory for instances of the specified type. It allocates memory from the stack or heap according to the type.
  • Use the new operator to form an object creation expression, which consists of the following contents:
    • Keyword new.
    • The type name of the instance to allocate memory for.
    • Match brackets, which may or may not contain parameters. I will discuss the possible parameters in detail later.

Keyword   Parentheses are required. ↓        <ins>   ↓  </ins> new *TypeName* ( ) ↑ Type

  • If the allocated memory is used for reference types, the object creation expression returns a reference to the allocated and initialized object instances in the heap.

这正是您需要分配和初始化内存来保存类实例数据的地方。使用new操作符创建一个对象创建表达式,并将它返回的值赋给类变量。这里有一个例子:

   Dealer theDealer;             // Declare variable for the reference theDealer = <ins>new Dealer()</ins>;     // Allocate memory for the class object and assign ↑           // it to the variable Object-creation expression

图 4-3 中左边的代码显示了用于分配内存和创建类Dealer实例的new操作符,然后将它赋给类变量。代码右侧的图中显示了内存结构。

Image

***图 4-3。*为类变量的数据分配内存

合并步骤

您可以通过用对象创建表达式初始化变量来组合这两个步骤。

        Declare variable <ins>         ↓       </ins> Dealer theDealer = <ins>new Dealer();</ins>                // Declare and initialize ↑ Initialize with an object-creation expression.

实例成员

一个类声明就像一个蓝图,你可以从中创建尽可能多的类实例。

  • Instance members : Each instance of a class is an independent entity with its own set of data members, which is different from other instances of the same class. These are called instance members because they are associated with instances of the class.
  • Static member : it is an instance member by default, but it is also possible to declare members called static members, which are associated with classes rather than instances. I will introduce these in Chapter 6.

作为实例成员的一个例子,下面的代码展示了带有类Player的三个实例的 poker 程序。图 4-4 显示每个实例的Name字段都有不同的值。

`   class Dealer                             // Declare class
class Player {                                  // Declare class
string Name;                                 // Field
...
}

class Program {
static void Main()
{
Dealer theDealer = new Dealer();
Player player1   = new Player();
Player player2   = new Player();
Player player3   = new Player();
...
}
}` Image

***图 4-4。*实例成员在类对象之间有不同的值。

访问修饰符

在一个类中,任何函数成员都可以通过简单地使用该成员的名字来访问该类的任何其他成员。

访问修饰符是成员声明的可选部分,它指定程序的哪些其他部分可以访问该成员。访问修饰符放在简单的声明形式之前。以下是字段和方法的语法:

`  Fields
*     AccessModifier Type Identifier*

Methods
AccessModifier ReturnType MethodName ()
`

成员访问的五个类别如下。我将在本章中描述前两个,其他的将在第七章中描述。

  • private
  • public
  • protected
  • internal
  • protected internal

私人和公共访问

私有成员只能从声明它们的类中访问,其他类无法看到或访问它们。

  • Private access is the default access level, so if a member is declared without an access modifier, it is a private member.
  • You can also use the private access modifier to explicitly declare a private member. There is no semantic difference between implicitly declaring private members and explicitly declaring private members. These two forms are equivalent.

例如,以下两个声明都指定了private int成员:

`           int MyInt1;                 // Implicitly declared private

private int MyInt2;                 // Explicitly declared private

Access modifier`

程序中的其他对象可以访问实例的公共成员。您必须使用public访问修饰符来指定公共访问。

    Access modifier ↓ public int MyInt;

描述公共和私人访问

本文中的图用带标签的方框表示类别,如图图 4-5 所示。

  • Members are represented as small label boxes within the class box.
  • Private members are represented as completely enclosed in their own class boxes.
  • Public members are represented as partially stuck outside their class boxes.

Image

***图 4-5。*代表班级和成员

成员访问示例

下面代码中的类C1声明了公共和私有字段和方法。图 4-6 展示了C1类成员的可见性。

`   class C1
{
int         F1;                     // Implicit private field
private int F2;                     // Explicit private field
public  int F3;                     // Public field

void DoCalc()                       // Implicit private method

public int GetVal()                 // Public method

}` Image

***图 4-6。*私有和公有类成员

从类内部访问成员

如前所述,一个类的成员可以通过名字访问其他类成员。

例如,下面的类声明显示了该类访问字段的方法和其他方法。即使字段和两个方法被声明为private,类的所有成员都可以被该类的任何方法(或任何函数成员)访问。图 4-7 说明了代码。

`   class DaysTemp
{
// Fields
private int High = 75;
private int Low  = 45;

// Methods
private int GetHigh()
{
return High;                          // Access private field
}

private int GetLow()
{
return Low;                           // Access private field
}

public float Average ()
{
return (GetHigh() + GetLow()) / 2;    // Access private methods
}             ↑          ↑
}            Accessing the private methods` Image

***图 4-7。*一个类内的成员可以自由地互相访问。

从类外访问成员

若要从类外部访问公共实例成员,必须包含变量名和成员名,用句点(点)分隔。这被称为点语法符号;稍后我会更详细地描述它。

例如,下面代码的第二行显示了从类外部访问方法的示例:

   DaysTemp myDt = new DaysTemp();      // Create an object of the class float fValue  = myDt.Average();      // Access it from outside ↑      ↑ Variable name  Member name

例如,下面的代码声明了两个类:DaysTempProgram

  • The two fields in DaysTemp are declared as public, so they can be accessed from outside the class.
  • Method Main is a member of Program class. It creates a variable and an object of class DaysTemp, and assigns values to the fields of the object. Then it reads the values of the fields and prints them out.

`   class DaysTemp                                   // Declare class DaysTemp

class Program                                    // Declare class Program
{
static void Main()
{       Variable name

DaysTemp temp = new DaysTemp();            // Create the object
Variable name and field
    ↓    
temp.High = 85;                            // Assign to the fields
temp.Low  = 60;               Variable name and field
    ↓    
Console.WriteLine("High:   {0}", temp.High );     // Read from fields
Console.WriteLine("Low:    {0}", temp.Low  );
}
}`

这段代码产生以下输出:


High:  85 Low:   60


把所有的东西放在一起

下面的代码创建了两个实例,并将它们的引用存储在名为t1t2的变量中。图 4-8 说明了内存中的t1t2。该代码演示了到目前为止在类的使用中讨论的以下三个操作:

  • Declare a class
  • Create an instance of the class
  • Access class members (that is, write to and read from a field)

`   class DaysTemp                          // Declare the class
{
public int High, Low;                // Declare the instance fields
public int Average()                 // Declare the instance method
{
return (High + Low) / 2;
}
}

class Program
{
static void Main()
{
// Create two instances of DaysTemp
DaysTemp t1 = new DaysTemp();
DaysTemp t2 = new DaysTemp();

// Write to the fields of each instance
t1.High = 76;     t1.Low = 57;
t2.High = 75;     t2.Low = 53;

// Read from the fields of each instance and call a method of
// each instance
Console.WriteLine("t1: {0}, {1}, {2}",
t1.High, t1.Low, t1.Average() );
Console.WriteLine("t2: {0}, {1}, {2}",
t2.High, t2.Low, t2.Average() );
↑        ↑         ↑
}                               Field      Field      Method
}`

该代码产生以下输出:


t1: 76, 57, 66 t2: 75, 53, 64


Image

***图 4-8。*实例 t1 和 T2 的内存布局

五、方法

一种方法的结构

一个方法是一个有名字的代码块。通过使用方法名,您可以从程序中的其他地方执行代码。您还可以将数据传入一个方法,并作为输出接收数据。

正如你在前一章看到的,方法是类的函数成员。方法有两个主要部分,如图图 5-1 所示——方法头和方法体。

  • The method header specifies the characteristics of the method, including:
    • Whether the method returns data, and if so, what type of data is returned?
    • Method name
    • What types of data can be passed in and out of the method, and how should the data be handled?
  • The method body contains the sequence of executable code statements. Execution starts with the first statement in the method body and continues to execute the whole method in sequence.

Image

***图 5-1。*一个方法的结构

下面的示例显示了方法头的形式。我将在接下来的几页中介绍每一部分。

    int MyMethod ( <ins>int par1, string par2</ins> ) ↑       ↑                  ↑ Return  Method         Parameter type    name              list

例如,下面的代码显示了一个名为MyMethod的简单方法,该方法依次多次调用WriteLine方法:

   void MyMethod() { Console.WriteLine("First"); Console.WriteLine("Last"); }

虽然前几章描述了类,但是还有另一种用户定义的类型叫做struct,我将在第十章中介绍。本章讲述的关于类方法的大部分内容对于struct方法也是正确的。

方法体中的代码执行

方法体是一个,它(正如你在《??》第二章中回忆的那样)是一系列花括号之间的语句。一个块可以包含以下项目:

  • Local variable
  • Control flow structure
  • Method call
  • The block is nested in it.

图 5-2 显示了一个方法体及其部分组件的例子。

Image

***图 5-2。*法体实例

局部变量

像我在第四章中提到的字段一样,局部变量存储数据。虽然字段通常存储关于对象状态的数据,但是通常创建局部变量来存储用于局部或暂时计算的数据。表 5-1 比较和对比局部变量和实例字段。

以下代码行显示了局部变量声明的语法。可选的初始化器由一个等号和一个用来初始化变量的值组成。

     Variable name     Optional initializer ↓     <ins>      ↓   </ins> *Type* Identifier = Value;

  • The existence and life of a local variable is limited to the block that created it and the block nested in it.
    • Variables exist at the time of declaration.
    • When the program block finishes executing, it doesn't exist.
  • Local variables can be declared anywhere in the method body, but they must be declared before they can be used.

以下示例显示了两个局部变量的声明和使用。第一个是类型int,第二个是类型SomeClass

   static void Main( ) { int myInt    = 15; SomeClass sc = new SomeClass(); ... }

Image

类型推断和 var 关键字

如果你看下面的代码,你会发现在声明的开始提供类型名,你提供的信息编译器已经可以从初始化的右边推断出来。

  • In the first variable declaration, the compiler can infer that 15 is a int.
  • In the second declaration, the object creation expression on the right returns an object of type MyExcellentClass.

因此,在这两种情况下,在声明的开头包含显式类型名是多余的。

   static void Main( ) { int total = 15; MyExcellentClass mec = new MyExcellentClass(); ... }

为了避免这种冗余,C# 允许在变量声明的开头使用关键字var来代替显式类型名,如下所示:

   static void Main( ) { Keyword ↓ var total = 15; var mec   = new MyExcellentClass(); ... }

关键字var并不代表不是一种特殊的变量。它只是从语句右侧的初始化中可以推断出的任何类型的语法简写。在第一个声明中,它是int的简写。第二种,它是MyExcellentClass的简写。前面带有显式类型名的代码段和带有var关键字的代码段在语义上是等价的。

使用var关键字的一些重要条件如下:

  • Can only be used for local variables, not fields.
  • It can only be used if the variable declaration contains initialization.
  • Once the compiler deduces the type of the variable, it is fixed.

Image 注意var关键字是而不是像 JavaScript var可以引用不同的类型。它是从等号右边推断出的实际类型的简写。var关键字并没有改变 C# 的强类型本质。

嵌套块内的局部变量

方法体中可以嵌套其他块。

  • There can be any number of blocks, which can be sequential or further nested. Blocks can be nested at any level.
  • Local variables can be declared within nested blocks, and like all local variables, their lifetime and visibility are limited to the blocks in which they are declared and the blocks nested within them.

图 5-3 展示了两个局部变量的寿命,显示了代码和堆栈的状态。箭头表示刚刚执行的那一行。

  • The variable var1 is declared in the method body before the nested block.
  • The variable var2 is declared in a nested block. It exists from the time of declaration until the end of the block where it is declared.
  • When control is passed out of the nested block, its local variables pop up from the stack.

Image

***图 5-3。*局部变量的生存期

Image 注意在 C 和 C++中,你可以声明一个局部变量,然后在一个嵌套块中,你可以声明另一个同名的局部变量。在内部范围内,内部名称会屏蔽外部名称。但是,在 C# 中,无论嵌套级别如何,都不能在名字的范围内声明另一个同名的局部变量。

局部常数

局部常量很像局部变量,除了一旦被初始化,它的值就不能被改变。像局部变量一样,局部常量必须在块内声明。

常数的两个最重要的特征如下:

  • The constant must be initialized by by at the time of declaration.
  • cannot be changed by after the constant is declared.

常数的核心声明如下所示。语法与字段或变量声明的语法相同,除了以下几点:

  • Add the keyword const before the type.
  • Mandatory initialization. The value of the initializer must be determinable at compile time, and it is usually one of the predefined simple types or an expression composed of them. It can also be a null reference, but it cannot be a reference to an object, because the reference to an object is determined at runtime.

Image 注意关键字const不是修饰符,而是核心声明的一部分。它必须紧接在类型之前。

   Keyword ↓ const *Type Identifier* <ins>= Value</ins>; ↑ Initializer required

像局部变量一样,局部常量是在方法体或代码块中声明的,它在声明它的块的末尾超出了范围。例如,在下面的代码中,内置类型double的局部常量PI在方法DisplayRadii的结尾超出了范围。

`   void DisplayRadii()
{
const double PI = 3.1416;                    // Declare local constant

for (int radius = 1; radius ⇐ 5; radius++)
{
double area = radius * radius * PI;       // Read from local constant
Console.WriteLine
("Radius: {0}, Area: {1}" radius, area);
}
}`

流量控制

方法包含组成程序的动作的大部分代码。其余的在其他函数成员中,比如属性和操作符。

术语控制流指的是程序的执行流。默认情况下,程序执行按顺序从一条语句移动到下一条语句。控制流语句允许您修改执行顺序。

在这一节中,我将只提到一些可以在代码中使用的控制语句。第九章详细介绍了它们。

  • Select statements : These statements allow you to select statements or statement blocks to be executed.
    • if: conditionally execute a statement.
    • if...else: conditionally execute one or another statement.
    • switch: conditionally execute a group.
  • A statement in an iteration statement: These statements allow you to loop or iterate through a block of statements.
    • for: Cycle-Test at the top
    • while: Cycle-Test at the top
    • do: Cycle-Test at the bottom
    • foreach: as a group
  • Each member of the jump statement is executed once: these statements allow you to jump from one place in the block or method to another.
    • break: Exit the current cycle.
    • continue: Go to the bottom of the current cycle.
    • goto: Go to a named statement.
    • return: Return the execution to the calling method.

例如,下面的方法显示了两个控制流语句。不要担心细节。

`   void SomeMethod()
{
int intVal = 3;
Equality comparison operator
 ↓ 
if( intVal == 3 )                                  // if statement
Console.WriteLine("Value is 3. ");

for( int i=0; i<5; i++ )                           // for statement
Console.WriteLine("Value of i: {0}", i);
}`

方法调用

您可以从方法体内部调用其他方法。

  • The phrases calling method and calling method are synonyms. I'll discuss this soon by calling the method with the method name and parameter list.

例如,下面的类声明了一个名为PrintDateAndTime的方法,它是从方法Main内部调用的:

`   class MyClass
{
void PrintDateAndTime()                  // Declare the method.
{
DateTime dt = DateTime.Now;            // Get the current date and time.
Console.WriteLine("{0}", dt);          // Write it out.
}

static void Main()                        // Declare the method.
{
MyClass mc = new MyClass();
mc.PrintDateAndTime();                // Invoke the method.
}              ↑         ↑
}            Method      Empty
name      parameter list`

图 5-4 说明了调用方法时的动作顺序:

  1. The execution of the current method is paused at the call point.
  2. Control transfers to the start of the called method.
  3. The called method is executed until completion.
  4. Control returns to the calling method.

Image

***图 5-4。*调用方法时的控制流

返回值

方法可以向调用代码返回值。返回值被插入到调用代码中表达式中发生调用的位置。

  • To return a value, the method must declare a return type before the method name.
  • If a method does not return a value, it must declare a return type void.

下面的代码显示了两个方法声明。第一个返回类型为int的值。第二个不返回值。

   Return type ↓ int  GetHour()     { ... } void DisplayHour() { ... } ↑ No value is returned.

声明返回类型的方法必须使用以下形式的return语句从该方法返回值,该语句在关键字return后包含一个表达式。方法中的每条路径都必须以这种形式的return语句结束。

   return *Expression*;                           // Return a value. ↑ Evaluates to a value of the return type

例如,下面的代码显示了一个名为GetHour的方法,它返回一个类型为int的值。

`   Return type

int GetHour( )
{
DateTime dt = DateTime.Now;              // Get the current date and time.
int hour    = dt.Hour;                   // Get the hour.

return hour;                             // Return an int.
}      ↑
Return  statement`

也可以返回用户自定义类型的对象。例如,以下代码返回一个类型为MyClass的对象:

   Return type — MyClass ↓ MyClass method3( ) { MyClass mc = new MyClass(); ... return mc;                                 // Return a MyClass object. }

作为另一个例子,在下面的代码中,方法GetHourMainWriteLine语句中被调用,并向WriteLine语句中的那个位置返回一个int值。

`   class MyClass
{          ↓  Return type
public int GetHour()
{
DateTime dt = DateTime.Now;            // Get the current date and time.
int hour    = dt.Hour;                 // Get the hour.

return hour;                           // Return an int.
}           ↑
}          Return value

class Program
{
static void Main()
{                                 Method invocation
MyClass mc = new MyClass();         ↓     
Console.WriteLine("Hour: {0}", mc.GetHour());
}                                  ↑    ↑
}                                 Instance  Method
name    name`

返回声明和作废方法

在上一节中,您看到了返回值的方法必须包含 return 语句。Void 方法不需要 return 语句。当控制流到达方法体的右花括号时,控制返回到调用代码,并且没有值被插回到调用代码中。

然而,通常情况下,当某些条件适用时,您可以通过提前退出该方法来简化程序逻辑。

  • You can exit a void method at any time by using the return statement in the following form, without parameters: return;
  • This form of return statement can only be used with methods declared as void.

例如,下面的代码显示了一个名为SomeMethodvoid方法的声明,它有三个可能的位置返回给调用代码。前两个位置在称为if语句的分支中,这在第九章的中有所涉及。最后一个地方是方法体的结尾。

`  Void return type

void SomeMethod()
{
...
if ( SomeCondition )                 // If ...
return;                           // return to the calling code.
...

if ( OtherCondition )                // If ...
return;                           // return to the calling code.

...
}                                       // Default return to the calling code.`

下面的代码展示了一个带有return语句的void方法的例子。只有在中午之后,该方法才会写出消息。图 5-5 中的所示的过程如下:

  • First, the method obtains the current date and time. Don't worry about understanding the details of this now. )
  • If the number of hours is less than 12 (that is, before noon), the return statement is executed, and the control immediately returns to the calling method without writing anything to the screen.
  • If the hour is 12 or more, skip the return statement, and the code executes the WriteLine statement to write the informational message to the screen.

`   class MyClass
{    ↓ Void return type
void TimeUpdate()
{
DateTime dt = DateTime.Now;          // Get the current date and time.
if (dt.Hour < 12)                 // If the hour is less than 12,
return;                        // then return.

Return to calling method
Console.WriteLine("It's afternoon!");   // Otherwise, print message.
}

static void Main()
{
MyClass mc = new MyClass();       // Create an instance of the class.
mc.TimeUpdate();                  // Invoke the method.
}
}` Image

***图 5-5。*使用返回类型为 void 的 return 语句

参数

到目前为止,您已经看到了方法是命名的代码单元,可以从程序中的许多地方调用,并且可以向调用代码返回单个值。返回单个值当然有价值,但是如果需要返回多个值呢?此外,如果能够在方法开始执行时将数据传递给该方法,那将非常有用。参数是一种特殊的变量,可以让你做这两件事。

形式参数

形参是在方法声明的参数列表中声明的局部变量,而不是在方法体中声明的。

下面的方法头显示了参数声明的语法。它声明了两个形参——一个类型为int,另一个类型为float

   public void PrintSum( <ins>int x, float y</ins> ) {                           ↑    ...           Formal parameter declarations }

  • Because formal parameters are variables, they have data types and names, which can be read and written.
  • Unlike other local variables of methods, parameters are defined outside the method body and initialized before the method starts (except for a type called output parameter, which I will introduce soon).

在大多数情况下,形式参数在整个方法体中使用,就像其他局部变量一样。例如,下面对方法PrintSum的声明使用了两个形参xy,以及一个局部变量sum,它们都属于int类型。

   public void PrintSum( int x, int y ) { int sum = x + y; Console.WriteLine("Newsflash:  {0} + {1} is {2}", x, y, sum); }

实际参数

当您的代码调用一个方法时,必须在方法中的代码开始执行之前初始化形参的值。

  • The expression or variable used to initialize formal parameters is called actual parameter . They are sometimes called arguments .
  • The actual parameters are placed in the parameter list of the method call.
  • Each argument must match the type of the corresponding parameter, or the compiler must be able to implicitly convert the argument to that type. I will explain the details of converting from one type to another in Chapter 16.

例如,下面的代码显示了方法PrintSum的调用,它有两个数据类型int的实际参数:

   PrintSum( 5, someInt ); ↑     ↑ Expression   Variable of type int

当调用该方法时,每个实际参数的值用于初始化相应的形参。然后执行方法体。图 5-6 说明了实际参数和形式参数之间的关系。

Image

***图 5-6。*实参初始化相应的形参。

请注意,在前面的示例代码中,以及在图 5-6 中,实参的数量与形参的数量相匹配,并且每个实参与相应形参的类型相匹配。遵循该模式的参数被称为位置参数。我们很快就会看到一些其他的选择。但是首先我们将更详细地看位置参数。

带有位置参数的方法示例

在下面的代码中,类MyClass声明了两个方法——一个接受两个整数并返回它们的和,另一个接受两个float并返回它们的平均值。在第二次调用中,注意编译器已经隐式地将两个int值— 5someInt—转换为float类型。

`   class MyClass    Formal parameters
{                      ↓       
public int Sum(int x, int y)                        // Declare the method.
{
return x + y;                                    // Return the sum.
}
Formal parameters
           ↓              
public float Avg(float input1, float input2)        // Declare the method.
{
return (input1 + input2) / 2.0F;                 // Return the average.
}
}

class Program
{
static void Main()
{
MyClass myT = new MyClass();
int someInt = 6;

Console.WriteLine
("Newsflash:  Sum: {0} and {1} is {2}",
5, someInt, myT.Sum( 5, someInt ));        // Invoke the method.

Actual parameters
Console.WriteLine
("Newsflash:  Avg: {0} and {1} is {2}",
5, someInt, myT.Avg( 5, someInt ));        // Invoke the method.
}                                    ↑
}                                 Actual parameters`

该代码产生以下输出:


Newsflash:  Sum: 5 and 6 is 11 Newsflash:  Avg: 5 and 6 is 5.5


值参数

有几种类型的参数,每一种都以稍微不同的方式向方法传递数据和从方法传递数据。到目前为止,我们看到的类型是默认类型,称为值参数。

当使用值参数时,通过将实际参数的值复制到形参来将数据传递给方法。当调用一个方法时,系统执行以下操作:

  • It allocates space for parameters on the stack.
  • It copies the values of actual parameters into formal parameters.

值参数的实际参数不一定是变量。它可以是计算匹配数据类型的任何表达式。例如,下面的代码显示了两个方法调用。首先,实际参数是一个类型为float的变量。在第二个例子中,它是一个计算结果为float的表达式。

`   float func1( float val )                              // Declare the method.
{               ↑
Float data type
float j = 2.6F;
float k = 5.1F;
...
}

Variable of type float

float fValue1 = func1( k );                        // Method call
float fValue2 = func1( (k + j) / 3 );              // Method call
...                         ↑
Expression that evaluates to a float`

在使用变量作为实际参数之前,必须给该变量赋值(输出参数除外,我将很快介绍这一点)。对于引用类型,变量可以被赋予一个实际引用或null

Image 第三章讲述了值类型,如您所知,这些值类型包含自己的数据。不要混淆我现在说的是值参数。他们完全不同。值参数是实参的值被复制到形参的参数。

例如,下面的代码显示了一个名为MyMethod的方法,它有两个参数——一个类型为MyClass的变量和一个int

  • This method adds 5 to the int type field and int belonging to this class. You may also notice that MyMethod uses the modifier static, which I haven't explained yet. You can ignore it for a while. I will explain the static method in Chapter 6.

`   class MyClass
{
public int Val = 20;                      // Initialize the field to 20.
}

class Program            Formal parameters
{                               ↓          
static void MyMethod( MyClass f1, int f2 )
{
f1.Val = f1.Val + 5;                   // Add 5 to field of f1 param.
f2     = f2 + 5;                       // Add 5 to second param.
Console.WriteLine( "f1.Val: {0}, f2: {1}", f1.Val, f2 );
}

static void Main()
{
MyClass a1 = new MyClass();
int     a2 = 10;

Actual parameters
   ↓   
MyMethod( a1, a2 );                    // Call the method.
Console.WriteLine( "f1.Val: {0}, f2: {1}", a1.Val, a2 );
}
}`

该代码产生以下输出:


f1.Val: 25, f2: 15 f1.Val: 25, f2: 10


图 5-7 说明了在方法执行的各个阶段实际和形式参数的值:

  • Before the method is called, the variables a1 and a2 that will be used as actual parameters are already on the stack.

  • At the beginning of the method, the system allocates space for the parameter on the stack and copies the value from the argument.

    • Because a1 is a reference type, the reference is copied, resulting in that both arguments and parameters refer to the same object in the heap.
    • Because a2 is a value type, the value is copied to generate an independent data item.
  • At the end of the method, the fields of f2 and f1 are increased by 5.

    • After the method is executed, the parameter is popped off the stack.
    • The value of value type a2 is not affected by activities in the method.
    • However, the value of the reference type a1 has been changed by the activity in the method.

Image

图 5-7 。数值参数

参考参数

第二种类型的参数称为参考参数。

  • When using a reference parameter, you must use the ref modifier in the declaration and call of the method.
  • The argument must be a variable and must be assigned to before it can be used as an argument. If it is a reference type variable, it can be given an actual reference or value null.

例如,以下代码阐释了声明和调用的语法:

`               Include the ref modifier.

void MyMethod( ref int val )           // Method declaration

int y = 1;                             // Variable for the actual parameter
MyMethod ( ref y );                    // Method call

Include the ref modifier.

MyMethod ( ref 3+5 );                  // Error!

Must use a variable`

在上一节中,您看到了对于值参数,系统在堆栈上为形参分配内存。相比之下,参考参数具有以下特征:

  • They do not allocate memory for parameters on the stack.
  • Instead, the parameter name acts as the alias of the argument variable, referring to the same memory location.

由于形参名和实参名的行为就好像它们引用了相同的内存位置,因此很明显,在方法执行过程中对形参所做的任何更改在方法完成后都可以通过实参变量看到。

Image 注意记住在方法声明调用中使用ref关键字。

例如,下面的代码再次显示了方法MyMethod,但是这次参数是引用参数而不是值参数:

`   class MyClass
{
public int Val = 20;                     // Initialize field to 20.
}

class Program         ref modifier         ref modifier
{                        ↓                ↓
static void MyMethod(ref MyClass f1, ref int f2)
{
f1.Val = f1.Val + 5;                  // Add 5 to field of f1 param.
f2     = f2 + 5;                      // Add 5 to second param.
Console.WriteLine( "f1.Val: {0}, f2: {1}", f1.Val, f2 );
}

static void Main()
{
MyClass a1 = new MyClass();
int a2     = 10;
ref modifiers
↓       ↓
MyMethod(ref a1, ref a2);             // Call the method.
Console.WriteLine( "f1.Val: {0}, f2: {1}", a1.Val, a2 );
}
}`

该代码产生以下输出:


f1.Val: 25, f2: 15 f1.Val: 25, f2: 15


图 5-8 说明了在方法执行的各个阶段实际和形式参数的值:

  • Before the method is called, the variables a1 and a2 that will be used as actual parameters are already on the stack.
  • At the beginning of the method, the name of the parameter has been set as the alias of the actual parameter. You can think that variables a1 and f1 point to the same memory location, and variables a2 and f2 point to the same memory location.
  • At the end of the method, the fields of f2 and f1 objects are increased by 5.
  • After the execution of the method, the names of the parameter disappeared ("out of range"), but the value of a2 (value type) and the value of the object pointed to by a1 (reference type) were changed by the activities in the method.

Image

***图 5-8。*通过引用参数,形参充当实参的别名。

引用类型为值和引用参数

在前面几节中,您看到了对于引用类型对象,您可以在方法调用中修改其成员,而不管您是将对象作为值参数还是作为引用参数发送。然而,我们并没有在方法内部给形参赋值。在这一节中,我们将看看当你在方法内部给一个引用类型的形参赋值时会发生什么。答案如下:

  • Pass the reference type object as a value parameter : If a new object is created inside the method and assigned to the parameter, it will break the connection between the parameter and the actual parameter, and the new object will not persist after the method is called.
  • Pass the reference type object as the reference parameter : If a new object is created inside the method and assigned to the parameter, then the new object still exists after the method ends, and it is the value referenced by the argument.

以下代码显示了第一种情况——使用引用类型对象作为值参数:

`   class MyClass

class Program
{
static void RefAsParameter( MyClass f1 )
{
f1.Val = 50;
Console.WriteLine( "After member assignment:    {0}", f1.Val );
f1 = new MyClass();
Console.WriteLine( "After new object creation:  {0}", f1.Val );
}

static void Main( )
{
MyClass a1 = new MyClass();

Console.WriteLine( "Before method call:         {0}", a1.Val );
RefAsParameter( a1 );
Console.WriteLine( "After method call:          {0}", a1.Val );
}
}`

该代码产生以下输出:


Before method call:         20 After member assignment:    50 After new object creation:  20 After method call:          50


图 5-9 下图说明了以下有关代码:

  • At the beginning of the method, both arguments and formal parameters point to the same object in the heap.
  • After being assigned to members of an object, they still point to the same object in the heap.
  • When the method assigns a new object to the parameter, the actual parameter (outside the method) still points to the original object, and the parameter points to the new object.
  • After the method is called, the argument points to the original object, and the parameter and the new object are gone.

Image

***图 5-9。*分配给用作值参数的引用类型对象

下面的代码说明了引用类型对象被用作引用参数的情况。除了方法声明和方法调用中的ref关键字之外,代码完全相同。

`   class MyClass

class Program
{

static void RefAsParameter( ref MyClass f1 )
{
// Assign to the object member.
f1.Val = 50;
Console.WriteLine( "After member assignment:    {0}", f1.Val );

// Create a new object and assign it to the formal parameter.
f1 = new MyClass();
Console.WriteLine( "After new object creation:  {0}", f1.Val );
}

static void Main( string[] args )
{
MyClass a1 = new MyClass();

Console.WriteLine( "Before method call:         {0}", a1.Val );
RefAsParameter( ref a1 );
Console.WriteLine( "After method call:          {0}", a1.Val );
}
}`

该代码产生以下输出:


Before method call:         20 After member assignment:    50 After new object creation:  20 After method call:          20


如您所知,引用参数的行为就好像实际参数是形式参数的别名。这使得对前面代码的解释变得容易。图 5-10 说明了以下关于代码:

  • When the method is called, the parameter and argument point to the same object in the heap.
  • The modification of a member's value is seen by both parameter and argument.
  • When the method creates a new object and assigns it to the parameter, the references of the parameter and the argument point to the new object.
  • After the method, the actual parameter points to the object created inside the method.

Image

***图 5-10。*分配给用作参考参数的参考类型对象

输出参数

输出参数用于将数据从方法内部传递回调用代码。它们的行为非常类似于参考参数。与参考参数一样,输出参数也有以下要求:

  • Modifiers must be used in both method declarations and calls. For the output parameter, the modifier is out, not ref.
  • Like the reference parameter, the actual parameter must be a variable-it cannot be another type of expression. This is meaningful because this method requires a memory location to store the value it returns.

例如,下面的代码声明了一个名为MyMethod的方法,它接受一个输出参数。

`              out modifier

void MyMethod( out int val )          // Method declaration

...
int y = 1;                            // Variable for the actual parameter
MyMethod ( out y );                   // Method call

out modifier`

像引用参数一样,输出参数的形式参数充当实际参数的别名。形参和实参都是同一个内存位置的名字。显然,在方法完成执行后,对方法内部形参的任何更改都可以通过实际的形参变量看到。

与参考参数不同,输出参数需要满足以下条件:

  • Within a method, the output parameter must be assigned before it can be read. This means that the initial values of the parameters are irrelevant, and you don't have to assign values to the actual parameters before the method call.
  • Within the method, every possible path in the code must assign a value to each output parameter before the method exits.

由于方法内部的代码必须先写入输出参数,然后才能读取它,所以不可能使用输出参数将数据发送到方法中。事实上,如果方法中有任何执行路径试图在方法为输出参数赋值之前读取该参数的值,编译器就会产生错误信息。

   public void Add2( out int outValue ) { int var1 = outValue + 2;  // Error! Can't read from an output parameter }                            // before it has been assigned to by the method.

例如,下面的代码再次显示了方法MyMethod,但是这次使用了输出参数:

`   class MyClass
{
public int Val = 20;                    // Initialize field to 20.
}

class Program       out modifier         out modifier
{                       ↓                ↓
static void MyMethod(out MyClass f1, out int f2)
{
f1 = new MyClass();                  // Create an object of the class.
f1.Val = 25;                         // Assign to the class field.
f2     = 15;                         // Assign to the int param.
}

static void Main()
{
MyClass a1 = null;
int a2;

MyMethod(out a1, out a2);            // Call the method.
}            ↑       ↑
}               out modifiers`

图 5-11 说明了在方法执行的各个阶段实际和形式参数的值。

  • Before the method is called, the variables a1 and a2 that will be used as actual parameters are already on the stack.
  • At the beginning of the method, the name of the parameter is set as the alias of the actual parameter. You can imagine the variables a1 and f1 pointing to the same memory location, or you can imagine the variables a2 and f2 pointing to the same memory location. The names a1 and a2 are out of range and cannot be accessed from within MyMethod.
  • Inside the method, the code creates an object of type MyClass and assigns it to f1. Then it assigns a value to the field of f1 and also to f2. The assignments of f1 and f2 are both necessary because they are output parameters.
  • After the method is executed, the name of the parameter is out of scope, but the values of reference type a1 and value type a2 are changed by the activities in the method.

Image

***图 5-11。*对于输出参数,形参充当实参的别名,但是附加的要求是必须在方法内部赋值。

参数数组

在我到目前为止介绍的参数类型中,每个形参都必须有一个实参。参数数组的不同之处在于它们允许零个或多个特定类型的实际参数用于特定的形参。关于参数数组的要点如下:

  • There can only be one parameter array in a parameter list.
  • If so, it must be the last parameter in the list.
  • All parameters represented by an array must be of the same type.

要声明参数数组,必须执行以下操作:

  • Use the params modifier before the data type.
  • Place a set of empty square brackets after the data type.

下面的方法头显示了类型为int的参数数组声明的语法。在这个例子中,形式参数inVals可以表示零个或多个实际的int参数。

                      Array of ints <ins>  ↓  </ins> void ListInts( <ins>params</ins> int[] <ins>inVals</ins> ) { ...             ↑            ↑ Modifier       Parameter name

类型名后面的方括号空集指定参数将是一个int s 的数组,这里不需要担心数组的细节。他们在第十二章中有详细介绍。但是,对于我们这里的目的,您只需要知道以下内容:

  • An array is an ordered collection of data items of the same type.
  • Access an array using a numeric index.
  • Array is a reference type, so all its data items are stored in the heap.

方法调用

可以通过两种方式为参数数组提供实际参数。这些是

   ListInts( 10, 20, 30 );              // Three ints

  • One-dimensional array element of data type.
       int[] intArray = {1, 2, 3}; ListInts( intArray );                // An array variable

请注意,在这些例子中,在调用中,您没有使用了params修饰符。参数数组中修饰符的用法不符合其他参数类型的模式。

  • Other parameter types are consistent, they either use a modifier or do not use a modifier.
    • Parameters are declared or called without modifiers.
    • Modifiers are required for both reference and output parameters.
  • The usage of params modifier is summarized as follows:
    • Need to be used in the declaration.
    • Not allowed in the call.
展开式

方法调用的第一种形式,在调用中使用单独的实际参数,有时被称为扩展形式。

例如,下面代码中方法ListInts的声明匹配它下面的所有方法调用,即使它们有不同数量的实际参数。

`   void ListInts( params int[] inVals )        // Method declaration

...
ListInts( );                                       // 0 actual parameters
ListInts( 1, 2, 3 );                               // 3 actual parameters
ListInts( 4, 5, 6, 7 );                            // 4 actual parameters
ListInts( 8, 9, 10, 11, 12 );                      // 5 actual parameters`

当对参数数组使用带有独立实际参数的调用时,编译器会执行以下操作:

  • It gets a list of actual parameters and uses them to create and initialize an array in the heap.
  • It stores the reference to the array in the parameter on the stack.
  • If there is no argument at the position of the corresponding shape parameter group, the compiler creates an array of zero elements and uses.

例如,下面的代码声明了一个名为ListInts的方法,它采用一个参数数组。Main声明三个int并将它们传递给数组。

`   class MyClass                  Parameter array

{                                  ↓                          
public void ListInts( params int[] inVals )
{
if ( (inVals != null) && (inVals.Length != 0))
for (int i = 0; i < inVals.Length; i++)     // Process the array.
{
inVals[i] = inVals[i] * 10;
Console.WriteLine("{0}", inVals[i]);   // Display new value.
}
}
}

class Program
{
static void Main()
{
int first = 5, second = 6, third = 7;          // Declare three ints.

MyClass mc = new MyClass();
mc.ListInts( first, second, third );           // Call the method.

Actual parameters
Console.WriteLine("{0}, {1}, {2}", first, second, third);
}
}`

该代码产生以下输出:


50 60 70 5, 6, 7


图 5-12 说明了在方法执行的各个阶段实际和形式参数的值:

  • Before the method call, three actual parameters were already on the stack.
  • At the beginning of the method, three actual parameters will be used to initialize the array in the heap, and the reference to the array will be assigned to the parameter inVals.
  • Inside the method, the code first checks to make sure that the array reference is not null, and then processes the array by multiplying each element in the array by 10 and storing it back.
  • After the method is executed, the parameter inVals is out of range.

Image

***图 5-12。*参数数组示例

关于参数数组,需要记住的一件重要事情是,当在堆中创建数组时,实际参数的值会被复制到数组中。这样,它们就像是值参数。

  • If the array parameter is of value type, the value of is copied, and the actual parameter inside the method will not be affected .
  • If the array parameter is a reference type, then reference is copied, and the object referenced by the actual parameter can be affected inside the method.

数组作为实际参数

您也可以在方法调用之前创建并填充一个数组,并将单个数组变量作为实际参数传递。在这种情况下,编译器使用你的数组,而不是创建一个。

例如,下面的代码使用了在前一个例子中声明的方法ListInts。在这段代码中,Main创建了一个数组,并使用数组变量作为实际参数,而不是使用单独的整数。

`   static void Main()
{
int[] myArr = new int[] { 5, 6, 7 };  // Create and initialize array.

MyClass mc = new MyClass();
mc.ListInts(myArr);                   // Call method to print the values.

foreach (int x in myArr)
Console.WriteLine("{0}", x);       // Print out each element.
}`

该代码产生以下输出:


50 60 70 50 60 70


参数类型汇总

由于有四种参数类型,有时很难记住它们的各种特征。表 5-2 总结了它们,便于比较和对比。

Image

方法重载

一个类可以有多个同名的方法。这被称为方法重载。每个同名的方法必须有不同于其他方法的签名

  • The method signature of consists of the following information in the method header of the method declaration:
    • The name of the method
    • Number of parameters
    • Data type and order of parameters
    • Parameter modifier
  • The return type is not part of the signature-although people often mistakenly think it is part of the signature.
  • Note that the name of the parameter is the part of the signature, not the part.

    Not part of signature ↓ long <ins>AddValues( int a, out int b)</ins> { ... } ↑      Signature

例如,以下四个方法是方法名AddValues的重载:

   class A { long AddValues( int   a, int   b)           { return a + b;         } long AddValues( int   c, int   d, int e)    { return c + d + e;     } long AddValues( float f, float g)           { return (long)(f + g); } long AddValues( long  h, long  m)           { return h + m;         } }

下面的代码显示了重载方法名AddValues的非法尝试。这两种方法的区别仅在于返回类型和形参的名称。但是它们仍然有相同的签名,因为它们有相同的方法名;并且它们的参数的数量、类型和顺序是相同的。编译器会为这段代码生成一条错误消息。

`   class B           Signature

{                    ↓             
long AddValues( long  a, long  b) { return a+b; }
int  AddValues( long  c, long  d) { return c+d; }  // Error, same signature
}                       ↑                                   
Signature`

命名参数

到目前为止,在我们对参数的讨论中,我们使用了位置参数,正如你所记得的,这意味着每个实参的位置与相应的形参的位置相匹配。

或者,C# 允许您使用命名参数。命名参数允许您以任何顺序列出方法调用中的实际参数,只要您显式指定参数的名称。详细情况如下:

  • No change of reporting method. The parameter already has a name.
  • However, in the method call, you use the formal parameter name followed by a colon before the actual parameter value or expression, as shown in the following method call. a, b and c here are methods Calc:

的三个形参的名称

            Actual parameter values ↓     ↓    ↓ c.Calc ( <ins>c: 2,</ins> <ins>a: 4</ins>, <ins>b: 3</ins>); ↑     ↑     ↑ Named parameters

图 5-13 说明了使用命名参数的结构。

Image

***图 5-13。*使用命名参数时,在方法调用中包含参数名。方法声明中不需要任何更改。

您可以在调用中同时使用位置参数和命名参数,,但是如果您使用,所有的位置参数必须首先列出。例如,以下代码显示了一个名为Calc的方法的声明,以及使用位置和命名参数的不同组合对该方法的五个不同调用:

`   class MyClass
{
public int Calc( int a, int b, int c )
{ return ( a + b ) * c;  }

static void Main()
{
MyClass mc = new MyClass( );

int r0 = mc.Calc( 4, 3, 2 );                   // Positional Parameters
int r1 = mc.Calc( 4, b: 3, c: 2 );             // Positional and Named Parameters
int r2 = mc.Calc( 4, c: 2, b: 3 );             // Switch order
int r3 = mc.Calc( c: 2, b: 3, a: 4 );          // All named parameters
int r4 = mc.Calc( c: 2, b: 1 + 2, a: 3 + 1 );  // Named parameter expressions

Console.WriteLine("{0}, {1}, {2}, {3}, {4}", r0, r1, r2, r3, r4);
}
}`

该代码产生以下输出:

14, 14, 14, 14, 14

命名参数作为一种自我记录程序的方式是很有用的,因为它们可以在方法调用的位置显示出哪些值被赋给了哪些形参。例如,在下面对方法GetCylinderVolume的两次调用中,第二次调用提供了更多的信息,并且不容易出错。

`   class MyClass
{
double GetCylinderVolume( double radius, double height )
{
return 3.1416 * radius * radius * height;
}

static void Main( string[] args )
{
MyClass mc = new MyClass();
double volume;
 ↓    ↓                     
volume = mc.GetCylinderVolume( 3.0, 4.0 );
...
volume = mc.GetCylinderVolume( radius: 3.0, height: 4.0 );
...                                 ↑             ↑

}                                       More informative
}`

可选参数

C# 也允许可选参数。可选参数是在调用方法时可以包含或省略的参数。

要指定参数是可选的,需要在方法声明中包含该参数的默认值。指定默认值的语法与初始化局部变量的语法相同,如下面代码的方法声明所示。在这个例子中

  • Parameter b has a default value of 3.
  • Therefore, if the method is called with only one parameter, the method will use the value of 3 as the initial value of the second parameter.

`   class MyClass                Optional parameter
{                                ↓   
public int Calc( int a, int b = 3 )
{                               ↑
return a + b;       Default value assignment
}

static void Main()
{
MyClass mc = new MyClass();

int r0 = mc.Calc( 5, 6 );             // Use explicit values.
int r1 = mc.Calc( 5 );                // Use default for b.

Console.WriteLine( "{0}, {1}", r0, r1 );
}
}`

该代码产生以下输出:


11, 8


关于声明可选参数,有几件重要的事情需要了解:

  • Not all types of parameters can be optional. Figure 5-14 shows when optional parameters can be used.

    • As long as the default value can be determined at compile time, you can use the value type as an optional parameter.
    • If the default value is null, only the reference type can be used as an optional parameter.

Image

***图 5-14。*可选参数只能是值类型参数。

  • All required parameters must be declared before declaring any optional parameters. If there is a params parameter, it must be declared after all optional parameters. Figure 5-15 illustrates the required syntactic order.

Image

***图 5-15。*在方法声明中,可选参数必须在所有必需参数之后、params 参数之前声明(如果有的话)。

正如您在前面的例子中看到的,您通过在方法调用中省略相应的实际参数来指示程序使用可选参数的默认值。但是,您不能省略可选参数的任意组合,因为在许多情况下,该方法应该使用哪些可选参数是不明确的。规则如下:

  • You must omit the parameters from the end of the optional parameter list and start at the beginning.
  • That is, the last optional parameter can be omitted, or the last n optional parameters can be omitted, but any optional parameters can not be selected and omitted. They must be taken away.

`   class MyClass
{
public int Calc( int a = 2, int b = 3, int c = 4 )
{
return (a + b) * c;
}

static void Main( )
{
MyClass mc = new MyClass( );
int r0 = mc.Calc( 5, 6, 7 );   // Use all explicit values.
int r1 = mc.Calc( 5, 6 );      // Use default for c.
int r2 = mc.Calc( 5 );         // Use default for b and c.
int r3 = mc.Calc( );           // Use all defaults.

Console.WriteLine( "{0}, {1}, {2}, {3}", r0, r1, r2, r3 );
}
}`

该代码产生以下输出:


77, 44, 32, 20


要从可选参数列表中的任意位置省略可选参数,而不是从列表的末尾,您必须使用可选参数的名称来消除赋值的歧义。在本例中,您同时使用了命名参数和可选参数特性。下面的代码演示了位置参数、可选参数和命名参数的这种用法。

`   class MyClass
{
double GetCylinderVolume( double radius = 3.0, double height = 4.0 )
{
return 3.1416 * radius * radius * height;
}

static void Main( )
{
MyClass mc = new MyClass();
double volume;

volume = mc.GetCylinderVolume( 3.0, 4.0 );       // Positional
Console.WriteLine( "Volume = " + volume );

volume = mc.GetCylinderVolume( radius: 2.0 );    // Use default height
Console.WriteLine( "Volume = " + volume );

volume = mc.GetCylinderVolume( height: 2.0 );    // Use default radius
Console.WriteLine( "Volume = " + volume );

volume = mc.GetCylinderVolume( );                // Use both defaults
Console.WriteLine( "Volume = " + volume );
}
}`

该代码产生以下输出:


Volume = 113.0976 Volume = 50.2656 Volume = 56.5488 Volume = 113.0976


堆叠帧

到目前为止,您知道局部变量和参数保存在堆栈中。在这一节中,我们将进一步了解该组织。

当一个方法被调用时,内存被分配到堆栈的顶部来保存与该方法相关联的大量数据项。这个内存块被称为该方法的堆栈帧

  • The stack frame contains memory to hold the following contents:

    • Return address—where to resume execution when the method exits.
    • Those parameters that allocate memory-that is, the value parameters of the method, and the parameter array (if there is one
    • Various other management data items related to method calls.
  • When a method is called, its entire stack frame is pushed onto the stack.

  • When the method exits, its entire stack frame is popped from the stack. Pop-up stack frame is sometimes called expand stack.

例如,下面的代码声明了三个方法。方法Main调用MethodA,?? 调用MethodB,这样就创建了三个堆栈框架。当方法退出时,堆栈展开。

`   class Program
{
static void MethodA( int par1, int par2)
{
Console.WriteLine("Enter MethodA: {0}, {1}", par1, par2);
MethodB(11, 18);                                    // Call MethodB.
Console.WriteLine("Exit  MethodA");
}

static void MethodB(int par1, int par2)
{
Console.WriteLine("Enter MethodB: {0}, {1}", par1, par2);
Console.WriteLine("Exit  MethodB");
}

static void Main( )
{
Console.WriteLine("Enter Main");
MethodA( 15, 30);                                   // Call MethodA.
Console.WriteLine("Exit  Main");
}
}`

这段代码产生以下输出:


Enter Main Enter MethodA: 15, 30 Enter MethodB: 11, 18 Exit  MethodB Exit  MethodA Exit  Main


图 5-16 显示了当方法被调用时,每个方法的堆栈框架是如何放置在堆栈上的,以及当方法完成时,堆栈是如何展开的。

Image

图 5-16 。简单程序中的堆栈帧

递归

一个方法除了调用其他方法,还可以调用本身。这被称为递归

递归可以产生一些非常优雅的代码,比如下面计算一个数的阶乘的方法。请注意,在这个示例中,在方法内部,该方法使用比其输入参数小一的实际参数来调用自己。

   int Factorial(int inValue) { if (inValue <= 1) return inValue; else return inValue * <ins>Factorial(inValue - 1)</ins>;     // Call Factorial again. }                                ↑ Calls itself

一个方法调用自身的机制与它调用另一个不同的方法是完全一样的。每次调用方法时,都会将新的堆栈帧推送到堆栈上。

例如,在下面的代码中,方法Count用比它的输入参数小 1 的值调用它自己,然后打印出它的输入参数。随着递归越来越深,堆栈也越来越大。

`   class Program
{
public void Count(int inVal)
{
if (inVal == 0)
return;
Count(inVal - 1);             // Invoke this method again.

Calls itself
Console.WriteLine("{0}", inVal);
}

static void Main()
{
Program pr = new Program();
pr.Count(3);
}
}`

该代码产生以下输出:


1 2 3


?? 图 5-17 说明了代码。注意,输入值为 3 时,方法Count有四个不同的独立堆栈帧。每个都有自己的输入参数值inVal

Image

***图 5-17。*用递归方法构建和展开堆栈的例子

六、关于类的更多信息

类成员

前两章讨论了九种类成员中的两种:字段和方法。在这一章中,我将介绍除事件和操作符之外的所有其他类成员,并解释它们的特性。我将在第十四章中讲述事件。

表 6-1 显示了类成员类型的列表。已经推出的产品标有钻石。本章涉及的内容都标有勾号。那些将在后面的文本中涉及的内容用空的复选框标记。

Image

成员修饰符的顺序

之前,您看到了字段和方法的声明可以包含修饰符,如publicprivate。在这一章中,我将讨论一些额外的修饰语。因为这些修饰语中的许多可以一起使用,所以出现的问题是,它们需要什么样的顺序?

类成员声明语句由以下部分组成:核心声明、一组可选的修饰符,以及一组可选的属性。用于描述该结构的语法如下。方括号表示包含的组件集是可选的。

   [ attributes ] [ modifiers ]  CoreDeclaration

可选组件如下:

  • modifier
    • If there is a modifier, it must be placed before the core declaration.
    • If there are multiple modifiers, they can be arranged arbitrarily.
  • attribute
    • If there is an attribute, it must be placed before the modifier and the core declaration.
    • If there are multiple attributes, you can sort them arbitrarily.

到目前为止,我只解释了两个修饰符:publicprivate。我会在第二十四章中讲述属性。例如,publicstatic都是修饰符,可以一起用来修改某些声明。因为它们都是修饰语,所以可以按任意顺序排列。下面两行在语义上是等价的:

`   public static int MaxVal;

static public int MaxVal;`

图 6-1 显示了应用到目前为止显示的成员类型的组件顺序:字段和方法。注意,字段的类型和方法的返回类型不是修饰符——它们是核心声明的一部分。

Image

***图 6-1。*属性、修饰符和核心声明的顺序

实例类成员

类成员可以与类的一个实例相关联,也可以与整个类相关联;也就是说,应用于该类的所有实例。默认情况下,成员与实例相关联。你可以认为一个类的每个实例都有自己的每个类成员的副本。这些成员被称为实例成员

对一个实例字段值的更改不会影响任何其他实例中成员的值。到目前为止,您看到的字段和方法都是实例字段和实例方法。

例如,下面的代码声明了一个类D,带有一个整数字段Mem1Main创建该类的两个实例。每个实例都有自己的字段Mem1副本。更改字段的一个实例副本的值不会影响另一个实例副本的值。图 6-2 显示了D类的两个实例。

`   class D
{
public int Mem1;
}

class Program
{
static void Main()
{
D d1 = new D();
D d2 = new D();
d1.Mem1 = 10; d2.Mem1 = 28;

Console.WriteLine("d1 = {0}, d2 = {1}", d1.Mem1, d2.Mem1);
}
}`

该代码产生以下输出:


d1 = 10, d2 = 28


Image

***图 6-2。*D 类的每个实例都有自己的 Mem1 字段副本。

静态字段

除了实例字段,类还可以有所谓的静态字段。

  • The static field is shared by all instances of class , and all instances access the same memory location. Therefore, if one instance changes the value of the memory location, all instances can see the change.
  • Use the static modifier to declare a static field as follows:

   class D { int Mem1;                     // Instance field <ins>static</ins> int Mem2;              // Static field ↑ }  Keyword

例如,图 6-3 中左边的代码用静态字段Mem2和实例字段Mem1声明了类DMain定义了类D的两个实例。该图显示静态字段Mem2与任何实例的存储器分开存储。实例内部的灰色字段表示这样一个事实,即从实例方法内部,访问或更新静态字段的语法与访问任何其他成员字段的语法相同。

  • Because Mem2 is static, two instances of class D share a Mem2 field. If Mem2 is changed, the change can be seen from both.
  • Member Mem1 does not declare static, so each instance has its own different copy.

Image

***图 6-3。*静态和实例数据成员

从类外访问静态成员

在前一章中,您看到了点语法符号用于从类外部访问public实例成员。点语法表示法包括列出实例名,后跟一个点,再后跟成员名。

像实例成员一样,静态成员也可以使用点语法符号从类外部访问。但是由于没有实例,所以必须使用类名,如下所示:

   Class name ↓ D.Mem2 = 5;            // Accessing the static class member ↑ Member name

静态字段的例子

以下代码通过添加两个方法扩展了前面的类D:

  • One method sets the values of two data members.
  • Another method displays the values of two data members.

`   class D {
int        Mem1;
static int Mem2;

public void SetVars(int v1, int v2) // Set the values

↑ Access as if it were an instance field

public void Display( string str )
{  Console.WriteLine("{0}: Mem1= {1}, Mem2= {2}", str, Mem1, Mem2); }
}
Access as if it were an instance field
class Program {
static void Main()
{
D d1 = new D(), d2 = new D();   // Create two instances.

d1.SetVars(2, 4);               // Set d1's values.
d1.Display("d1");

d2.SetVars(15, 17);             // Set d2's values.
d2.Display("d2");

d1.Display("d1");       // Display d1 again and notice that the
}                          // value of static member Mem2 has changed!
}`

这段代码产生以下输出:


d1: Mem1= 2, Mem2= 4 d2: Mem1= 15, Mem2= 17 d1: Mem1= 2, Mem2= 17


静态成员的生存期

静态成员的生存期不同于实例成员的生存期。

  • As you saw before, instance members appear when the instance is created and disappear when the instance is destroyed. however
  • Static members exist and are accessible , even if there is no instance of the class .

图 6-4 展示了一个类D,带有一个静态字段Mem2。尽管Main没有定义该类的任何实例,但它将值5赋给静态字段,并毫无问题地打印出来。

Image

***图 6-4。*没有类实例的静态字段仍然可以被赋值和读取,因为字段与类相关联,而不是与实例相关联。

图 6-4 中的代码产生以下输出:


Mem2 = 5


Image 注意即使没有类的实例,静态成员仍然存在。如果一个静态字段有一个初始化器,那么这个字段在使用这个类的任何静态字段之前被初始化,但是不一定在程序执行的开始。

静态函数成员

除了静态字段,还有静态函数成员。

  • Static function members, like static fields, are independent of any class instance. Even if there is no instance of the class, you can still call static methods.
  • Static function members cannot access instance members. However, they can access other static members.

例如,以下类包含一个静态字段和一个静态方法。注意,静态方法的主体访问静态字段。

    class X { static public int A;                               // Static field static public void PrintValA()                     // Static method { Console.WriteLine("Value of A: {0}", A); }                                          ↑ }                                        Accessing the static field

以下代码使用前面代码中定义的类X:

   class Program { static void Main() { X.A = 10;               // Use dot-syntax notation X.PrintValA();          // Use dot-syntax notation }  ↑ }  Class name

该代码产生以下输出:


Value of A: 10


图 6-5 说明了前面的代码。

Image

***图 6-5。*一个类的静态方法可以被调用,即使这个类没有实例。

其他静态类成员类型

在表 6-2 中显示了可以声明static的类成员类型。其他成员类型不能声明为static

Image

成员常数

成员常量类似于上一章介绍的局部常量,只是它们是在类声明中声明的,而不是在方法中声明的,如下例所示:

`   class MyClass
{
const int IntVal = 100;              // Defines a constant of type int
↑           ↑                // with a value of 100.
}         Type        Initializer

const double PI = 3.1416;               // Error: cannot be declared outside a type
// declaration`

与局部常量一样,用于初始化成员常量的值必须在编译时可计算,并且通常是预定义的简单类型之一或由它们组成的表达式。

   class MyClass { const int IntVal1 = 100; const int IntVal2 = 2 * IntVal1;  // Fine, since the value of IntVal1 }                                    // was set in the previous line.

与局部常数一样,不能在声明成员常数后将其赋值。

   class MyClass { const int IntVal;                // Error: initialization is required. IntVal = 100;                    // Error: assignment is not allowed. }

Image 注意与 C 和 C++不同,C# 中没有全局常量。每个常数都必须在类型中声明。

常数如静力学

然而,成员常量比局部常量更有趣,因为它们的行为类似于静态值。它们对该类的每个实例都是“可见”的,即使没有该类的实例,它们也是可用的。与实际的静态不同,常量没有自己的存储位置,在编译时由编译器以类似于 C 和 C++中的#define值的方式替换。

例如,下面的代码用常量字段PI声明了类XMain没有创建X的任何实例,但是它可以使用字段PI并打印它的值。图 6-6 说明了代码。

`   class X

class Program
{
static void Main()
{
Console.WriteLine("pi = {0}", X.PI);    // Use static field PI
}
}`

该代码产生以下输出:


pi = 3.1416


Image

***图 6-6。*常量字段的行为类似静态字段,但在内存中没有存储位置。

尽管常量成员的行为类似于静态,但是您不能将常量声明为static,如下面的代码行所示。

   static const double PI = 3.14;     // Error: can't declare a constant as static

属性

属性是表示类或类实例中数据项的成员。使用属性看起来非常像对字段进行写入或读取。语法是一样的。

例如,下面的代码展示了一个名为MyClass的类的用法,它既有一个公共字段又有一个公共属性。从它们的用法来看,你无法区分它们。

`   MyClass mc = new MyClass();

mc.MyField    = 5;                               // Assigning to a field
mc.MyProperty = 10;                              // Assigning to a property

WriteLine("{0} {1}", mc.MyField, mc.MyProperty); // Read field and property`

像字段一样,属性具有以下特征:

  • Is a named class member.
  • There is one type of it.
  • It can be assigned to and read from.

然而,与字段不同,属性是函数成员,因此:

  • It does not necessarily allocate memory for data storage.
  • It executes the code.

一个属性是两个匹配方法的命名集合,称为访问器

  • set Accessors are used to assign values to attributes.

get

图 6-7 显示了一个属性的表示。左边的代码显示了声明类型为int的名为MyValue的属性的语法。右边的图像显示了属性在文本中是如何可视化表示的。请注意,访问器显示在后面,因为,您很快就会看到,它们不是可直接调用的。

Image

***图 6-7。*一个名为 MyValue 的示例属性,类型为 int

属性声明和访问器

setget访问器有预定义的语法和语义。您可以将set访问器视为一个具有单个参数的方法,该参数“设置”属性值。get访问器没有参数,返回属性值。

  • set The accessor always has the following contents:
    • A single implicit value parameter named value, and the property
    • The return type of is the same void
  • get The accessor always has the following contents:
    • No parameter
    • An and attribute

的返回类型相同

图 6-8 显示了属性声明的结构。注意在图中,两个访问器声明都没有显式参数或返回类型声明。他们不需要它们,因为它们是中隐含的财产类型。

Image

***图 6-8。*属性声明的语法和结构

set访问器中的隐式参数value是一个正常值参数。像其他值参数一样,您可以使用它将数据发送到方法体中,或者在本例中,发送到访问器块中。一旦进入程序块,就可以像普通变量一样使用value,包括给它赋值。

关于访问器的其他要点如下:

  • All paths implemented through get accessor must contain a return statement that returns the attribute type value.
  • set and get accessors can be declared in any order, and there are no other methods except these two accessors on an attribute.

一个属性的例子

下面的代码展示了一个名为C1的类的声明示例,它包含一个名为MyValue的属性。

  • Note that the attribute itself does not have any storage. Instead, the accessor decides how to handle the incoming data and the data that should be sent out. In this case, the attribute is stored in a field named TheRealValue.
  • set The accessor gets its input parameter value and assigns the value to the field TheRealValue.
  • get The accessor only returns the value of field TheRealValue.

图 6-9 说明了代码。

`   class C1
{
private int TheRealValue;               // Field: memory allocated

public int MyValue                      // Property: no memory allocated
{
set

get
{
return TheRealValue;
}
}
}` Image

***图 6-9。*属性访问器经常使用字段进行存储

使用属性

如前所述,您可以像访问字段一样读写属性。访问器是隐式调用的。

  • To write an attribute, use the attribute name to the left of the assignment statement.
  • To read an attribute, use the name of the attribute in the expression.

例如,下面的代码包含一个名为MyValue的属性声明的概要。您只使用属性名写入和读取属性,就像它是一个字段名一样。

   int MyValue             // Property declaration { set{ ... } get{ ... } } ... Property name ↓ MyValue = 5;            // Assignment: the set method is implicitly called. z = MyValue;            // Expression: the get method is implicitly called. ↑ Property name

根据您是写入属性还是读取属性,隐式调用适当的访问器。您不能显式调用访问器。尝试这样做会产生编译错误。

   y = MyValue.get();      // Error! Can't explicitly call get accessor. MyValue.set(5);         // Error! Can't explicitly call set accessor.

属性和关联字段

一个属性通常与一个字段相关联,如前两节所示。一种常见的做法是通过声明字段private和声明属性public将字段封装在类中,以提供从类外部对字段的受控访问。与资产相关联的字段被称为后台字段后台存储

例如,以下代码使用公共属性MyValue对私有字段TheRealValue进行受控访问:

`   class C1
{
private int TheRealValue = 10;   // Backing Field: memory allocated
public  int MyValue              // Property: no memory allocated
{
set  // Sets the value of field TheRealValue
get{ return TheRealValue; }   // Gets the value of the field
}
}

class Program
{
static void Main()
{
Read from the property as if it were a field.
C1 c = new C1();                       ↓    
Console.WriteLine("MyValue:  {0}", c.MyValue);

c.MyValue = 20;        ← Use assignment to set the value of a property.
Console.WriteLine("MyValue:  {0}", c.MyValue);
}
}`

有几种命名属性及其支持字段的惯例。一个惯例是对两个名称使用相同的字符串,但是对字段使用骆驼大小写,对属性使用帕斯卡大小写。(Camel case 描述了一个复合词标识符,其中每个词的第一个字母,除了第一个,都是大写的,其余的字母都是小写的。Pascal 大小写是复合词中每个单词的第一个字母大写的地方。)虽然这违反了一般规则,即不同的标识符只有大小写不同是不好的做法,但它的优点是以一种有意义的方式将两个标识符联系在一起。

另一个约定是对属性使用 Pascal 大小写,然后对字段使用相同标识符的 camel 大小写版本,前面带下划线。

以下代码显示了这两种约定:

`   private int firstField;                  // Camel casing
public  int FirstField                   // Pascal casing
{
get { return firstField; }
set
}

private int _secondField;                // Underscore and camel casing
public  int SecondField
{
get { return _secondField; }
set
}`

执行其他计算

属性访问器不仅限于从关联的后台字段来回传递值;getset访问器可以执行任何计算,或者不执行任何计算。唯一需要的动作get访问器返回一个属性类型的值。

例如,下面的例子展示了一个有效(但可能没用)的属性,当调用它的get访问器时,它只返回值5。当调用set访问器时,它不做任何事情。隐式参数value的值被忽略。

   public int Useless { set{  /* I'm not setting anything.                */ } get {     /* I'm always just returning the value 5.   */ return 5; } }

下面的代码展示了一个更实际、更有用的属性,其中set访问器在设置关联字段之前执行过滤。set访问器将字段TheRealValue设置为输入值,除非输入值大于 100。在这种情况下,它将TheRealValue设置为100

   int TheRealValue = 10;                     // The field int MyValue                                // The property { set                                     // Sets the value of the field { TheRealValue = value > 100           // but makes sure it's not > 100 ? 100 : value; } get                                     // Gets the value of the field { return TheRealValue; } }

Image 注意在前面的代码示例中,等号和语句结尾之间的语法可能看起来有些奇怪。该表达式使用了条件运算符,我将在第八章的中详细介绍。条件运算符是一个三元运算符,计算问号前面的表达式,如果表达式计算结果为true,则返回问号后面的表达式。否则,它返回冒号后的表达式。有些人会在这里使用一个if...then语句,但是条件操作符更合适,当我们在第八章中详细查看这两个构造时,你会看到。

只读和只写属性

通过省略属性的声明,可以不定义属性的一个或另一个(但不是两个)访问器。

  • The attribute of only one get accessor is called read-only attribute. Read-only property is a safe way to transfer data items from a class or class instance, and does not allow excessive access.
  • Only the attribute of set accessor is called , and only the attribute of is written. Write-only property is a safe way to transfer data items from outside the class to the class without excessive access.
  • At least one of the two accessors must be defined, or the compiler will generate an error message.

图 6-10 显示了只读和只写属性。

Image

图 6-10 。属性可以有一个或另一个未定义的访问器。

属性与公共字段

作为首选的编码实践,属性优先于公共字段有几个原因:

由于属性是函数成员,而不是数据成员,它们允许你处理输入和输出,这是你不能用公共字段做的。* You can have read-only or write-only attributes, but you can't have these characteristics of a field.* The semantics of compiled variables and compiled attributes are different.

第二点在您释放被其他代码访问的程序集时有所暗示。例如,有时您可能想使用公共字段而不是属性,理由是如果您需要向字段中保存的数据添加处理,您总是可以在以后将字段更改为属性。这是真的,但是如果你做了那样的改变,你也将不得不重新编译任何其他访问该字段的程序集*,因为字段和属性的编译语义是不同的。另一方面,如果你将它实现为一个属性,并且仅仅改变它的实现,你就不需要重新编译其他访问它的程序集。*

一个计算的只读属性的例子

到目前为止,在大多数示例中,属性都与一个支持字段相关联,并且getset访问器已经引用了该字段。但是,属性不必与字段相关联。在下面的例子中,get访问器计算的返回值。

在代码中,毫不奇怪,class RightTriangle表示一个直角三角形。图 6-11 显示了只读属性Hypotenuse

  • It has two common fields, which represent the lengths of the two right-angled sides of the triangle. These fields can be written and read.
  • The third edge is represented by the attribute Hypotenuse, which is read-only and its return value is based on the lengths of the other two edges. It is not stored in the field. Instead, it will calculate the correct values for the current values of A and B as needed.

`   class RightTriangle
{
public double A = 3;
public double B = 4;
public double Hypotenuse                    // Read-only property
{
get{ return Math.Sqrt((AA)+(BB)); }    // Calculate return value
}
}

class Program
{
static void Main()
{
RightTriangle c = new RightTriangle();
Console.WriteLine("Hypotenuse:  {0}", c.Hypotenuse);
}
}`

该代码产生以下输出:


Hypotenuse:  5


Image

图 6-11 。只读属性斜边

自动实现属性

因为属性经常与后台字段相关联,C# 提供了自动实现的属性,或者自动实现的属性,这允许你只声明属性,而不声明后台字段。编译器会为您创建一个隐藏的后台字段,并自动将getset访问器连接到该字段。

关于自动实现的属性的要点如下:

  • You don't have to declare backup fields-the compiler allocates storage space for you according to the type of attribute.
  • You cannot provide the bodies of accessors-they must simply be declared as semicolons. get serves as a simple read of the memory, and set serves as a simple write.
  • You can't access background fields except through accessors. Because you can't access it in any other way, it doesn't make sense to have read-only or write-only automatically implemented properties-so they are not allowed.

下面的代码显示了自动实现的属性的示例:

`   class C1
{                  ← No declared backing field
public int MyValue                         // Allocates memory
{
set; get;
}     ↑   ↑
}  The bodies of the accessors are declared as semicolons.

class Program
{
static void Main()
{                        Use auto-implemented properties as regular properties.
C1 c = new C1();                         ↓
Console.WriteLine("MyValue:  {0}", c.MyValue);

c.MyValue = 20;
Console.WriteLine("MyValue:  {0}", c.MyValue);
}
}`

该代码产生以下输出:


MyValue:  0 MyValue:  20


除了方便之外,自动实现的属性允许您轻松地将属性插入到您可能想要声明公共字段的地方。

静态属性

属性也可以声明为static。像所有静态成员一样,静态属性的访问器具有以下特征:

  • They can't access instance members of a class-although they can be accessed by them.
  • Whether this class has instances or not, they exist.
  • When you access them from outside the class, you must refer to them by class name instead of instance name.

例如,下面的代码展示了一个具有名为MyValue的自动实现的静态属性的类。在Main的前三行中,属性被访问,即使没有类的实例。Main的最后一行调用一个实例方法,该方法从类内部的访问属性。

`   class Trivial
{
public static int MyValue { get;  set; }

public void PrintValue()               Accessed from inside the class
{                                                  ↓
Console.WriteLine("Value from inside: {0}", MyValue);
}
}

class Program
{
static void Main()                   Accessed from outside the class
{                                              ↓       
Console.WriteLine("Init Value: {0}", Trivial.MyValue);
Trivial.MyValue = 10;           ←  Accessed from outside the class
Console.WriteLine("New Value : {0}", Trivial.MyValue);

Trivial tr = new Trivial();
tr.PrintValue();
}
}`


Init Value: 0 New Value : 10 Value from inside: 10


实例构造函数

一个实例构造器是一个特殊的方法,每当一个类的新实例被创建时就被执行。

  • Constructors are used to initialize the state of class instances.
  • If you want to create an instance of your class from outside the class, you need to declare the constructor public.

图 6-12 显示了一个构造函数的语法。构造函数看起来像类声明中的其他方法,但有以下例外:

  • The name of the constructor is the same as the class name.
  • The constructor cannot have a return value.

Image

图 6-12 。构造函数声明

例如,下面的类使用其构造函数来初始化其字段。在这种情况下,它有一个名为TimeOfInstantiation的字段,用当前日期和时间初始化。

   class MyClass { DateTime TimeOfInstantiation;                        // Field ... public MyClass()                                     // Constructor { TimeOfInstantiation = DateTime.Now;               // Initialize field } ... }

Image 注意刚刚完成了静态属性部分,仔细看看初始化TimeOfInstantiation的那一行。这个DateTime类(实际上是一个struct,但是你可以把它当成一个类,因为我还没有覆盖struct s)来自 BCL,NowDateTime的一个静态属性。属性创建了一个DateTime类的新实例,用系统时钟的当前日期和时间初始化它,并返回一个对新DateTime实例的引用。

带参数的构造函数

构造函数在以下方面与其他方法相似:

  • A constructor can have parameters. The syntax of the parameter is exactly the same as other methods.
  • A constructor can be overloaded.

当您使用一个对象创建表达式来创建一个类的新实例时,您可以使用new操作符,后跟该类的一个构造函数。new操作符使用该构造函数来创建类的实例。

例如,在下面的代码中,Class1有三个构造函数:一个不带参数,一个带int,另一个带stringMain使用每一个创建一个实例。

`   class Class1
{
int Id;
string Name;

public Class1()            { Id=28;    Name="Nemo"; }   // Constructor 0
public Class1(int val)     { Id=val;   Name="Nemo"; }   // Constructor 1
public Class1(String name)    // Constructor 2

public void SoundOff()
{ Console.WriteLine("Name {0},   Id {1}", Name, Id); }
}

class Program
{
static void Main()
{
Class1 a = new Class1(),                     // Call constructor 0.
b = new Class1(7),                    // Call constructor 1.
c = new Class1("Bill");               // Call constructor 2.

a.SoundOff();
b.SoundOff();
c.SoundOff();
}
}`

该代码产生以下输出:


Name Nemo,   Id 28 Name Nemo,   Id 7 Name Bill,   Id 0


默认构造函数

如果在类声明中没有显式提供实例构造函数,则编译器会提供一个隐式的默认构造函数,该构造函数具有以下特征:

  • It doesn't need parameters.
  • It has an empty body.

如果你为一个类声明了任何构造函数*,那么编译器不会为这个类定义默认的构造函数。*

例如,以下示例中的Class2声明了两个构造函数。

  • Because there is at least one explicitly defined constructor, the compiler will not create any additional constructors.
  • In Main, an attempt was made to create a new instance with a constructor without parameters. Because has no constructor with zero parameters, the compiler will generate an error message.

`   class Class2
{
public Class2(int Value)       // Constructor 0
public Class2(String Value)    // Constructor 1
}

class Program
{
static void Main()
{
Class2 a = new Class2();   // Error! No constructor with 0 parameters
...
}
}`

Image 注意你可以将访问修饰符赋给实例构造函数,就像你赋给其他成员一样。您还需要声明构造函数public,这样您就可以从类外部创建实例。你也可以创建private构造函数,它不能从类外调用,但可以在类内使用,你将在下一章看到。

静态构造函数

构造函数也可以声明为static。实例构造函数初始化一个类的每个新实例,而static构造函数在类级别初始化项目。通常,静态构造函数初始化类的静态字段。

  • Items of class level are initialized before referencing any static members.
    • Before creating any instances of the class
  • Static constructors are similar to instance constructors in the following aspects:
    • The name of the static constructor must be the same as that of the class.
    • The constructor cannot return a value.
  • Static constructors differ from instance constructors in the following aspects:
    • Static constructors use the static keyword in declarations.
    • A class can only have one static constructor and no parameters.
    • Static constructors cannot have accessibility modifiers.

下面是一个静态构造函数的示例。注意,它的形式与实例构造函数的形式相同,但是增加了关键字static

   class Class1 { static Class1 () { ...                // Do all the static initializations. } ...

关于静态构造函数,您应该知道的其他重要事情如下:

  • A class can have both static constructors and instance constructors.
  • Like a static method, a static constructor cannot access the instance members of its class, nor can it use the this accessor, which we will introduce soon. Static constructors cannot be explicitly called from programs. They are automatically called by the system, at some time before any instance of the class is created.
    • The that precedes any static member of the reference class.

静态构造函数的例子

下面的代码使用一个静态构造函数来初始化一个名为RandomKey、类型为Random的私有静态字段。Random是由 BCL 提供的产生随机数的类。它在System名称空间中。

`   class RandomNumberClass
{
private static Random RandomKey;         // Private static field

static RandomNumberClass()               // Static constructor
{
RandomKey = new Random();             // Initialize RandomKey
}

public int GetRandomNumber()
{
return RandomKey.Next();
}
}

class Program
{
static void Main()
{
RandomNumberClass a = new RandomNumberClass();
RandomNumberClass b = new RandomNumberClass();

Console.WriteLine("Next Random #: {0}", a.GetRandomNumber());
Console.WriteLine("Next Random #: {0}", b.GetRandomNumber());
}
}`

该代码的一次执行产生了以下输出:


Next Random #: 47857058 Next Random #: 1124842041


对象初始化器

到目前为止,在本文中,您已经看到对象创建表达式由关键字new后跟一个类构造函数及其参数列表组成。一个对象初始化器通过在表达式末尾放置一个成员初始化列表来扩展语法。对象初始化器允许你在创建一个新的对象实例时设置字段和属性的值。

语法有两种形式,如下所示。一种形式包含构造函数的参数列表,另一种不包含。注意,第一种形式甚至没有使用圆括号来括住参数列表。

                                                  Object initializer <ins>↓</ins>    new *TypeName*          { *FieldOrProp* = *InitExpr*, *FieldOrProp* = *InitExpr*, ...} new *TypeName*(*ArgList*) { <ins>*FieldOrProp = InitExpr*</ins>, <ins>*FieldOrProp = InitExpr*</ins>, ...} ↑                      ↑ Member initializer            Member initializer

例如,对于一个名为Point的类,它有两个公共整数字段XY,您可以使用下面的表达式来创建一个新的对象:

   new Point { <ins>X = 5</ins>, <ins>Y = 6</ins> }; ↑      ↑ Init X    Init Y

关于对象初始值设定项,需要了解的重要事项如下:

  • The code that creates the object must be able to access the fields and properties being initialized. For example, in the previous code, X and Y must be public.
  • Initialization occurs after the completion of the constructor, so these values may have been set in the constructor and then reset to the same or different values in the object initialization.

下面的代码展示了一个使用对象初始化器的例子。在Main中,pt1只调用构造函数,它设置它的两个字段的值。然而,对于pt2,构造函数将字段的值设置为 1 和2,初始化器将它们更改为56

`   public class Point

class Program
{
static void Main( )
{                            Object initializer
Point pt1 = new Point();       ↓       
Point pt2 = new Point   ;
Console.WriteLine("pt1: {0}, {1}", pt1.X, pt1.Y);
Console.WriteLine("pt2: {0}, {1}", pt2.X, pt2.Y);
}
}`

该代码产生以下输出:


pt1: 1, 2 pt2: 5, 6


析构函数

析构函数在不再引用某个类的实例后执行清理或释放非托管资源所需的操作。非托管资源是使用 Win32 API 获得的文件句柄或非托管内存块。这些东西不是你用就能得到的 .NET 资源,所以如果您坚持使用 .NET 类,你不需要为你的类写析构函数。出于这个原因,我打算把对析构函数的描述留到第二十五章。

只读修饰符

可以用readonly修饰符声明一个字段。其效果类似于将字段声明为const,因为一旦设置了值,就不能更改。

  • Although the const field can only be initialized in the declaration statement of the field, the readonly field can be set in any of the following places:
    • Field declaration statement-similar to const.
    • The constructor of any class. If it is a static field, it must be completed in the static constructor.
  • Although the value of const field must be determined at compile time, the value of readonly field can be determined at run time. This extra freedom allows you to set different values in different environments or in different constructors!
  • Unlike const, it is always like a static one. Here is the real situation of a readonly field:
    • It can be an instance field or a static field.
    • It has a storage location in memory.

例如,下面的代码声明了一个名为Shape的类,有两个readonly字段。

  • The field PI is initialized in its declaration.
  • The field NumberOfSides is set to 3 or 4, depending on which constructor is called.

`   class Shape
{  Keyword           Initialized
↓                ↓     
readonly double PI = 3.1416;
readonly int    NumberOfSides;
↑                ↑
Keyword            Not initialized

public Shape(double side1, double side2)                  // Constructor
{
// Shape is a rectangle
NumberOfSides = 4;

... Set in constructor
}

public Shape(double side1, double side2, double side3)    // Constructor
{
// Shape is a triangle
NumberOfSides = 3;

... Set in constructor
}
}`

这个关键字

在类中使用的关键字this是对当前实例的引用。它只能在下列类成员的中使用:

  • Instance constructor.
  • Example tactics.
  • And instance accessors for property indexers. (The indexer will be introduced in the next section. )

显然,由于静态成员不是实例的一部分,所以不能在任何静态函数成员的代码中使用this关键字。相反,它用于以下用途:

  • Classification members and local variables or parameters

  • As an actual parameter

例如,下面的代码声明了类MyClass,带有一个int字段和一个采用单个int参数的方法。方法比较参数和字段的值,并返回较大的值。唯一复杂的因素是字段和形参的名称是相同的:Var1。通过使用this访问关键字来引用字段,这两个名称在方法内部是有区别的。

`   class MyClass {
int Var1 = 10;
↑    Both are called “Var1”    ↓
public int ReturnMaxSum(int Var1)
{       Parameter     Field
↓        ↓    
return Var1 > this.Var1
? Var1                 // Parameter
: this.Var1;           // Field
}
}

class Program {
static void Main()
{
MyClass mc = new MyClass();

Console.WriteLine("Max: {0}", mc.ReturnMaxSum(30));
Console.WriteLine("Max: {0}", mc.ReturnMaxSum(5));
}
}`

该代码产生以下输出:


Max: 30 Max: 10


索引器

假设你要定义类Employee,有三个类型为string的字段(如图 6-13 中的所示)。然后,您可以使用它们的名称来访问这些字段,如Main中的代码所示。

Image

图 6-13 。没有索引器的简单类

然而,有时候用索引访问它们会很方便,就好像实例是一个字段数组一样。这正是索引器允许你做的事情。如果你要为类Employee写一个索引器,方法Main可能看起来像图 6-14 中的代码。注意,索引器不使用点语法符号,而是使用索引符号,它由方括号之间的索引组成。

Image

图 6-14 。使用索引字段

什么是索引器?

索引器是一对getset访问器,类似于属性的访问器。图 6-15 显示了一个可以获取和设置string类型值的类的索引器的表示。

Image

图 6-15 。索引器的表示

索引器和属性

索引器和属性在很多方面都很相似。

  • Like the property, the indexer does not allocate memory for storage.
  • Indexers and properties are mainly used to provide access to and other data members. These data members are associated with indexers and properties, which provide get and set access.
    • A attribute usually represents a single data member.
    • A indexer usually represents multiple data members.

Image 注意你可以把一个索引器想象成一个属性,它提供对类的多个数据成员的获取和设置访问。您可以通过提供索引来选择许多可能的数据成员中的哪一个,索引本身可以是任何类型,而不仅仅是数字。

关于索引器,您还应该知道以下几点:

  • Like properties, indexers can have one or two accessors.
  • Indexers are always instance members; Therefore, the indexer cannot be declared as static.
  • Like attributes, the code that implements get and set accessors need not be associated with any fields or attributes. The code can do anything, or nothing, as long as the get accessor returns a value of a specified type.

声明一个索引器

声明索引器的语法如下所示。请注意以下关于索引器的内容:

  • An indexer has no name . The name is replaced by the keyword this.
  • The parameter table is between the brackets in .
  • There must be at least one parameter declaration in the parameter list.

             Keyword     Parameter list ↓   <ins>        ↓          </ins> ReturnType this [ *Type param1*, ... ] {               ↑                  ↑ get     Square bracket   Square bracket { ... } set { ... } }

声明索引器类似于声明属性。图 6-16 显示了句法的异同。

Image

图 6-16 。比较索引器声明和属性声明

索引器设置访问器

当索引器是赋值的目标时,set访问器被调用并接收两项数据,如下所示:

  • An implicit parameter named value that holds the data to be stored.
  • One or more index parameters that indicate where it should be stored.

    emp[0] = "Doe"; ↑      ↑ Index   Value Parameter

您在set访问器中的代码必须检查索引参数,确定数据应该存储在哪里,然后存储它。

图 6-17 显示了set访问器的语法和含义。图的左侧显示了访问器声明的实际语法。右侧显示了访问器的语义,如果它是使用普通方法的语法编写的。右图显示了set访问器具有以下语义:

  • It has a void return type.
  • Use the same parameter list as in the indexer declaration.
  • It has an implicit value parameter named value, which is of the same type as the indexer.

Image

图 6-17 。set 访问器声明的语法和含义

索引器获取访问器

当索引器用于检索值时,使用一个或多个索引参数调用get访问器。索引参数表示要检索的值。

   string s = emp[0]; ↑ Index parameter

get访问器主体中的代码必须检查索引参数,确定它们代表哪个字段,并返回该字段的值。

图 6-18 显示了get访问器的语法和含义。图的左侧显示了访问器声明的实际语法。右侧显示了访问器的语义,如果它是使用普通方法的语法编写的。get访问器的语义如下:

  • It is the same as the parameter list in the indexer declaration.

Image

图 6-18 。get 访问器声明的语法和含义

关于索引器的更多信息

与属性一样,getset访问器不能被显式调用。相反,当索引器用于值的表达式时,会自动调用get访问器。当用赋值语句给索引器赋值时,会自动调用set访问器。

当“调用”索引器时,参数在方括号之间提供。

     Index   Value ↓      ↓ emp[0] = "Doe";                               // Calls set accessor string NewName = emp[0];                      // Calls get accessor ↑ Index

为雇员示例声明索引器

下面的代码为前面的例子声明了一个索引器:class Employee

  • The indexer must read and write the value of type string, so string must be declared as the type of indexer. It must be declared as public so that it can be accessed from outside the class.
  • The three fields in the example are arbitrarily indexed as integers 0 to 2, so the parameter between square brackets (called index in this example) must be of type int.
  • In the body of the set accessor, the code determines which field the index refers to and assigns the value of the implicit variable value to it. In the body of the get accessor, the code determines which field the index refers to and returns the value of that field.

`   class Employee
{
public string LastName;                     // Call this field 0.
public string FirstName;                    // Call this field 1.
public string CityOfBirth;                  // Call this field 2.

public string this[int index]               // Indexer declaration
{
set                                      // Set accessor declaration
{
switch (index) {
case 0: LastName = value;
break;
case 1: FirstName = value;
break;
case 2: CityOfBirth = value;
break;

default:                           // (Exceptions in Ch. 11)
throw new ArgumentOutOfRangeException("index");
}
}

get                                      // Get accessor declaration
{
switch (index) {
case 0: return LastName;
case 1: return FirstName;
case 2: return CityOfBirth;

default:                           // (Exceptions in Ch. 11)
throw new ArgumentOutOfRangeException("index");
}
}
}
}`

另一个索引器例子

以下是索引类Class1的两个int字段的附加示例:

`   class Class1
{
int Temp0;                         // Private field
int Temp1;                         // Private field
public int this [ int index ]      // The indexer
{
get
{
return ( 0 == index )        // Return value of either Temp0 or Temp1
? Temp0
: Temp1;
}

set
{
if( 0 == index )
Temp0 = value;            // Note the implicit variable "value".
else
Temp1 = value;            // Note the implicit variable "value".
}
}
}

class Example
{
static void Main()
{
Class1 a = new Class1();

Console.WriteLine("Values -- T0: {0},  T1: {1}", a[0], a[1]);
a[0] = 15;
a[1] = 20;
Console.WriteLine("Values -- T0: {0}, T1: {1}", a[0], a[1]);
}
}`

该代码产生以下输出:


Values -- T0: 0,  T1: 0 Values -- T0: 15, T1: 20


索引器重载

一个类可以有任意数量的索引器,只要参数列表是不同的;分度器类型不同是不够的。这被称为索引器重载,因为所有索引器都有相同的“名字”——this访问引用。

例如,下面的类有三个索引器:两个类型为string,一个类型为int。在两个string类型的索引器中,一个有一个int参数,另一个有两个int参数。

`   class MyClass
{
public string this [ int index ]
{
get
set
}

public string this [ int index1, int index2 ]
{
get
set
}

public int this [ float index1 ]
{
get
set
}

...
}`

Image 注意记住一个类的重载索引器必须有不同的参数列表。

访问器上的访问修饰符

在这一章中,你已经看到了两种类型的函数成员有getset访问器:属性和索引器。默认情况下,成员的两个访问者与成员本身具有相同的访问级别。也就是说,如果一个属性的访问级别是public,那么它的两个访问器具有相同的访问级别。索引器也是如此。

但是,您可以为这两个访问者分配不同的访问级别。例如,下面的代码展示了声明私有set访问器和公共get访问器的一个常见且重要的范例。get是公共的,因为属性的访问级别是公共的。

请注意,在这段代码中,虽然可以从类外部读取属性,但它只能从类内部设置,在本例中是由构造函数设置的。这是封装的一个重要工具。

`   class Person     Accessors with different access levels
{                        ↓        ↓     
public string Name { get; private set; }
public Person( string name )

}

class Program
{
static public void Main( )
{
Person p = new Person( "Capt. Ernest Evans" );
Console.WriteLine( "Person's name is {0}", p.Name );
}
}`

该代码产生以下输出:


Person's name is Capt. Ernest Evans


对于访问者的访问修饰符有一些限制。最重要的如下:

  • Only when a member (attribute or indexer) has both get accessors and set accessors can accessors have access modifiers.
  • Although both accessors must exist, only one of them can have access modifiers.
  • The access modifier of the accessor must be stricter than the access level of the member .

图 6-19 显示了访问级别的层次结构。在图表中,访问者的访问级别必须严格低于成员的访问级别。

例如,如果一个属性的访问级别为public,那么您可以将图表上四个较低的访问级别中的任何一个授予其中一个访问者。但是如果属性的访问级别是protected,那么您可以在其中一个访问器上使用的唯一访问修饰符是private

Image

图 6-19 。严格限制访问器级别的层次结构

分部类和分部类型

一个类的声明可以在几个分部类声明中划分。

  • Each partial classes declaration contains declarations of some class members.
  • The partial classes declaration of a class can be in the same file or in different files.

与单个关键字class相比,每个部分声明必须标记为partial class。除了添加了类型修饰符partial,分部类的声明看起来和普通类的声明一样。

`   Type modifier

partial class MyPartClass    // Same class name as following
{
member1 declaration
member2 declaration
...
}

Type modifier

partial class MyPartClass    // Same class name as preceding
{
member3 declaration
member4 declaration
...
}`

Image 注意类型修饰符partial不是一个关键字,所以在其他情况下你可以在你的程序中使用它作为标识符。但是当用在关键字classstructinterface之前时,它表示使用了分部类型。

例如,图 6-20 左边的方框代表一个带有类声明的文件。图中右边的方框表示被分成两个文件的同一个类声明。

Image

图 6-20 。使用分部类型的类拆分

组成一个类的所有分部类声明必须一起编译。使用分部类声明的类与所有类成员都在单个类声明体中声明的意义相同。

Visual Studio 在其标准 Windows 程序模板中使用此功能。当从标准模板创建 ASP.NET 项目、Windows 窗体项目或 Windows Presentation Foundation(WPF)项目时,这些模板会为每个网页、窗体或 WPF 窗口创建两个类文件。在 ASP.NET 或 Windows 窗体的情况下,以下是正确的:

  • A file contains a partial classes containing code generated by Visual Studio, which declares the components on the page. You should not modify the partial classes in this file, because when you modify the component on the page, it will be regenerated by Visual Studio.
  • The other file contains the partial classes that you use to realize the appearance and behavior of the page or form component.

除了分部类之外,还可以创建其他两种分部类型,如下所示:

  • Division structure. (The structure is included in Chapter 10 of . )
  • Partial interface. (Interface is included in Chapter 15 . )

分部分项方法

分部方法是在分部类的不同部分声明的方法。分部方法的不同部分可以在分部类的不同部分中声明,也可以在同一部分中声明。分部方法的两个部分如下:

  • Define partial method declaration
    • Lists the signature and return types.
    • The implementation part of the declaration syntax contains only one semicolon.
  • Implementing partial method declarations
    • Lists the signature and return types.
    • Implementation is in normal format, as you know, it is a statement block.

关于分部方法,需要了解的重要事项如下:

  • The definition and implementation declaration must match in signature and return type. The signature and return type have the following characteristics:
    • The return type must be void.
    • Signature cannot contain access modifier, makes partial method implicitly private .
    • Parameter table cannot contain out parameter.
    • Context partial must be included in the definition and implementation declaration before keyword void.
  • There can be a method to define a division, but no method to implement a division. In this case, the compiler removes the declaration and any calls to the method inside the class. If there is no division method defined, there can be no division method implemented.

下面的代码展示了一个名为PrintSum的分部方法的例子。

  • PrintSum Declared in different parts of partial classes MyClass: the definition statement is in the first part and the implementation statement is in the second part. The implementation prints out the sum of its two integer parameters.
  • Because partial methods are implicitly private, PrintSum cannot be called from outside the class. Method Add is a public method that calls PrintSum.
  • Main Create an object of class MyClass and call public method Add, which calls method PrintSum to print out the sum of input parameters.

`   partial class MyClass
{        Must be void

partial void PrintSum(int x, int y);      // Defining partial method
↑                                ↑
Contextual keyword                   No implementation here
public void Add(int x, int y)
{
PrintSum(x, y);
}
}

partial class MyClass
{
partial void PrintSum(int x, int y)       // Implementing partial method
{
Console.WriteLine("Sum is {0}", x + y);     ←  Implementation
}
}

class Program
{
static void Main( )
{
var mc = new MyClass();
mc.Add(5, 6);
}
}`

该代码产生以下输出:


Sum is 11


七、类和继承

类继承

继承允许你定义一个新的类来合并和扩展一个已经声明的类。

  • You can use an existing class, called base class, as the basis of a new class, called derived class. Members of the derived class of consist of the following:
    • Declare one's membership
    • Member of the base class
  • To declare a derived class, you need to add a base class specification after the class name. The base class specification consists of a colon followed by the class name used as the base class. It is said that derived classes inherit directly from the listed base classes.
  • A derived class is considered to be the base class that extends it, because it includes the members of the base class and any additional functions provided in its own declaration.
  • Derived class cannot delete any members inherited by .

例如,下面显示了名为OtherClass的类的声明,该类是从名为SomeClass的类派生而来的:

`                  Class-base specification


class OtherClass : SomeClass
{            ↑     ↑
...          Colon  Base class
}`

图 7-1 显示了每个类的一个实例。左边的类SomeClass,有一个字段和一个方法。右边的类OtherClass是从SomeClass派生的,包含一个额外的字段和一个额外的方法。

Image

***图 7-1。*基类和派生类

访问继承的成员

对继承成员的访问就像在派生类本身中声明一样。(继承的构造函数有一点不同——我将在本章后面介绍它们。)例如,下面的代码声明了类SomeClassOtherClass,它们显示在图 7-1 中。代码显示,OtherClass的所有四个成员都可以无缝访问,不管它们是在基类还是派生类中声明的。

  • Main Create an object of a derived class OtherClass.
  • The next two lines in Main call Method1 in the base class, use Field1 in the base class, and then use Field2 in the derived class.
  • The next two lines in Main call Method2 in derived from , use Field1 in the base class again, and then use Field2 in the derived class.

`   class SomeClass                          // Base class
{
public string Field1 = "base class field ";
public void Method1( string value ) {
Console.WriteLine("Base class -- Method1:     {0}", value);
}
}

class OtherClass: SomeClass             // Derived class
{
public string Field2 = "derived class field";
public void Method2( string value ) {
Console.WriteLine("Derived class -- Method2:  {0}", value);
}
}

class Program
{
static void Main() {
OtherClass oc = new OtherClass();

oc.Method1( oc.Field1 );       // Base method with base field
oc.Method1( oc.Field2 );       // Base method with derived field
oc.Method2( oc.Field1 );       // Derived method with base field
oc.Method2( oc.Field2 );       // Derived method with derived field
}
}`

该代码产生以下输出:


Base class -- Method1:     base class field Base class -- Method1:     derived class field Derived class -- Method2:  base class field Derived class -- Method2:  derived class field


所有的类都是从类对象派生出来的

除了特殊类object,所有的类都是派生类,即使它们没有一个基于类的规范。类object是唯一没有被派生的类,因为它是继承层次的基础。

没有基类规范的类是直接从类object隐式派生的。省略基类规范只是指定object是基类的简写。两种形式语义等价,如图图 7-2 所示。

Image

***图 7-2。*左边的类声明隐式派生自类对象,而右边的类声明显式派生自对象。这两种形式在语义上是等价的。

关于类派生的其他重要事实如下:

  • A class declares that only one class can be listed in its base class specification. This is called single inheritance .
  • Although a class can only directly inherit a base class, there is no restriction on the derived level . That is to say, the class listed as the base class may be derived from another class, and another class is derived from another class, and so on, until finally reaching object.

基类派生类是相对术语。所有的类都是派生类,要么来自object要么来自另一个类——所以通常当我们称一个类为派生类时,我们的意思是它是直接从除了object之外的某个类派生的。图 7-3 显示了一个简单的类层次结构。在这之后,我不会在图中显示object,因为所有的类最终都是从它派生的。

Image

***图 7-3。*一个阶级等级体系

屏蔽基类的成员

派生类不能删除它继承的任何成员;但是,它可以用同名的成员来屏蔽基类成员。这非常有用,也是继承的主要特征之一。

例如,您可能希望从具有特定方法的基类继承。尽管该方法对于声明它的类来说是完美的,但它可能并不完全符合您在派生类中的要求。在这种情况下,您要做的是用派生类中声明的新成员来屏蔽基类方法。屏蔽派生类中的基类成员的一些重要方面如下:

  • Mask a member that inherits data, and declare a new member with the same type and the same name .
  • To mask an inherited function member, declare a new function member with the same signature. Remember, the signature consists of a name and a list of parameters, but does not include the return type.
  • To let the compiler know that you intentionally blocked an inherited member, use the new modifier. Without it, the program can compile successfully, but the compiler will warn you that an inherited member is hidden.
  • You can also block static members.

下面的代码声明了一个基类和一个派生类,每个基类都有一个名为Field1string成员。关键字new用于明确告诉编译器屏蔽基类成员。图 7-4 展示了每个类的一个实例。

`   class SomeClass                        // Base class
{
public string Field1;
...
}

class OtherClass : SomeClass           // Derived class
{
new public string Field1;           // Mask base member with same name

Keyword` Image

***图 7-4。*屏蔽一个基类的字段

在下面的代码中,OtherClassSomeClass派生,但是隐藏了它的两个继承成员。注意new修改器的使用。图 7-5 说明了代码。

`   class SomeClass                                      // Base class
{
public string Field1 = "SomeClass Field1";
public void   Method1(string value)
{ Console.WriteLine("SomeClass.Method1:  {0}", value); }
}

class OtherClass : SomeClass                        // Derived class
{  Keyword

new public string Field1 = "OtherClass Field1";  // Mask the base member.
new public void   Method1(string value)          // Mask the base member.
↑   { Console.WriteLine("OtherClass.Method1:  {0}", value); }
}  Keyword

class Program
{
static void Main()
{
OtherClass oc = new OtherClass();       // Use the masking member.
oc.Method1(oc.Field1);                  // Use the masking member.
}
}`

该代码产生以下输出:


OtherClass.Method1:  OtherClass Field1


Image

**图 7-5。**隐藏基类的一个字段和一个方法

基地通道

如果你的派生类必须访问一个隐藏的继承成员,你可以通过使用一个基本访问表达式来访问它。该表达式由关键字base组成,后跟一个句点和成员名称,如下所示:

   Console.WriteLine("{0}", <ins>base.Field1</ins>); ↑ ↑         Base access

例如,在下面的代码中,派生类OtherClassField1隐藏在其基类中,但是通过使用基本访问表达式来访问它。

`   class SomeClass {                                       // Base class
public string Field1 = "Field1 -- In the base class";
}

class OtherClass : SomeClass {                          // Derived class

new public string Field1 = "Field1 -- In the derived class";
↑                   ↑
Hides the field in the base class
public void PrintField1()
{
Console.WriteLine(Field1);              // Access the derived class.
Console.WriteLine(base.Field1);         // Access the base class.
}                         ↑          
}                         Base access

class Program {
static void Main()
{
OtherClass oc = new OtherClass();
oc.PrintField1();
}
}`

该代码产生以下输出:


Field1 -- In the derived class Field1 -- In the base class


如果您发现您的程序代码经常使用该功能,即访问隐藏的继承成员,您可能需要重新评估您的类的设计。一般来说,有更优雅的设计——但如果有其他东西都不行的情况,这个功能就在那里。

使用对基类的引用

派生类的实例由基类的实例加上派生类的附加成员组成。对派生类的引用指向整个类对象,包括基类部分。

如果你有一个对派生类对象的引用,你可以通过使用转换操作符将引用转换为基类的类型来得到一个对该对象基类部分的引用。转换操作符放在对象引用的前面,由一组括号组成,括号中包含被转换到的类的名称。铸造在第十六章中有详细介绍。

接下来的几节将介绍如何通过引用对象的基类部分来访问对象。我们将从下面的两行代码开始,它们声明了对对象的引用。图 7-6 说明了代码,并显示了不同变量所看到的对象部分。

  • The first line declares and initializes the variable derived, which contains a reference to an object of type MyDerivedClass.
  • The second line declares a variable MyBaseClass of the base class type, converts the reference in derived into this type, and gives a reference to the base class part of the object.
    • The reference to the base class part is stored in the variable mybc to the left of the assignment operator.
    • The reference to the base class part cannot "see" the rest of the derived class object, because it "sees" it through the reference to the base class.

   MyDerivedClass derived = new MyDerivedClass();      // Create an object. MyBaseClass mybc       = (MyBaseClass) derived;     // Cast the reference. Image

图 7-6 。Reference derived 可以看到整个 MyDerivedClass 对象,而 mybc 只能看到对象的 MyBaseClass 部分。

下面的代码显示了这两个类的声明和使用。图 7-7 说明了内存中的对象和引用。

Main创建一个类型为MyDerivedClass的对象,并将其引用存储在变量derived中。Main还创建了一个MyBaseClass类型的变量,并用它来存储对对象基类部分的引用。当在每个引用上调用Print方法时,调用调用引用可以看到的方法的实现,产生不同的输出字符串。

`   class MyBaseClass
{
public void Print()
{
Console.WriteLine("This is the base class.");
}
}

class MyDerivedClass : MyBaseClass
{
new public void Print()
{
Console.WriteLine("This is the derived class.");
}
}

class Program
{
static void Main()
{
MyDerivedClass derived = new MyDerivedClass();
MyBaseClass mybc = (MyBaseClass)derived;

Cast to base class
derived.Print();           // Call Print from derived portion.
mybc.Print();              // Call Print from base portion.
}
}`

该代码产生以下输出:


This is the derived class. This is the base class.


Image

***图 7-7。*对派生类和基类的引用

虚拟和覆盖方法

在上一节中,您看到了当您通过使用对基类的引用来访问派生类的对象时,您只能获得基类的成员。虚拟方法允许对基类的引用“向上”访问派生类。

如果满足以下条件,您可以使用对基类的引用来调用派生类中的方法:

  • Methods in derived classes and methods in base classes each have the same signature and return type.
  • The method in the base class is labeled virtual.
  • The method in the derived class is labeled override.

例如,以下代码显示了基类和派生类中方法的virtualoverride修饰符:

   class MyBaseClass                                   // Base class { <ins>virtual</ins> public void Print() ↑ ... class MyDerivedClass : MyBaseClass                  // Derived class { <ins>override</ins> public void Print() ↑

图 7-8 说明了这组virtualoverride方法。注意这种行为与前一种情况有什么不同,在前一种情况下,我使用了new来隐藏基类成员。

  • When the Print method is called with a reference to the base class (mybc), the method call is passed up to the derived class and executed because
    • The method in the base class is marked as virtual.
    • There is a matching override method in the derived class.
      illustrates this point by displaying the arrow coming out from behind the virtual Print method and pointing to the override Print method.

Image

***图 7-8。*一个虚拟方法和一个覆盖方法

下面的代码与上一节中的相同,但是这一次,方法被标记为virtualoverride。这会产生一个与前一个示例非常不同的结果。在此版本中,通过基类调用方法会调用派生类中的方法。

`   class MyBaseClass
{
virtual public void Print()
{
Console.WriteLine("This is the base class.");
}
}

class MyDerivedClass : MyBaseClass
{
override public void Print()
{
Console.WriteLine("This is the derived class.");
}
}

class Program
{
static void Main()
{
MyDerivedClass derived = new MyDerivedClass();
MyBaseClass mybc       = (MyBaseClass)derived;

derived.Print();          Cast to base class
mybc.Print();
}
}`

该代码产生以下输出:


This is the derived class. This is the derived class.


关于virtualoverride修改器的其他重要信息如下:

  • Overrides and overridden methods must have the same accessibility. In other words, the covered method cannot be, for example, private and the covered method public.
  • Cannot override static or there is no method declared as virtual.
  • Methods, properties and indexers (which I introduced in the previous chapter), and another member type, called event (which I will introduce later in the text), can be declared as virtual and override.

覆盖标记为 override 的方法

重写方法可以发生在任何级别的继承之间。

  • When you call an overridden method with a reference to the base class part of an object, the method call is passed up to the derivation hierarchy to be executed to the most derived version of the method marked override.
  • If there are other method declarations not marked as override at a higher level of derivation, they will not be called.

例如,下面的代码显示了构成继承层次结构的三个类:MyBaseClassMyDerivedClassSecondDerived。这三个类都包含一个名为Print的方法,具有相同的签名。在MyBaseClass中,Print被标注为virtual。在MyDerivedClass中,标注为override。在类SecondDerived中,你可以用overridenew来声明方法Print。让我们看看在每种情况下会发生什么。

`   class MyBaseClass                                    // Base class
{
virtual public void Print()
{ Console.WriteLine("This is the base class."); }
}

class MyDerivedClass : MyBaseClass                   // Derived class
{
override public void Print()
{ Console.WriteLine("This is the derived class."); }
}

class SecondDerived : MyDerivedClass                 // Most-derived class
{
... // Given in the following pages
}`

案例 1:用覆盖声明打印

如果你将SecondDerivedPrint方法声明为override,那么它将覆盖方法的两个派生较少的版本,如图图 7-9 所示。如果对基类的引用被用来调用Print,它会沿着链一直传递到类SecondDerived中的实现。

下面的代码实现了这种情况。注意方法Main最后两行中的代码。

  • The first of the two statements calls the Print method by using a reference to the highest-level derived class SecondDerived. This is not called by reference to the base class part, so it will call the method implemented in SecondDerived.
  • However, the second statement calls the Print method by referring to the base class MyBaseClass.

`   class SecondDerived : MyDerivedClass
{
override public void Print() {
↑   Console.WriteLine("This is the second derived class.");
}
}

class Program
{
static void Main()
{
SecondDerived derived = new SecondDerived(); // Use SecondDerived.
MyBaseClass mybc = (MyBaseClass)derived;     // Use MyBaseClass.

derived.Print();
mybc.Print();
}
}`

结果是不管Print是通过派生类还是基类调用,最具派生类的方法都被调用。当通过基类调用时,它会沿着继承层次向上传递。该代码产生以下输出:


This is the second derived class. This is the second derived class.


Image

***图 7-9。*执行被传递到多级覆盖链的顶端。

案例 2:用 new 声明打印

如果改为将SecondDerivedPrint方法声明为new,结果如图图 7-10 所示。Main与前一种情况相同。

`   class SecondDerived : MyDerivedClass
{
new public void Print()
{
Console.WriteLine("This is the second derived class.");
}
}

class Program
{
static void Main()                                    // Main
{
SecondDerived derived = new SecondDerived();       // Use SecondDerived.
MyBaseClass mybc      = (MyBaseClass)derived;      // Use MyBaseClass.

derived.Print();
mybc.Print();
}
}`

结果是,当通过对SecondDerived的引用调用方法Print时,SecondDerived中的方法被执行,正如您所料。然而,当通过对MyBaseClass的引用调用该方法时,该方法调用只向上传递一级,到达类MyDerived,在那里执行。这两种情况的唯一区别是SecondDerived中的方法是用修饰符override还是修饰符new声明的。

该代码产生以下输出:


This is the second derived class. This is the derived class.


Image

***图 7-10。*隐藏被覆盖的方法

覆盖其他成员类型

在前面的几节中,您已经看到了virtual / override名称是如何在方法上工作的。这些与属性、事件和索引器的工作方式完全相同。例如,下面的代码使用virtual / override显示了一个名为MyProperty的只读属性。

`   class MyBaseClass
{
private int _myInt = 5;
virtual public int MyProperty
{
get { return _myInt; }
}
}

class MyDerivedClass : MyBaseClass
{
private int _myInt = 10;
override public int MyProperty
{
get { return _myInt; }
}
}

class Program
{
static void Main()
{
MyDerivedClass derived = new MyDerivedClass();
MyBaseClass mybc       = (MyBaseClass)derived;

Console.WriteLine( derived.MyProperty );
Console.WriteLine( mybc.MyProperty );
}
}`

该代码产生以下输出:


10 10


构造函数执行

在前一章中,你看到了一个构造函数执行代码来准备一个类供使用。这包括初始化类的静态和实例成员。在这一章中,你看到了派生类对象的一部分是基类的一个对象。

  • In order to create the base class part of the object, the constructor of the base class is implicitly called as part of the instance creation process.
  • Each class in the inheritance hierarchy chain executes its own base class constructor before executing its own constructor body.

例如,下面的代码显示了类MyDerivedClass及其构造函数的声明。当调用构造函数时,它在执行自己的主体之前调用无参数构造函数MyBaseClass()

   class MyDerivedClass : MyBaseClass { MyDerivedClass()        // Constructor uses base constructor MyBaseClass() { ... }

图 7-11 显示了施工顺序。创建实例时,首先要做的事情之一是初始化对象的所有实例成员。之后,基类构造函数被调用。只有这样,类本身的构造函数体才会被执行。

Image

***图 7-11。*宾语结构的顺序

例如,在下面的代码中,在基类构造函数被调用之前,MyField1MyField2的值将被分别设置为50

`   class MyDerivedClass : MyBaseClass
{
int MyField1 = 5;                      // 1. Member initialized
int MyField2;                          //    Member initialized

public MyDerivedClass()                // 3. Body of constructor executed

}

class MyBaseClass
{
public MyBaseClass()                   // 2. Base class constructor called

}`

Image 小心在构造函数中调用虚方法强烈不鼓励。当执行基类构造函数时,基类中的虚方法将调用派生类中的重写方法。但那是在派生构造函数的主体被执行之前。因此,它会在类完全初始化之前向上调用派生类。

构造函数初始值设定项

默认情况下,构造对象时会调用基类的无参数构造函数。但是构造函数可以重载,所以一个基类可能不止一个。如果你想让你的派生类使用一个特定的基类构造函数而不是无参数构造函数,你必须在一个构造函数初始化器中指定它。

有两种形式的构造函数初始值设定项:

  • The first form uses the keyword base and specifies which base class constructor to use.
  • The second form uses the keyword this and specifies which constructor in the class should be used.

基类构造函数初始值设定项放在类的构造函数声明中参数列表后面的冒号后面。构造函数初始化器由关键字base和要调用的基构造函数的参数列表组成。

例如,下面的代码显示了类MyDerivedClass的构造函数。

  • The constructor initializer specifies that the construction process calls the base class constructor with two parameters, where the first parameter is a string and the second parameter is a int.
  • The parameters in the basic parameter table must match the parameter table of expected basic constructor in type and order.

Constructor initializer <ins>     ↓     </ins> public MyDerivedClass( int x, string s ) : <ins>base</ins>( s, x ) {                                           ↑ ...                                      Keyword

当你在没有构造函数初始化器的情况下声明一个构造函数时,这是一个带有由base()组成的构造函数初始化器的表单的快捷方式,如图图 7-12 所示。这两种形式在语义上是等价的。

Image

***图 7-12。*建造师的等价形式

另一种形式的构造函数初始化器指示构造过程(实际上是编译器)使用来自同一个类的不同构造函数。例如,下面显示了类MyClass的单参数构造函数。但是,这个单参数构造函数使用了来自同一个类的构造函数,但是有两个参数,提供一个默认参数作为第二个参数。

                                   Constructor initializer <ins>               ↓              </ins> public MyClass(int x): <ins>this</ins>(x, "Using Default String") {                       ↑ ...               Keyword }

另一种特别方便的情况是,一个类有几个构造函数,它们有公共代码,应该总是在对象构造过程的开始执行。在这种情况下,您可以提取公共代码,并将其放在一个构造函数中,该构造函数被所有其他构造函数用作构造函数初始值设定项。事实上,这是一个建议的实践,因为它减少了代码重复。

你可能认为你可以声明另一个方法来执行这些普通的初始化,并让所有的构造函数调用这个方法。这不是很好,有几个原因。首先,当编译器知道一个方法是构造函数时,它可以优化某些东西。第二,有些事情只能在构造函数中完成,而不能在其他地方完成。例如,在前一章中,你了解到readonly字段只能在构造函数中初始化。如果您试图在任何其他方法中初始化一个readonly字段,您将得到一个编译器错误,即使该方法仅由一个构造函数调用。

回到那个公共构造函数——如果它可以独立作为一个有效的构造函数,初始化需要初始化的类中的所有东西,那么让它作为一个public构造函数是非常好的。

然而,如果它没有完全初始化一个对象呢?在这种情况下,不允许从类外部调用构造函数,因为这样会创建未完全初始化的对象。为了避免这个问题,您可以声明构造函数private而不是public,并且只让其他构造函数使用它。以下代码说明了这种用法:

`   class MyClass
{
readonly int    firstVar;
readonly double secondVar;

public string UserName;
public int UserIdNumber;

private MyClass( )            // Private constructor performs initializations
{                             // common to the other constructors
firstVar  = 20;
secondVar = 30.5;
}

public MyClass( string firstName ) : this() // Use constructor initializer

public MyClass( int idNumber ) : this( )    // Use constructor initializer
{
UserName     = "Anonymous";
UserIdNumber = idNumber;
}
}`

类访问修饰符

一个类可以被系统中的其他类看到和访问。本节解释了类的可访问性。虽然我将在解释和例子中使用类,因为这是我到目前为止在本文中所涉及的,但可访问性规则也适用于我稍后将涉及的其他类型。

术语可见有时也用于术语可达。它们可以互换使用。类的可访问性有两个级别:publicinternal

  • The class marked public can be accessed by the code in any assembly in the system. To make a class visible to other assemblies, use the public access modifier, as shown below:   Keyword ↓ public class MyBaseClass { ...
  • A class marked internal can only be seen by classes in its own assembly. (Remember in Chapter 1 that a assembly is either a program or a DLL. I will introduce the components in detail in Chapter 21 . )     Keyword ↓ internal class MyBaseClass { ...
    • This is the default accessibility level, so code outside the assembly cannot access the class unless the modifier public is explicitly specified in the class declaration. You can explicitly declare an internal class by using the internal access modifier.

图 7-13 说明了从组件外部对internalpublic类的访问。类MyClass对于左边组件中的类是不可见的,因为MyClass被标记为internal。然而,类OtherClass对于左边的类是可见的,因为它被标记为public

Image

***图 7-13。*其他程序集中的类可以访问公共类,但不能访问内部类。

程序集之间的继承

到目前为止,我一直在包含基类的同一个程序集中声明派生类。但是 C# 也允许你从不同程序集中定义的基类派生一个类。

若要使您的类从另一个程序集中的基类派生,必须满足以下条件:

  • The base class must be declared public so that it can be accessed from outside its assembly.
  • You must include a reference to the assembly that contains the base class in the References section of the Visual Studio project. You can find the title in Solution Explorer.

为了在不使用完全限定名的情况下更容易引用另一个程序集中的类和类型,请在源文件的顶部放置一个using指令,其命名空间包含您想要访问的类或类型。

Image 注意添加对另一个程序集的引用和添加一个using指令是两件不同的事情。添加对另一个程序集的引用会告诉编译器所需类型的定义位置。添加using指令允许您引用其他类,而不必使用它们的完全限定名。第二十一章对此有详细介绍。

例如,下面两段来自不同程序集中的代码显示了从另一个程序集中继承一个类是多么容易。第一个代码清单创建了一个程序集,它包含一个名为MyBaseClass的类的声明,该类具有以下特征:

  • It is declared in a source file named Assembly1.cs and a namespace named BaseClassNS.
  • It is declared as public so that it can be accessed from other assemblies.
  • It contains a single member, a method named PrintMe, and it just writes a simple message to identify the class.

   // Source file name Assembly1.cs using System; Namespace containing declaration of base class ↓ namespace BaseClassNS { Declare the class public so it can be seen outside the assembly. ↓ public class MyBaseClass { public void PrintMe() { Console.WriteLine("I am MyBaseClass"); } } }

第二个程序集包含一个名为DerivedClass的类的声明,该类继承自第一个程序集中声明的MyBaseClass。源文件名为Assembly2.cs。图 7-14 展示了两个组件。

  • DerivedClass has an empty body, but it inherits the method PrintMe from MyBaseClass.
  • Main Create an object of type DerivedClass and call its inheritance method PrintMe.

`   // Source file name Assembly2.cs
using System;
using BaseClassNS;

Namespace containing declaration of base class
namespace UsesBaseClass
{                  Base class in other assembly

class DerivedClass: MyBaseClass {
// Empty body
}

class Program {
static void Main( )
{
DerivedClass mdc = new DerivedClass();
mdc.PrintMe();
}
}
}`

该代码产生以下输出:


I am MyBaseClass


Image

***图 7-14。*跨程序集继承

成员访问修饰符

前两节解释了类的可访问性。对于类可访问性,只有两个修饰符— internalpublic。本节涵盖了成员可访问性。类可访问性描述了类的可见性;成员可访问性描述了类对象成员的可见性。

类中声明的每个成员对系统的各个部分都是可见的,这取决于在类声明中分配给它的访问修饰符。您已经看到了private成员仅对同一类的其他成员可见,而public成员对程序集之外的类也是可见的。在本节中,我们将再次查看publicprivate访问级别,以及其他三个可访问级别。

在研究成员可访问性的细节之前,我需要先提一些一般性的事情:

在一个类的声明中显式声明的所有成员对彼此都是可见的,不管它们的可访问性规范如何。继承的成员没有在类的声明中显式声明,所以,正如你将看到的,继承的成员对派生类的成员可能是可见的,也可能是不可见的。* The following are the names of the five member access levels. So far, I have only introduced public and private.

*   `public`
*   `private`
*   `protected`
*   `internal`
*   `protected internal`*   You must specify a member access level for each member. If no access level is specified for the member, its implicit access level is `private`.*   A member cannot be more accessible than its class. That is to say, if the accessibility level of a class limits it to the assembly, the individual members of the class can't be seen outside the assembly, no matter what their access modifiers are, even `public`.

访问成员的区域

一个类通过用访问修饰符标记它的成员来指定它的哪些成员可以被其他类访问。你已经看到了publicprivate修改器。下面的声明显示了一个类,该类声明了具有五种访问级别的成员:

   public class MyClass { public             int Member1; private            int Member2; protected          int Member3; internal           int Member4; protected internal int Member5; ...

另一个类——比方说 classB——可以或不可以根据它的两个特征访问这些成员,这两个特征是:

  • Is class b from MyClass
  • Is the derived class B the same as MyClass

类在同一个程序集中

这两个特征产生了四个组,如图 7-15 所示。与类别MyClass相关,另一个类别可以是以下任一类别:

  • In the same assembly, from MyClass in the same assembly (bottom right)
  • Derived from, but not from MyClass in different assemblies (lower left)
  • Derived from MyClass in different assemblies (upper right)
  • Derived from, not from MyClass (top left)

派生

这些特征用于定义五个访问级别,我将在下一节中介绍。

Image

***图 7-15。*无障碍区域

公共成员可访问性

public访问级别限制最少。程序集内外的所有类都可以自由访问该成员。图 7-16 展示了MyClasspublic类成员的可访问性。

要声明一个公共成员,使用public访问修饰符,如下所示。

`   Keyword

public int Member1;` Image

***图 7-16。*公共类的公共成员对同一程序集和其他程序集中的所有类都是可见的。

私人会员可访问性

private访问级别是最严格的。

  • private Class members can only be accessed by members of their own class. It cannot be accessed by other classes, including classes derived from it.
  • However, members of private can be accessed by members of classes nested in their classes. Nested classes are contained in in Chapter 25.

图 7-17 展示了一个private成员的可访问性。

Image

***图 7-17。*任何类的私有成员仅对它自己的类(或嵌套类)的成员可见。

受保护成员的可访问性

protected访问级别类似于private访问级别,除了它也允许从类派生的类访问成员。图 7-18 说明了protected的可达性。请注意,即使程序集之外从类派生的类也可以访问该成员。

Image

***图 7-18。*公共类的受保护成员对它自己的类和从它派生的类的成员是可见的。派生类甚至可以位于其他程序集中。

内部成员可访问性

标记为internal的成员对集合中的所有类可见,但对集合外的类不可见,如图 7-19 中的所示。

Image

***图 7-19。*公共类的内部成员对同一程序集内的任何类的成员都是可见的,但对程序集外的类是不可见的。

受保护的内部成员可访问性

标记为protected internal的成员对从该类继承的所有类可见,也对程序集内的所有类可见,如图 7-20 中的所示。注意,允许访问的类集合是由protected修饰符允许的类集合加上由internal修饰符允许的类集合的组合。注意,这是protectedinternal联合——而不是交集。

Image

***图 7-20。*公共类的受保护内部成员对同一程序集中的类成员以及从该类派生的类成员可见。对于不是从该类派生的其他程序集中的类,它是不可见的。

成员访问修饰符概要

下面两个表总结了五个成员访问级别的特征。表 7-1 列出了每个修改器,并给出了其效果的直观总结。

Image

图 7-21 显示了五个成员访问修饰符的相对可访问性。

Image

***图 7-21。*各种成员访问修饰符的相对可访问性

表 7-2 在表的左侧列出了访问修饰符,在顶部列出了类的类别。派生类是指从声明成员的类派生的类。非派生的意味着不是从声明成员的类派生的类。单元格中的复选标记意味着类的类别可以访问带有相应修饰符的成员。

Image

抽象成员

抽象成员是被设计为被覆盖的函数成员。抽象成员具有以下特征:

  • It must be a function member. That is, fields and constants cannot be abstract members.
  • Must be marked with abstract modifier.
  • There is no code block that can be implemented. The code of an abstract member is represented by a semicolon.

例如,类定义中的以下代码声明了两个抽象成员:一个名为PrintStuff的抽象方法和一个名为MyProperty的抽象属性。注意分号代替了实现块。

`    Keyword                                            Semicolon in place of implementation
↓                                    ↓
abstract public void PrintStuff(string s);

abstract public int MyProperty
{
get;  ←  Semicolon in place of implementation
set;  ←  Semicolon in place of implementation
}`

抽象成员只能在抽象类中声明,我们将在下一节中讨论。可以将四种类型的成员声明为抽象成员:

  • way
  • attribute
  • event
  • Indexer

关于抽象成员的其他重要事实如下:

  • Abstract members, although they must be covered by the corresponding members in the derived class, cannot use virtual modifier in addition to abstract modifier .
  • Like virtual members, the implementation of abstract members in derived classes must specify the override modifier.

表 7-3 比较对比虚拟成员和抽象成员。

Image

抽象类

抽象类被设计为从。一个抽象类只能作为另一个类的基类。

  • You can't create an instance of an abstract class.
  • Declare an abstract class with the abstract modifier.

    Keyword ↓ abstract class MyClass { ... }

  • An abstract class can contain abstract members or regular non-abstract members. Members of abstract classes can be any combination of abstract members and ordinary members and implementations.
  • An abstract class itself can be derived from another abstract class. For example, the following code shows an abstract class derived from another abstract class:

`   abstract class AbClass                    // Abstract class

abstract class MyAbClass : AbClass        // Abstract class derived from
{                                         // an abstract class
...
}`

  • Any class derived from an abstract class must use the override keyword to implement all abstract members of the class, unless the derived class itself is abstract.

抽象类和抽象方法的例子

下面的代码展示了一个名为AbClass的抽象类,它有两个方法。

第一个方法是一个普通的方法,它有一个输出类名的实现。第二种方法是必须在派生类中实现的抽象方法。类DerivedClass继承自AbClass并实现和覆盖抽象方法。Main创建一个DerivedClass的对象并调用它的两个方法。

`    Keyword

abstract class AbClass                                  // Abstract class
{
public void IdentifyBase()                           // Normal method
{ Console.WriteLine("I am AbClass"); }
Keyword

abstract public void IdentifyDerived();              // Abstract method
}

class DerivedClass : AbClass                            // Derived class
{   Keyword

override public void IdentifyDerived()               // Implementation of
{ Console.WriteLine("I am DerivedClass"); }          // abstract method
}

class Program
{
static void Main()
{
// AbClass a = new AbClass();        // Error.  Cannot instantiate
// a.IdentifyDerived();              // an abstract class.

DerivedClass b = new DerivedClass(); // Instantiate the derived class.
b.IdentifyBase();                    // Call the inherited method.
b.IdentifyDerived();                 // Call the "abstract" method.
}
}`

该代码产生以下输出:


I am AbClass I am DerivedClass


抽象类的另一个例子

下面的代码显示了包含数据成员和函数成员的抽象类的声明。记住,数据成员——字段和常量——不能声明为abstract

`   abstract class MyBase     // Combination of abstract and nonabstract members
{
public int SideLength        = 10;             // Data member
const  int TriangleSideCount = 3;              // Data member

abstract public void PrintStuff( string s );   // Abstract method
abstract public int  MyInt { get; set; }       // Abstract property

public int PerimeterLength( )                  // Regular, nonabstract method
{ return TriangleSideCount * SideLength; }
}

class MyClass : MyBase
{
public override void PrintStuff( string s )    // Override abstract method
{ Console.WriteLine( s ); }

private int _myInt;
public override int MyInt                      // Override abstract property
{
get { return _myInt; }
set
}
}

class Program
{
static void Main( string[] args )
{
MyClass mc = new MyClass( );
mc.PrintStuff( "This is a string." );
mc.MyInt = 28;
Console.WriteLine( mc.MyInt );
Console.WriteLine( "Perimeter Length: {0}", mc.PerimeterLength( ) );
}
}`

该代码产生以下输出:


This is a string. 28 Perimeter Length: 30


密封类

在上一节中,您看到了抽象类必须用作基类——它不能被实例化为独立的类对象。一个密封类的情况正好相反。

  • Sealed classes can only be instantiated as independent class objects, not as base classes.
  • A sealed class is marked with the sealed modifier.

例如,下面的类是一个密封类。任何将它作为另一个类的基类的尝试都会产生编译错误。

   Keyword ↓ sealed class MyClass { ... }

静态类

静态类是所有成员都是静态的类。静态类用于对不受实例数据影响的数据和函数进行分组。静态类的一个常见用途可能是创建一个包含数学方法和值的数学库。

关于静态类,需要知道的重要事情如下:

  • The class itself must be marked with static.
  • All members of the class must be static.
  • A class can have a static constructor, but it cannot have an instance constructor, because an instance of a class cannot be created.
  • Static classes are implicitly sealed. That is, you cannot inherit from a static class.

通过使用类名和成员名,可以像访问任何静态成员一样访问静态类的成员。

下面的代码显示了一个静态类的示例:

`   Class must be marked static

static public class MyMath
{
public static float PI = 3.14f;
public static bool IsOdd(int x)
↑      { return x % 2 == 1; }
Members must be static
↓     
public static int Times2(int x)
{ return 2 * x; }
}

class Program
{
static void Main( )
{                                           Use class name and member name.
int val = 3;                                        ↓     
Console.WriteLine("{0} is odd is {1}.", val,  MyMath.IsOdd(val));
Console.WriteLine("{0} * 2 = {1}.",     val,  MyMath.Times2(val));
}
}`

该代码产生以下输出:


3 is odd is True. 3 * 2 = 6.


扩展方法

到目前为止,在本文中,你看到的每一个方法都与声明它的类相关联。扩展方法特性扩展了这个界限,允许你编写与类相关的方法,而不是声明它们的类。

要了解如何使用这个特性,请看下面的代码。它包含类MyData,存储三个类型为double的值,还包含一个构造函数和一个名为Sum的方法,返回三个存储值的总和。

`   class MyData
{
private double D1;                                     // Fields
private double D2;
private double D3;

public MyData(double d1, double d2, double d3)         // Constructor

public double Sum()                                    // Method Sum
{
return D1 + D2 + D3;
}
}`

这是一个非常有限的类,但是假设它包含另一个方法,返回三个数据点的平均值,那么它会更有用。根据您目前对类的了解,有几种方法可以实现附加功能:

  • If you have source code and can modify this class, of course, you can also add new methods to this class.
  • However, if you can't modify this class-for example, if it is in a third-party class library-then, as long as it is not sealed, you can treat it as a base class and implement additional methods in classes derived from it.

但是,如果您没有访问代码的权限,或者该类是密封的,或者有一些其他设计原因阻止了这些解决方案的工作,那么您将不得不在另一个类中编写一个方法,该方法使用该类的公共可用成员。

例如,您可以编写一个类似下面代码中的类。代码包含一个名为ExtendMyData的静态类,该类包含一个名为Average的静态方法,该方法实现了附加功能。注意,该方法将MyData的一个实例作为参数。

`   static class ExtendMyData         Instance of MyData class
{                                    ↓    
public static double Average( MyData md )
{
return md.Sum() / 3;
}         ↑
}   Use the instance of MyData.

class Program
{
static void Main()
{                                                   Instance of MyData
MyData md = new MyData(3, 4, 5);                        ↓
Console.WriteLine("Average: {0}", ExtendMyData.Average(md));
}                                                 ↑  
}                                             Call the static method.`

该代码产生以下输出:


Average: 4


虽然这是一个完美的解决方案,但是如果您可以在类实例本身上调用该方法,而不是创建另一个类的实例来操作它,那将会更好。下面两行代码说明了不同之处。第一个使用刚刚展示的方法——在另一个类的实例上调用静态方法。第二个展示了我们想要使用的形式——调用对象本身的实例方法。

   ExtendMyData.Average( md )               // Static invocation form md.Average();                            // Instance invocation form

扩展方法允许您使用第二种形式,即使第一种形式是编写调用的正常方式。

通过对方法Average的声明做一点小小的修改,您可以使用实例调用的形式。您需要做的更改是在参数声明中的类型名称前添加关键字this,如下所示。将this关键字添加到静态类的静态方法的第一个参数会将其从类ExtendMyData的常规方法更改为类MyData扩展方法。现在,您可以使用这两种调用形式。

  Must be a static class ↓     static class ExtendMyData {   Must be public and static                           Keyword and type <ins>    ↓     </ins>                   <ins>    ↓     </ins>  public static double Average( this MyData md ) { ... } }

扩展方法的重要要求如下:

  • Classes that declare extension methods must declare static.
  • The extension method itself must declare static.
  • The extension method must contain the keyword this as its first parameter type, followed by the class name it is extending.

图 7-22 展示了一个扩展方法的结构。

Image

***图 7-22。*一个扩展方法的结构

下面的代码展示了一个完整的程序,包括类MyData和扩展方法Average,在类ExtendMyData中声明。注意方法Average被调用,就好像它是MyData实例成员!图 7-22 说明了代码。类MyDataExtendMyData一起像期望的类一样工作,有三个方法。

`   namespace ExtensionMethods
{
sealed class MyData
{
private double D1, D2, D3;
public MyData(double d1, double d2, double d3)

public double Sum() { return D1 + D2 + D3; }
}

static class ExtendMyData       Keyword and type
{                                    ↓     
public static double Average(this MyData md)
{        ↑
Declared static
return md.Sum() / 3;
}
}

class Program
{
static void Main()
{
MyData md = new MyData(3, 4, 5);
Console.WriteLine("Sum:     {0}", md.Sum());
Console.WriteLine("Average: {0}", md.Average());
}                                          ↑
}                                 Invoke as an instance member of the class
}`

该代码产生以下输出:


Sum:     12 Average: 4


命名约定

写程序需要想出很多名字;类、变量、方法、属性的名字,还有很多我还没有提到的东西。当你通读一个程序时,使用命名约定是一个重要的方法,可以给你一个关于你正在处理的对象种类的线索。

我在第六章中提到了一点命名,但是现在你已经知道了更多关于类的知识,我可以给你更多的细节。表 7-4 给出了三种主要的命名方式以及它们在中的常用方式 .NET 程序。

Image

不是每个人都同意这些约定,尤其是前导下划线部分。我自己发现前导下划线非常难看,但很有用,并在我自己的代码中将其用于私有和受保护的变量。微软自己在这个问题上似乎也有矛盾。在其建议的约定中,Microsoft 不将前导下划线作为选项。但是他们在自己的代码中使用了它。

在本书的其余部分,我将坚持微软官方推荐的对私有和受保护字段使用 camel 套管的惯例。

关于下划线的最后一点是,它们通常不在标识符的主体中使用,除了在事件处理程序的名字中,我将在第十四章中介绍。

八、表达式和运算符

表情

本章定义表达式并描述 C# 提供的运算符。它还解释了如何定义 C# 运算符来处理用户定义的类。

一个操作符是一个符号,代表一个返回单一结果的操作。操作数是操作者用作输入的数据元素。操作员执行以下操作:

  • Take its operand as input.
  • Perform an action
  • According to the action

返回值

一个表达式是一串操作符和操作数。C# 运算符接受一个、两个或三个操作数。以下是一些可以作为操作数的构造:

  • characters
  • constant
  • variable
  • Method call
  • Element accessors, such as array accessors and indexers
  • Other expressions

可以使用运算符组合表达式,以创建更复杂的表达式,如以下包含三个运算符和四个操作数的表达式所示:

Image

求值表达式是将每个运算符以正确的顺序应用于其操作数以产生一个值的过程。

  • Returns the value to the position where the expression is evaluated. There, it may be used as an operand in a closed expression again.
  • Besides the return value, some expressions also have side effects, such as setting a value in memory.

文字量

文字是键入源代码中的数字或字符串,表示特定类型的特定集合值。

例如,下面的代码显示了六种类型的文字。例如,请注意double文字和float文字之间的差异。

   static void Main()         Literals { ↓ Console.WriteLine("{0}", 1024);            // int literal Console.WriteLine("{0}", 3.1416);          // double literal Console.WriteLine("{0}", 3.1416F);         // float literal Console.WriteLine("{0}", true);            // boolean literal Console.WriteLine("{0}", 'x');             // character literal Console.WriteLine("{0}", "Hi there");      // string literal }

这段代码的输出如下:


1024 3.1416 3.1416 True x Hi there


因为文字被写入源代码,所以它们的值在编译时必须是已知的。几个预定义类型有自己的文字形式:

  • Type bool has two characters: true and false.
  • For reference type variables, the word null indicates that the variable does not point to data in memory.

整数文字量

整数文字是最常用的文字。它们被写成十进制数字序列,具有以下特征:

  • No decimal point
  • An optional suffix to specify the type of integer.

例如,下面几行显示了整数 236 的四个文本。每个都被编译器解释为不同类型的整数,这取决于它的后缀。

   236               // int 236L              // long 236U              // unsigned int 236UL             // unsigned long

整数类型的文字也可以写成十六进制(hex)形式。数字必须是十六进制数字(0 到 F),字符串必须以0x0X开头(数字 0 ,字母 x )。

图 8-1 显示了整数文字格式的形式。名称在方括号中的组件是可选的。

Image

***图 8-1。*整数文字格式

表 8-1 列出了整数文字后缀。对于给定的后缀,编译器会将数字字符串解释为四种列出的整数类型中最小的一种,这四种整数类型可以在不丢失数据的情况下表示该值。

例如,以文字2365000000000为例,它们都没有后缀。由于236可以用 32 位来表示,所以它将被编译器解释为int。然而,第二个数字不适合 32 位,所以编译器将它表示为一个long

Image

实字面值

实数的文字由以下内容组成:

  • decimal digits
  • Optional decimal point
  • Optional exponential part
  • Optional suffix

例如,以下代码显示了实数类型的各种文本格式:

   float  f1 = 236F; double d1 = 236.714; double d2 = .35192; double d3 = 6.338e-26;

图 8-2 显示了真实文字的有效格式。名称在方括号中的组件是可选的。表 8-2 显示了真正的后缀及其含义。

Image

***图 8-2。*真正的文字格式

Image

Image 注意不带后缀的真实文字是double类型,不是float

字符字面量

字符文字由两个单引号之间的字符表示组成。字符表示可以是以下任何一种:单个字符、简单转义序列、十六进制转义序列或 Unicode 转义序列。

  • The type of a character is char.
  • A simple escape sequence is a backslash followed by a single character.
  • The hexadecimal escape sequence is a backslash followed by an uppercase or lowercase x , followed by up to four hexadecimal digits.
  • Unicode escape sequence is a backslash followed by an uppercase or lowercase u , followed by up to four hexadecimal digits.

例如,以下代码显示了各种格式的字符文本:

   char c1 = 'd';                       // Single character char c2 = '\n';                      // Simple escape sequence char c3 = '\x0061';                  // Hex escape sequence char c4 = '\u005a';                  // Unicode escape sequence

表 8-3 显示了一些重要的特殊字符及其编码。

Image

字符串文字

字符串文字使用双引号,而不是字符文字中使用的单引号。有两种类型的字符串文字:

  • General string literal
  • Word for word character string

常规字符串由一组双引号之间的字符序列组成。常规字符串文字可以包括以下内容:

  • character
  • Simple escape sequence
  • Hexadecimal and Unicode escape sequences

这里有一个例子:

   string st1 = "Hi there!"; string st2 = "Val1\t5, Val2\t10"; string st3 = "Add\x000ASome\u0007Interest";

逐字字符串文字的书写方式类似于常规字符串文字,但以@字符开头。逐字字符串的重要特征如下:

  • Word-for-word literals differ from regular string literals in that the escape sequence is not calculated. Everything between double quotation marks (including what is usually considered as an escape sequence) is printed out as listed in the string.
  • The only exception to word for word is multiple sets of consecutive double quotes, which are interpreted as a double quote character.

例如,下面的代码比较了一些常规字符串和逐字字符串:

`   string rst1 = "Hi there!";
string vst1 = @"Hi there!";

string rst2 = "It started, "Four score and seven..."";
string vst2 = @"It started, Four score and seven...";

string rst3 = "Value 1 \t 5, Val2 \t 10";    // Interprets tab esc sequence
string vst3 = @"Value 1 \t 5, Val2 \t 10";   // Does not interpret tab

string rst4 = "C:\Program Files\Microsoft\";
string vst4 = @"C:\Program Files\Microsoft";

string rst5 = " Print \x000A Multiple \u000A Lines";
string vst5 = @" Print
Multiple
Lines";`

打印这些字符串会产生以下输出:


`Hi there!
Hi there!

It started, "Four score and seven..."
It started, "Four score and seven..."

Value 1          5, Val2         10
Value 1 \t 5, Val2 \t 10

C:\Program Files\Microsoft
C:\Program Files\Microsoft\

Print
Multiple
Lines

Print
Multiple
Lines`


Image 注意编译器通过让相同的字符串在堆中共享相同的内存位置来节省内存。

评估的顺序

一个表达式可以由许多嵌套的子表达式组成。子表达式的求值顺序会对表达式的最终值产生影响。

例如,给定表达式 3 * 5 + 2,根据子表达式的求值顺序,有两种可能的结果,如图 8-3 所示。

  • If you multiply first, the result is 17.
  • If you add 5 and 2 together first, the result is 21.

Image

***图 8-3。*简单的评估顺序

优先顺序

您从小学时代就知道,在前面的例子中,乘法必须在加法之前执行,因为乘法的优先级高于加法。但是与小学时代不同,那时你有四个操作符和两个优先级,C# 的情况稍微复杂一些,它有超过 45 个操作符和 14 个优先级。

表 8-4 显示了操作符的完整列表以及每个操作符的优先级。该表在顶部列出了优先级最高的运算符,并在底部列出了优先级最低的运算符。

Image

关联性

当编译器计算所有运算符都具有不同优先级的表达式时,将计算每个子表达式,从具有最高优先级的子表达式开始,向下计算优先级。

但是如果两个顺序操作符具有相同的优先级呢?例如,给定表达式 2 / 6 * 4,有两种可能的评估序列:

(2 / 6) * 4 = 4/3

或者

2 / (6 * 4) = 1/12

当顺序运算符具有相同的优先级时,求值顺序由*运算符结合性决定。*也就是说,给定两个优先级相同的操作符,根据操作符的结合律,一个或另一个将具有优先级。运算符结合性的一些重要特征如下,并在表 8-5 中进行了总结:

  • The left union operator is evaluated from left to right.
  • The right association operator evaluates from right to left.
  • Binary operators, except assignment operators, are left associative.
  • Assignment operators and conditional operators are right associative.

因此,根据这些规则,前面的示例表达式应该从左到右分组,得到(2 / 6) * 4,即 4/3。

通过使用括号,可以显式设置表达式子表达式的求值顺序。带括号的子表达式

  • Override priority and binding rules
  • From the innermost nested set to the outermost set

的顺序进行评估

简单的算术运算符

简单算术运算符执行四种基本算术运算,列于表 8-6 。这些运算符是二元的和左结合的。

Image

算术运算符对所有预定义的简单算术类型执行标准算术运算。

以下是简单算术运算符的示例:

`   int x1 = 5 + 6;          double d1 = 5.0 + 6.0;
int x2 = 12 - 3;         double d2 = 12.0 - 3.0;
int x3 = 3 * 4;          double d3 = 3.0 * 4.0;
int x4 = 10 / 3;         double d4 = 10.0 / 3.0;

byte b1 = 5 + 6;
sbyte sb1 = 6 * 5;`

余数运算符

余数运算符(%)将第一个操作数除以第二个操作数,忽略商,并返回余数。表 8-7 给出了它的描述。

余数运算符是二元的和左结合的。

Image

以下几行显示了整数余数运算符的示例:

  • 0 % 3 = 0, because 0 divided by 3 is 0 with a remainder of 0.
  • 1 % 3 = 1, because 1 divided by 3 is 0 and the remainder is 1.
  • 2 % 3 = 2, because 2 divided by 3 is 0 and the remainder is 2.

%

  • 4 % 3 = 1, because 4 divided by 3 equals 1 and the remainder is 1.

余数运算符也可用于实数,给出的实数余数

   Console.WriteLine("0.0f % 1.5f is {0}" , 0.0f % 1.5f); Console.WriteLine("0.5f % 1.5f is {0}" , 0.5f % 1.5f); Console.WriteLine("1.0f % 1.5f is {0}" , 1.0f % 1.5f); Console.WriteLine("1.5f % 1.5f is {0}" , 1.5f % 1.5f); Console.WriteLine("2.0f % 1.5f is {0}" , 2.0f % 1.5f); Console.WriteLine("2.5f % 1.5f is {0}" , 2.5f % 1.5f);

该代码产生以下输出:


0.0f % 1.5f is 0               // 0.0 / 1.5 = 0 remainder 0 0.5f % 1.5f is 0.5             // 0.5 / 1.5 = 0 remainder .5 1.0f % 1.5f is 1               // 1.0 / 1.5 = 0 remainder 1 1.5f % 1.5f is 0               // 1.5 / 1.5 = 1 remainder 0 2.0f % 1.5f is 0.5             // 2.0 / 1.5 = 1 remainder .5 2.5f % 1.5f is 1               // 2.5 / 1.5 = 1 remainder 1


关系和相等比较运算符

关系和相等比较操作符是二元操作符,它们比较它们的操作数并返回一个类型为bool的值。表 8-8 列出了这些操作员。

关系运算符和等式运算符是二元的和左关联的。

Image

带有关系或相等运算符的二元表达式返回一个类型为bool.的值

Image 注意与 C 和 C++不同,C# 中的数字没有布尔解释。

   int x = 5; if( x )           // Wrong.  x is of type int, not type boolean. ... if( x == 5 )      // Fine, since expression returns a value of type boolean ...

打印时,布尔值truefalse由字符串输出值TrueFalse表示。

   int x = 5, y = 4; Console.WriteLine("x == x is {0}" , x == x); Console.WriteLine("x == y is {0}" , x == y);

这段代码产生以下输出:


x == x is True x == y is False


比较和相等操作

当比较大多数引用类型是否相等时,只比较引用。

  • If the references are equal-that is, if they point to the same object in memory-the equal comparison is true; Otherwise, it is false, even though two independent objects in memory are completely equivalent in other aspects.
  • This is called shallow ratio .
    illustrates the comparison of reference types. On the left side of the figure
  • , the references held by a and b are the same, so the comparison will return true. On the right side of the picture
  • , the reference is different, so even if the contents of two AClass objects are exactly the same, the comparison will return false.

Image

***图 8-4。*比较引用类型是否相等

类型string的对象也是引用类型,但是比较方式不同。当比较字符串是否相等时,会比较字符串的长度和区分大小写的内容。

  • If two strings have the same length and the same case-sensitive content, the equal comparison returns true, even if they occupy different memory areas.
  • This is called deep comparison .

在第十五章中提到的代表也是引用类型,也使用深度比较。当比较委托是否相等时,如果两个委托都是null,或者如果两个委托的调用列表中有相同数量的成员并且调用列表匹配,则比较返回true

比较数值表达式时,会比较类型和值。当比较enum类型时,比较是在操作数的基础值上进行的。枚举包含在第十三章中。

递增和递减运算符

递增运算符将操作数加 1。减量运算符从操作数中减去 1。表 8-9 列出了操作者及其描述。

这些操作符是一元的,有两种形式,即前置形式和后置形式,它们的作用不同。

  • In the prefix, the operator is placed before the operand; Such as ++x and --y.
  • In postposition, the operator is placed after the operand; Such as x++ and y--.

Image

在比较运算符的前置和后置形式时

  • The only difference is the value returned by the operator to the expression.

*表 8-10 显示了一个总结行为的例子。

例如,下面简单演示了四种不同版本的运算符。为了显示同一输入的不同结果,在每个赋值语句之前,操作数x的值被重置为5

`   int x = 5, y;
y = x++;   // result: y: 5, x: 6
Console.WriteLine("y: {0}, x: {1}" , y, x);

x = 5;
y = ++x;   // result: y: 6, x: 6
Console.WriteLine("y: {0}, x: {1}" , y, x);

x = 5;
y = x--;   // result: y: 5, x: 4
Console.WriteLine("y: {0}, x: {1}" , y, x);

x = 5;
y = --x;   // result: y: 4, x: 4
Console.WriteLine("y: {0}, x: {1}" , y, x);`

该代码产生以下输出:


y: 5, x: 6 y: 6, x: 6 y: 5, x: 4 y: 4, x: 4


条件逻辑运算符

逻辑运算符用于比较或否定其操作数的逻辑值,并返回结果逻辑值。表 8-11 列出了操作员。

逻辑 AND 和逻辑 OR 运算符是二进制和左关联的。逻辑 NOT 是一元的。

Image

这些运算符的语法如下,其中 Expr1Expr2 计算为布尔值:

   Expr1 && Expr2 *Expr1* || *Expr2* !    *Expr*

以下是一些例子:

`   bool bVal;
bVal = (1 == 1) && (2 == 2);      // True, both operand expressions are true
bVal = (1 == 1) && (1 == 2);      // False, second operand expression is false

bVal = (1 == 1) || (2 == 2);      // True, both operand expressions are true
bVal = (1 == 1) || (1 == 2);      // True, first operand expression is true
bVal = (1 == 2) || (2 == 3);      // False, both operand expressions are false

bVal = true;                      // Set bVal to true.
bVal = !bVal;                     // bVal is now false.`

条件逻辑运算符以“短路”模式运行,这意味着,如果在对 Expr1 求值之后,结果已经确定,那么它跳过对 Expr2 的求值。以下代码显示了在计算第一个操作数后可以确定其值的表达式示例:

`   bool bVal;
bVal = (1 == 2) && (2 == 2);    // False, after evaluating first expression

bVal = (1 == 1) || (1 == 2);    // True, after evaluating first expression`

由于短路行为,不要将有副作用的表达式(如更改值)放在 Expr2 中,因为它们可能不会被求值。在下面的代码中,变量iVal的后增量不会被执行,因为在第一个子表达式被执行后,可以确定整个表达式的值是false

`   bool bVal; int iVal = 10;

bVal = (1 == 2) && (9 == iVal++);       // result:  bVal = False, iVal = 10
↑                 ↑
False           Never evaluated`

逻辑运算符

按位逻辑运算符通常用于设置方法参数的位模式。表 8-12 列出了按位逻辑运算符。

除了按位求反运算符之外,这些运算符都是二元和左关联的。按位求反运算符是一元的。

Image

二元按位运算符比较两个操作数中每个位置的相应位,并根据逻辑运算设置返回值中的位。

图 8-5 展示了按位逻辑运算的四个例子。

Image

***图 8-5。*按位逻辑运算符的例子

下面的代码实现了前面的示例:

`   const byte x = 12, y = 10;
sbyte a;

a = x & y;                   //  a = 8
a = x | y;                   //  a = 14
a = x ^ y;                   //  a = 6
a = ~x;                      //  a = -13`

移位操作符

按位移位运算符将位模式向右或向左移动指定的位置数,空出的位用 0 或 1 填充。表 8-13 列出了移位操作符。

移位运算符是二元的和左结合的。这里显示了按位移位运算符的语法。移动的位置数量由 Count 给出。

   *Operand* << *Count*                         // Left shift *Operand* >> *Count*                         // Right shift

Image

对于绝大多数的 C# 编程来说,你不需要了解任何底层的硬件。但是,如果您正在对有符号的数字进行按位操作,了解数字表示会很有帮助。底层硬件以一种叫做二进制补码的形式表示带符号的二进制数。在二进制补码表示中,正数有它们正常的二进制形式。要对一个数求反,需要对该数进行逐位求反,然后加 1。这个过程将正数转化为负数,反之亦然。在二进制补码中,所有负数在最左边的位上都有一个 1。图 8-6 显示了对数字 12 的否定。

Image

***图 8-6。*要得到一个二进制补码数的反数,取其按位反数并加 1。

当移位有符号数字时,底层表示很重要,因为将整数值左移一位的结果与将它乘以二的结果相同。向右移动和除以二是一样的。

然而,如果您将一个负数向右移位,并且最左边的位被填充为 0,则会产生错误的结果。最左边的 0 表示正数。但这是不正确的,因为负数除以 2 不会产生正数。

为了解决这种情况,当操作数是一个有符号整数时,如果操作数最左边的位是 1(表示一个负数),左边的位位置用 1 而不是 0 填充。这保持了正确的二进制补码表示。对于正数或无符号数,左边空出的位用 0 填充。

图 8-7 显示了表达式 14 << 3 如何在byte中求值。此操作会导致以下情况:

  • Each bit in operand (14) is shifted to the left by three bits.
  • The left three digits at the right end are filled with zeros.
  • The resulting value is 112.

Image

***图 8-7。*左移三位的例子

图 8-8 说明了按位移位操作。

Image

***图 8-8。*按位移位

下面的代码实现了前面的示例:

`   int a, b, x = 14;

a = x << 3;              // Shift left
b = x >> 3;              // Shift right

Console.WriteLine("{0} << 3 = {1}" , x, a);
Console.WriteLine("{0} >> 3 = {1}" , x, b);`

该代码产生以下输出:


14 << 3 = 112 14 >> 3 = 1


赋值运算符

赋值运算符计算运算符右侧的表达式,并使用该值设置运算符左侧的变量表达式的值。表 8-14 列出了赋值运算符。

赋值操作符是二元的和右关联的。

Image

语法如下:

VariableExpression Operator Expression

对于简单赋值,计算运算符右边的表达式,将其值赋给左边的变量。

   int x; x = 5; x = y * z;

记住赋值表达式是一个表达式,因此返回一个值到它在语句中的位置。执行赋值后,赋值表达式的值是左操作数的值。因此,在表达式x = 10的情况下,值 10 被赋给变量x。x 的值,现在是 10,变成了整个表达式的值。

由于赋值是一个表达式,它可以是一个更大表达式的一部分,如图 8-9 所示。表达式的评估如下:

  • Since the assignment is right-related, the evaluation starts from the right, and the variable x is assigned a value of 10.
  • That expression is the right operand assigned by the variable y, so the value of x, now 10, is assigned to y.
  • The assignment of y is the right operand of the assignment of z-all three variables have values of 10.

Image

***图 8-9。*赋值表达式在完成赋值后返回其左操作数的值。

可以位于赋值运算符左侧的对象类型如下。它们将在本文后面讨论。

  • Variables (local variables, fields, parameters)
  • attribute
  • Indexer
  • event

复合赋值

通常,您会希望对表达式求值,并将结果添加到变量的当前值中,如下所示:

   x = x + expr;

复合赋值操作符允许一种简化方法,以避免在某些常见情况下左侧变量在右侧重复出现。例如,下面两个语句在语义上是等价的,但是第二个语句更短,也同样容易理解。

   x = x + (y – z); x += y – z;

其他复合赋值语句是类似的:

                                     Notice the parentheses.                                          ↓     ↓ x *= y – z;      // Equivalent to x = x * (y – z) x /= y – z;      // Equivalent to x = x / (y – z) ...

条件运算符

条件运算符是根据条件的结果返回两个值之一的强大而简洁的方法。表 8-15 显示了操作员。

条件运算符是三元的。

Image

条件运算符的语法如下所示。它有一个测试表达式和两个结果表达式。

  • Condition must return a value of type bool.
  • If Condition evaluates to true, Expression1 evaluates and returns. Otherwise, evaluate and return Expression2 .

   Condition ? Expression1 : Expression2

条件运算符可以与if...else结构进行比较。例如,下面的if...else构造检查一个条件,如果条件为真,该构造将5赋给变量intVar。否则,它给它赋值 10。

   if ( x < y )                                       // if...else intVar = 5; else intVar = 10;

条件运算符可以以不太冗长的形式执行相同的操作,如以下语句所示:

   intVar = x < y  ?  5  :  10;                       // Conditional operator

将条件和每个返回表达式放在单独的行上,如下面的代码所示,这使得意图非常容易理解。

   intVar = x < y ?  5 :  10 ;

图 8-10 比较了示例中所示的两种形式。

Image

***图 8-10。*条件运算符 vs. if...否则

例如,下面的代码使用了三次条件运算符——在每个WriteLine语句中使用一次。首先,它返回x的值或者y的值。在后两个实例中,它返回空字符串或字符串“not”。

`   int x = 10, y = 9;
int highVal = x > y                            // Condition
? x                           // Expression 1
: y;                          // Expression 2
Console.WriteLine("highVal:  {0}\n" , highVal);

Console.WriteLine("x is{0} greater than y" ,
x > y                    // Condition
? ""                     // Expression 1
: " not" );              // Expression 2
y = 11;
Console.WriteLine("x is{0} greater than y" ,
x > y                    // Condition
? ""                     // Expression 1
: " not" );              // Expression 2`

该代码产生以下输出:


`highVal:  10

x is greater than y
x is not greater than y`


Image if...else语句是一条控制流语句。它应该用于执行两个动作中的一个或另一个。条件运算符返回一个表达式。用于返回两个中的一个或另一个。

一元算术运算符

一元运算符设置数值的符号。它们在表 8-16 中列出。

  • The unary operator simply returns the value of the operand.
  • The unary negative operator returns the value of the operand minus 0.

Image

例如,以下代码显示了运算符的用法和结果:

   int x = +10;         // x = 10 int y = -x;          // y = -10 int z = -y;          // z = 10

用户自定义类型转换

用户定义的转换在第十六章中有更详细的讨论,但是我在这里也会提到它们,因为它们是操作符。

您可以为自己的类和结构定义隐式和显式转换。这允许您将用户定义类型的对象转换为其他类型,反之亦然。* C# provides implicit and explicit conversions.
* Using implicit conversion , when the compiler resolves what type to use in a specific context, it will automatically convert if necessary.
* With explicit conversion , the compiler will only convert when using the explicit conversion operator.

声明隐式转换的语法如下。所有用户定义的转换都需要publicstatic修饰符。

         Required                     Target               Source <ins>        ↓             </ins>             ↓        <ins>          ↓          </ins> public static implicit operator *TargetType* ( *SourceType Identifier* ) { ... return *ObjectOfTargetType*; }

显式转换的语法是相同的,只是用explicit代替了implicit

下面的代码展示了一个转换操作符的声明示例,它将把类型为LimitedInt的对象转换为类型为int的对象,反之亦然。

`   class LimitedInt                   Target     Source
{
↓        ↓      
public static implicit operator int (LimitedInt li)    // LimitedInt to int
{
return li.TheValue;
}                               Target         Source
↓            ↓  
public static implicit operator LimitedInt (int x)     // int to LimitedInt
{
LimitedInt li = new LimitedInt();
li.TheValue = x;
return li;
}

private int _theValue = 0;
public int TheValue
}`

例如,下面的代码重复并使用了刚刚定义的两个类型转换操作符。在Main中,一个int文字被转换成一个LimitedInt对象,在下一行中,一个LimitedInt对象被转换成一个int

`   class LimitedInt
{
const int MaxValue = 100;
const int MinValue = 0;

public static implicit operator int(LimitedInt li)       // Convert type
{
return li.TheValue;
}

public static implicit operator LimitedInt(int x)        // Convert type
{
LimitedInt li = new LimitedInt();
li.TheValue = x;
return li;
}

private int _theValue = 0;
public int TheValue                                      // Property
{
get { return _theValue; }
set
{
if (value < MinValue)
_theValue = 0;
else
_theValue = value > MaxValue
? MaxValue
: value;
}
}
}

class Program
{
static void Main()                           // Main
{
LimitedInt li = 500;                      // Convert 500 to LimitedInt
int value     = li;                       // Convert LimitedInt to int

Console.WriteLine("li: {0}, value: {1}" , li.TheValue, value);
}
}`

该代码产生以下输出:


li: 100, value: 100


显式转换和强制转换运算符

前面的示例代码展示了从intLimitedInt类型的隐式转换,以及从LimitedIntint类型的隐式转换。但是,如果您将两个转换操作符声明为explicit,那么在进行转换时,您将不得不显式地使用 cast 操作符。

一个转换操作符由你想要将表达式转换成的类型名组成,在一对括号内。例如,在下面的代码中,方法Main将值 500 转换为一个LimitedInt对象。

                               Cast operator <ins>     ↓       </ins> LimitedInt li = (LimitedInt) 500;

例如,下面是代码的相关部分,其中的更改标记为:

在这两个版本的代码中,输出如下:


li: 100, value: 100


还有另外两个运算符,它们接受一种类型的值,并返回另一种指定类型的值。这些是is操作符和as操作符。这些都在第十六章的结尾有所涉及。

运算符重载

正如您所看到的,C# 操作符被定义为使用预定义的类型作为操作数。如果遇到用户定义的类型,操作者根本不知道如何处理它。运算符重载允许您定义 C# 运算符应该如何对用户定义类型的操作数进行运算。

  • Operator overloading is only available for classes and structures.
  • By declaring a method named operator x to realize behavior (for example, operator +, operator - and so on), you can overload an operator x for your class or structure.
    • The overloaded method of unary operator adopts a single parameter of type class or struct.
    • Overload method of binary operator takes two parameters, at least one of which must be of type class or struct.    public static LimitedInt operator -(LimitedInt x)               // Unary public static LimitedInt operator +(LimitedInt x, double y)     // Binary

运算符重载方法的声明需要满足以下条件:

  • Declarations must use both static and public modifiers.
  • Operator must be a member of the class or structure to which the operator belongs.

例如,下面的代码显示了类LimitedInt的两个重载操作符:加法操作符和求反操作符。您可以看出这是否定,而不是减法,因为运算符重载方法只有一个参数,因此是一元的,而减法运算符是二元的。

`   class LimitedInt Return
{
Required       Type    Keyword  Operator  Operand
       ↓           ↓         ↓     ↓        ↓                
public static LimitedInt operator + (LimitedInt x, double y)
{
LimitedInt li = new LimitedInt();
li.TheValue = x.TheValue + (int)y;
return li;
}

public static LimitedInt operator - (LimitedInt x)
{
// In this strange class, negating a value just sets it to 0.
LimitedInt li = new LimitedInt();
li.TheValue = 0;
return li;
}
...
}`

对运算符重载的限制

并非所有的运算符都可以重载,而且可以重载的类型也有限制。关于运算符重载的限制,您应该知道的一些重要的事情将在本节的后面描述。

只有下列运算符可以重载。列表中明显缺少的是赋值运算符。

可重载的一元运算符:+-!~++--truefalse

可重载的二元运算符:+``-``*``/``%``&``|``^``<<``>>``==``!=``>``<``>=``<=

递增和递减运算符是可重载的。但是与预定义版本不同,重载操作符的使用前和使用后没有区别。

在操作符重载的情况下,您不能做以下事情:

  • Create a new operator
  • Change the syntax of operators.
  • Redefine how operators act on predefined types.
  • Change the priority or combination of operators.

Image 注意你的重载操作符要符合操作符的直观含义。

运算符重载的例子

以下示例显示了类LimitedInt的三个运算符的重载:求反、减法和加法。

`class LimitedInt {
const int MaxValue = 100;
const int MinValue = 0;

public static LimitedInt operator -(LimitedInt x)
{
// In this strange class, negating a value just sets its value to 0.
LimitedInt li = new LimitedInt();
li.TheValue = 0;
return li;
}

public static LimitedInt operator -(LimitedInt x, LimitedInt y)
{
LimitedInt li = new LimitedInt();
li.TheValue = x.TheValue - y.TheValue;
return li;
}

public static LimitedInt operator +(LimitedInt x, double y)
{
LimitedInt li = new LimitedInt();
li.TheValue = x.TheValue + (int)y;
return li;
}

private int _theValue = 0;
public int TheValue
{
get { return _theValue; }
set
{
if (value < MinValue)
_theValue = 0;
else
_theValue = value > MaxValue
? MaxValue
: value;
}
}
}
   class Program {
static void Main() {
LimitedInt li1 = new LimitedInt();
LimitedInt li2 = new LimitedInt();
LimitedInt li3 = new LimitedInt();
li1.TheValue = 10; li2.TheValue = 26;
Console.WriteLine(" li1: {0}, li2: {1}" , li1.TheValue, li2.TheValue);

li3 = -li1;
Console.WriteLine("-{0} = {1}" , li1.TheValue, li3.TheValue);

li3 = li2 - li1;
Console.WriteLine(" {0} - {1} = {2}" ,
li2.TheValue, li1.TheValue, li3.TheValue);

li3 = li1 - li2;
Console.WriteLine(" {0} - {1} = {2}" ,
li1.TheValue, li2.TheValue, li3.TheValue);
}
}`

该代码产生以下输出:


li1: 10, li2: 26 -10 = 0 26 - 10 = 16 10 - 26 = 0


操作员的类型

typeof操作符返回作为其参数给出的任何类型的System.Type对象。从这个对象,你可以了解类型的特征。(对于任何给定的类型,只有一个System.Type对象。)不能重载typeof操作符。表 8-17 列出了操作员的特征。

typeof运算符是一元的。

Image

以下是typeof运算符的语法示例。TypeSystem名称空间中的一个类。

   Type t = typeof ( SomeClass )

例如,下面的代码使用了typeof操作符来获取关于名为SomeClass的类的信息,并打印其公共字段和方法的名称。

`   using System.Reflection;  // Use the Reflection namespace to take full advantage
// of determining information about a type.
class SomeClass
{
public int  Field1;
public int  Field2;

public void Method1()
public int  Method2() { return 1; }
}

class Program
{
static void Main()
{
Type t = typeof(SomeClass);
FieldInfo[]  fi = t.GetFields();
MethodInfo[] mi = t.GetMethods();

foreach (FieldInfo f in fi)
Console.WriteLine("Field : {0}" , f.Name);
foreach (MethodInfo m in mi)
Console.WriteLine("Method: {0}" , m.Name);
}
}`

该代码产生以下输出:


Field : Field1 Field : Field2 Method: Method1 Method: Method2 Method: ToString Method: Equals Method: GetHashCode Method: GetType


操作符typeof也被GetType方法调用,该方法可用于每种类型的每一个对象。例如,下面的代码检索对象的类型名称:

`   class SomeClass

class Program
{
static void Main()
{
SomeClass s = new SomeClass();

Console.WriteLine("Type s: {0}" , s.GetType().Name);
}
}`

该代码产生以下输出:


Type s: SomeClass


其他操作员

本章介绍的运算符是内置类型的标准运算符。还有其他特殊用途的操作符,以及它们的操作数类型,将在本书后面讨论。例如,可空类型有一个特殊的操作符,称为空合并操作符,这在第二十五章中有详细描述。*

九、语句

什么是报表?

C# 中的语句与 C 和 C++中的语句非常相似。本章涵盖了 C# 语句的特征,以及该语言提供的控制流语句。

  • Statement is a source code instruction that describes the type or tells the program to perform actions.
  • There are three categories of statements:
    • Declaration statement : A statement that declares a type or variable.
    • Embedded statement : a statement that performs actions or manages control flow.
    • tag statement : the statement that controls the jump to.

前面的章节已经介绍了许多不同的声明语句,包括局部变量、类和类成员的声明。本章介绍嵌入式语句,这些语句不声明类型、变量或实例。相反,它们使用表达式和控制流构造来处理由声明语句声明的对象和变量。

  • Simple statement consists of an expression followed by a semicolon.

  • Declarative statement
  • Embedded statement
  • Tag statement
  • Nested block

以下代码给出了每种情况的示例:

`   int x = 10;              // Simple declaration
int z;                   // Simple declaration

{                        // Start of a block
int y = 20;           // Simple declaration
z = x + y;            // Embedded statement
top: y = 30;             // Labeled statement
...
{                     // Start of a nested block
...
}                     // End of nested block
}                        // End of outer block`

Image 注意一个块在语法上算作一个嵌入式语句。在语法上需要嵌入语句的任何地方,都可以使用块。

一个空语句只包含一个分号。您可以在语言语法需要嵌入语句但程序逻辑不需要任何操作的任何位置使用空语句。

例如,下面的代码显示了使用 empty 语句的示例。

  • The second line in the code is an empty statement. This is necessary because there must be an embedded statement between the if part and the else part of the construction.
  • The fourth line is a simple statement, indicated by a terminating semicolon.

   if( x < y ) ;                     // Empty statement else z = a + b;            // Simple statement

表情语句

前一章看了表情。表达式返回值,但是它们也会有副作用。

  • A side effect is an action that affects the state of the program.
  • Many expressions are only evaluated for their side effects.

通过在表达式后放置语句终止符(分号),可以从表达式创建语句。表达式返回的任何值都将被丢弃。例如,下面的代码显示了一个表达式语句。它由赋值表达式(一个赋值运算符和两个操作数)后跟一个分号组成。这做了以下两件事:

x

这被认为是副作用。

  • After setting the value of x, the expression returns the new value of x. But nothing can receive this return value, so it was ignored. x = 10;

评估表达式的全部原因是为了实现副作用。

控制流报表

C# 提供了现代编程语言常见的控制流结构。

  • Conditional execution Execute or skip a piece of code according to conditions. The conditional execution statement is as follows:
    • if
    • if...else
    • switch
  • Loop statement Repeats a piece of code. The loop statement reads as follows:
    • while
    • do
    • for
    • foreach
  • Jump statement Changes the control flow from one code to a specific statement in another code. The jump statement reads as follows:
    • break
    • continue
    • return
    • goto
    • throw

条件执行和循环构造(除了foreach)需要一个测试表达式,或者条件,来决定程序应该在哪里继续执行。

Image 注意与 C 和 C++不同,在 C# 中测试表达式必须返回一个bool类型的值。在 C# 中,数字没有布尔解释。

if 语句

if语句实现条件执行。此处显示了if语句的语法,并在图 9-1 中进行了说明。

  • TestExpr A value of type bool must be calculated.
  • If TestExpr evaluates to true, Statement is executed.
  • If the evaluation is false, skip Statement .

   if( *TestExpr* ) *Statement* Image

***图 9-1。*if 语句

以下代码显示了if语句的示例:

`   // With a simple statement
if( x ⇐ 10 )
z = x – 1;        // Single statement--no curly braces needed

// With a block
if( x >= 20 )
{
x = x – 5;        // Block--curly braces needed
y = x + z;
}

int x = 5;
if( x )               // Error: test expression must be a bool, not int
`

如果...else 语句

if...else语句实现了双向分支。此处显示了if...else语句的语法,并在图 9-2 中进行了说明。

  • If TestExpr evaluates to true, Statement1 is executed.
  • Otherwise, perform Statement2 instead.

   if( *TestExpr* ) *Statement1* else *Statement2* Image

***图 9-2。*如果...else 语句

下面是一个if...else语句的例子:

   if( x <= 10 ) z = x – 1;               // Single statement else {                           // Multiple statements--block x = x – 5; y = x + z; }

当然, Statement1Statement2 或者两者本身都可以是if或者if...else语句,可以进一步嵌套。如果您正在查看包含嵌套的if...else语句的代码,并且需要确定哪个else与哪个if相配,有一个简单的规则。每个else都属于没有关联else子句的最近的前一个 if

Statement2 是一个ifif...else语句时,常见的格式如下所示,将第二个if子句与else子句放在同一行。这个例子展示了两个if...else语句,但是您可以创建一个任意长的链。

   if( *TestExpr1* ) *Statement1* else if ( *TestExpr2* ) *Statement2* else *Statement3*

while 循环

while循环是一个简单的循环结构,其中测试表达式在循环的顶部执行。这里显示了while循环的语法,并在图 9-3 中进行了说明。

  • Evaluate TestExpr first
  • If TestExpr evaluates to false, execution will continue after the while loop ends.
  • Otherwise, when TestExpr is evaluated as true, Statement is executed and TestExpr is evaluated again. Every time the calculation result of TestExpr is true, Statement will be executed once When TestExpr evaluates to false, the loop ends.

   while( *TestExpr* ) *Statement* Image

***图 9-3。*while 循环

下面的代码展示了一个while循环的例子,其中测试表达式变量从值 3 开始,并在每次迭代中递减。当变量值变为 0 时,循环退出。

   int x = 3; while( x > 0 ) { Console.WriteLine("x:  {0}", x); x--; } Console.WriteLine("Out of loop");

该代码产生以下输出:


x:  3 x:  2 x:  1 Out of loop


do 循环

do循环是一个简单的循环结构,其中测试表达式在循环的底部执行。这里显示了do循环的语法,并在图 9-4 中进行了说明。

  • First, Statement is executed.
  • Then, TestExpr is evaluated.
  • If TestExpr returns to true, Statement is executed again.
  • Every time TestExpr returns to true, Statement is executed again.
  • When TestExpr returns to false, control is transferred to the statement after the loop construction is completed.

   do *Statement* while( *TestExpr* );             // End of do loop Image

***图 9-4。*do 循环

do循环有几个特征使它区别于其他控制流结构。它们是:

  • The main body of the loop, Statement , is always executed at least once, even if TestExpr is originally false. The reason is that TestExpr is not evaluated until the bottom of the loop.
  • A semicolon is required after the closing bracket of the test expression.

以下代码显示了一个do循环的例子:

   int x = 0; do Console.WriteLine("x is {0}", x++); while (x<3); ↑ Required

该代码产生以下输出:


x is 0 x is 1 x is 2


for 循环

当测试表达式在循环的顶部求值时,只要它返回true,for循环结构就执行循环体。这里显示了for循环的语法,如图 9-5 中的所示。

  • At the beginning of the for loop, Initializer is executed once.
  • TestExpr is the later evaluation.
  • If TestExpr returns true, Statement is executed, followed by IterationExpr .
  • Control then returns to the top of the loop and evaluates TestExpr again.
  • As long as TestExpr returns true Statement , followed by IterationExpr , it will be executed.
  • Once TestExpr returns to false, continue to execute the statements after Statement .

                 Separated by semicolons ↓         ↓                               for( *Initializer*; *TestExpr*; *IterationExpr* ) *Statement*

语句的某些部分是可选的,其他部分是必需的。

  • Initializer , TestExpr , IterationExpr are optional. Their positions can be left blank. If the TestExpr position is empty, the test assumes that true is returned. Therefore, if the program wants to avoid entering an infinite loop, there must be other ways to exit the statement.
  • These semicolons are always required for field separators, even if any optional items are omitted.

Image

***图 9-5。*for 循环

图 9-5 说明了通过for语句的控制流程。您还应该了解其组件的以下信息:

  • Initializer is only executed once, before any other part of for structure. It is usually used to declare and initialize local values to be used in loops.
  • TestExpr is evaluated to determine whether Statement should be executed or skipped. It must calculate a value of type bool. As mentioned earlier, if TestExpr is empty, the assumption is always true.
  • IterationExp R is executed immediately after Statement and before returning to the top of TestExpr cycle.

例如,在下面的代码中

  • First, the initializer (int i=0) defines a variable named i and initializes its value to 0.
  • Then evaluate the condition (i<3). If it is true, then loop body is executed.
  • At the bottom of the loop, after all the loop statements are executed, execute the IterationExpr statement-in this case, increment the value of i.

`   // The body of this for loop is executed three times.
for( int i=0 ; i<3 ; i++ )
Console.WriteLine("Inside loop.  i:  {0}", i);

Console.WriteLine("Out of Loop");`

该代码产生以下输出:


Inside loop.  i:  0 Inside loop.  i:  1 Inside loop.  i:  2 Out of Loop


for 语句中变量的范围

初始化器中声明的变量,称为循环变量,仅在 for 语句中可见。**

  • This is different from C and C++, where declarations introduce variables into closed blocks.
  • The following code illustrates this point:

`   Type is needed here for declaration.

for(int i=0; i<10; i++ ) // Variable i is in scope here, and also
Statement;             // here within the statement.
// Here, after the statement, i no longer exists.

Type is needed here again because the previous variable i has gone out of existence.

for(int i=0; i<10; i++ ) // We need to define a new variable i here,
Statement;            // the previous one has gone out of existence.`

在循环体内声明的局部变量只有在循环内才是已知的。

Image 注意循环变量通常被赋予标识符ijk。这是 FORTRAN 编程时代的传统。在 FORTRAN 中,以字母 IJKLMN 开头的标识符默认为INTEGER类型,您不必声明它们。由于循环变量通常是整数,程序员简单地使用了简单的惯例,即使用I作为循环变量的名称。它简短易用,而且你不用申报。如果他们有一个嵌套循环,内部循环变量通常被命名为J。如果还有另一个内部嵌套循环,该变量被命名为K

虽然有些人反对使用非描述性的名称作为标识符,但我喜欢历史联系,以及使用这些标识符作为循环变量时的清晰和简洁。

初始值设定项和迭代表达式中的多个表达式

初始化表达式和迭代表达式都可以包含多个表达式,只要用逗号分隔。

例如,以下代码在初始值设定项中有两个变量声明,在迭代表达式中有两个表达式:

`   static void Main( )
{
const int MaxI = 5;

Two declarations           Two expressions
        ↓                           for (int i = 0, j = 10; i < MaxI; i++, j += 10)
{
Console.WriteLine("{0}, {1}", i, j);
}
}`

该代码产生以下输出:


0, 10 1, 20 2, 30 3, 40 4, 50


切换语句

switch语句实现多路分支。图 9-6 显示了switch语句的语法和结构。

  • The switch statement contains zero or more switch segments .
  • Each switch segment starts with one or more switch labels.
  • Each switch segment must end with a break statement or one of the other four jump statements.
    • The jump statements are break, return, continue, goto and throw. These will be introduced later in this chapter.
    • Among the five jump statements, the break statement is the most commonly used statement to end a switch paragraph. break statement transfers execution to the end of switch statement. I will discuss all jump statements later in this chapter.

开关标签按顺序进行评估。如果一个匹配测试表达式的值,它的 switch 部分被执行,然后控制跳转到switch语句的底部。

Image

***图 9-6。*switch 语句的结构

开关标签具有以下形式:

<ins>case</ins> ConstantExpression: ↑                    ↑ Keyword           Switch label terminator

图 9-6 中通过结构的控制流程如下:

  • Test the expression TestExpr and evaluate it at the top of the construct.
  • If the value of TestExpr is equal to the value of ConstExpr1 , the constant expression in the first switch tag is executed, and then the statements in the statement list of after the switch tag are executed until one of the jump statements is encountered.
  • default paragraph is optional, but if it is included, it must end with one of the jump statements.

图 9-7 说明了通过switch语句进行控制的一般流程。您可以通过一个带有goto语句或return语句的switch语句来修改流程。

Image

***图 9-7。*通过 switch 语句控制的流程

一个开关例子

下面的代码执行了五次switch语句,其中x的值从15。从输出中,可以看出在循环的每个周期中执行了哪个 case 部分。

`   for( int x=1; x<6; x++ )
{
switch( x )                          // Evaluate the value of variable x.
{
case 2:                                        // If x equals 2
Console.WriteLine("x is {0} -- In Case 2", x);
break;                                      // Go to end of switch.

case 5:                                        // If x equals 5
Console.WriteLine("x is {0} -- In Case 5", x);
break;                                      // Go to end of switch.

default:                                       // If x is neither 2 nor 5
Console.WriteLine("x is {0} -- In Default case", x);
break;                                      // Go to end of switch.
}
}`

该代码产生以下输出:


x is 1 -- In Default case x is 2 -- In Case 2 x is 3 -- In Default case x is 4 -- In Default case x is 5 -- In Case 5


更多关于开关的语句

一个switch语句可以有任意数量的开关部分,包括零个。不需要default部分,如下例所示。但是,包含它通常被认为是一种好的做法,因为它可以捕捉潜在的错误。

例如,下面代码中的switch语句没有default部分。switch语句在一个for循环中,该循环执行该语句五次,x的值从1开始,到5结束。

   for( int x=1; x<6; x++ ) { switch( x ) { case 5: Console.WriteLine("x is {0} -- In Case 5", x); break; } }

该代码产生以下输出:


x is 5 -- In Case 5


以下代码只有默认部分:

   for( int x=1; x<4; x++ ) { switch( x ) { default: Console.WriteLine("x is {0} -- In Default case", x); break; } }

该代码产生以下输出:


x is 1 -- In Default case x is 2 -- In Default case x is 3 -- In Default case


切换标签

开关标签中关键字case后面的表达式必须是常量表达式,因此在编译时,编译器必须完全可以对其求值。它还必须与测试表达式的类型相同。

例如,图 9-8 显示了三个示例switch报表。

Image

***图 9-8。*带有不同类型开关标签的开关报表

Image 注意与 C 和 C++不同,在 C# 中每个switch段,包括可选的默认段,都必须以其中一个跳转语句结束。在 C# 中,你不能在一个开关部分执行代码,然后通过进入下一个。

尽管 C# 不允许从一个开关部分切换到另一个开关部分,但您可以执行以下操作:

  • You can attach multiple switch labels to any switch section.
  • After the list of statements associated with a case, there must be a jump statement before the next switch tag, unless there is no to insert the executable statement between the switch tags.

例如,在下面的代码中,由于前三个开关标签之间没有可执行语句,所以一个接一个就可以了。然而,案例 5 和案例 6 之间有一个可执行语句,所以在案例 6 之前必须有一个跳转语句。

   switch( x ) { case 1:                    // Acceptable case 2: case 3:   ...                     // Execute this code if x equals 1, 2, or 3. break; case 5: y = x + 1; case 6:                    // Not acceptable because there is no break ...

跳转报表

当控制流到达跳转语句时,程序执行被无条件地转移到程序的另一部分。跳转语句如下:

  • break
  • continue
  • return
  • goto
  • throw

本章涵盖了这些陈述中的前四条。第十一章的中解释了throw声明。

break 语句

在本章的前面,你看到了在switch语句中使用的break语句。它也可用于以下语句类型:

  • for
  • foreach
  • while
  • do

在这些语句的主体中,break导致执行退出最里面的封闭循环。

例如,如果下面的while循环只依赖于它的测试表达式,那么它将是一个无限循环,测试表达式总是true。但是,在循环的三次迭代之后,遇到了break语句,循环被退出。

   int x = 0; while( true ) { x++; if( x >= 3 ) break; }

继续语句

continue语句使程序执行到以下类型的最内层封闭循环顶层:

  • while
  • do
  • for
  • foreach

例如,下面的for循环执行五次。在前三次迭代中,它遇到了continue语句并直接返回到循环的顶部,错过了循环底部的WriteLine语句。执行仅在最后两次迭代中到达WriteLine语句。

`   for( int x=0; x<5; x++ )            // Execute loop five times
{
if( x < 3 )                      // The first three times
continue;                     // Go directly back to top of loop

// This line is only reached when x is 3 or greater.
Console.WriteLine("Value of x is {0}", x);
}`

该代码产生以下输出:


Value of x is 3 Value of x is 4


下面的代码展示了一个在while循环中的continue语句的例子。这段代码产生与前面的for循环示例相同的输出。

`   int x = 0;
while( x < 5 )
{
if( x < 3 )
{
x++;
continue;                         // Go back to top of loop
}

// This line is reached only when x is 3 or greater.
Console.WriteLine("Value of x is {0}", x);
x++;
}`

标注报表

带标签的语句由一个标识符、一个冒号和一个语句组成。它具有以下形式:

         Identifier: Statement

执行带标签的语句时,就好像标签不存在,只包含了 Statement 部分。

  • Tagging statements allows control to be transferred from another part of the code to statements.
  • Tagged statements can only be used inside a block.

标签

标签有自己的声明空间,因此带标签语句中的标识符可以是任何有效的标识符,包括那些可能在重叠范围内声明的标识符,如局部变量或参数名。

例如,以下代码显示了与局部变量具有相同标识符的标签的有效用法:

   { int xyz = 0;                                     // Variable xyz ... xyz: Console.WriteLine("No problem.");           // Label xyz }

然而,还是有一些限制。标识符不能是

  • A keyword
  • Same as another label identification and overlapping range

标注报表的范围

带标签的语句不能从声明它们的块之外的处看到(或访问)。带标签的语句的范围如下:

  • Declare as
  • Block, nested in the block.

内的任何块

例如,图 9-9 左边的代码包含了几个嵌套的块,并标记了它们的作用域。在程序的作用域 B 中声明了两个带标签的语句:incrementend

  • The shaded part on the right side of the figure shows the area where the markup statement is located in the code.
  • Code in scope B, and all nested blocks, can see and access the marked statements.
  • Code from any internal scope can jump from to marked statement.
  • Code from outside (scope A in this example) cannot jump to a block with a tag statement.

Image

***图 9-9。*标签的范围包括嵌套块。

goto 语句

goto语句无条件地将控制权转移给一个*标记的语句。*其一般形式如下,其中 Identifier 是被标注语句的标识符:

         goto *Identifier* ;

例如,下面的代码显示了一个goto语句的简单用法:

`   bool thingsAreFine;
while (true)
{
thingsAreFine = GetNuclearReactorCondition();

if ( thingsAreFine )
Console.WriteLine("Things are fine.");
else
goto NotSoGood;
}

NotSoGood: Console.WriteLine("We have a problem.");`

goto语句必须是标记语句的范围内。

  • A goto statement can jump to any marked statement in its own block, or can jump out of to any block it is nested in.
  • The goto statement cannot jump to any block nested within its own block.

Image 警告强烈反对使用goto语句,因为这会导致代码结构不良,难以调试和维护。Edsger Dijkstra 在 1968 年写给 ACM 的题为“转到被认为有害的语句”的信是对计算机科学的一个重要贡献;这是最先发表的关于使用goto语句的缺陷的描述之一。

switch 语句中的 goto 语句

还有另外两种形式的goto语句,在switch语句中使用。这些goto语句将控制转移到switch语句中相应命名的开关标签。

   goto case ConstantExpression; goto default;

使用语句

某些类型的非托管对象数量有限,或者占用大量系统资源。很重要的一点是,当你的代码完成后,要尽快发布它们。using语句有助于简化过程,并确保这些资源得到适当的处置。

资源是实现System.IDisposable接口的类或结构。接口在第十五章中有详细介绍——但简而言之,接口是类和结构可以选择实现的未实现函数成员的集合。IDisposable接口包含一个名为Dispose的方法。

使用资源的阶段如图 9-10 所示,包括以下内容:

  • Allocate resources
  • Use resources
  • Dispose of resources

如果在使用资源的代码部分发生意外的运行时错误,则释放资源的代码可能无法执行。

Image

***图 9-10。*使用资源的组件

Image 注意using语句不同于using指令。第二十一章中详细介绍了using指令。

包装一种资源的使用

using语句通过巧妙地打包资源的使用,有助于减少意外运行时错误的潜在问题。

using语句有两种形式。第一种形式如下,如图 9-11 所示。

  • Code in brackets allocates resources.
  • Statement is the code for using resources.
  • The using statement implicitly generates the code of processing resources.

   using ( <ins>*ResourceType* *Identifier* = *Expression*</ins> ) <ins>*Statement*</ins> ↑                        ↑ Allocates resource              Uses resource

意外的运行时错误被称为异常,在第二十二章中有详细介绍。处理异常可能性的标准方法是将可能导致异常的代码放在try块中,并将必须执行的任何代码放在finally块中,无论是否有异常。

这种形式的using语句正是这样做的。它执行以下操作:

  • Allocate resources
  • Put Statement into try block.
  • Create a call to the Dispose method of the resource and put it in the finally block.

Image

***图 9-11。*using 语句的效果

using 语句的例子

下面的代码使用了两次using语句——一次用于名为TextWriter的类,一次用于名为TextReader的类,两次都来自于System.IO名称空间。按照using语句的要求,这两个类都实现了IDisposable接口。

  • TextWriter The resource opens a text file for writing, and writes a line into the file.
  • TextReader The resource then opens the same text file and reads and displays the contents line by line.
  • In both cases, the using statement ensures that the Dispose method of the object is called.
  • Pay attention to the difference between using statement in Main and using instruction in the first two lines.

`   using System;                    // using DIRECTIVE; not using statement
using System.IO;                 // using DIRECTIVE; not using statement

namespace UsingStatement
{
class Program
{
static void Main( )
{
// using statement
using (TextWriter tw = File.CreateText("Lincoln.txt") )
{
tw.WriteLine("Four score and seven years ago, ...");
}

// using statement
using (TextReader tr = File.OpenText("Lincoln.txt"))
{
string InputString;
while (null != (InputString = tr.ReadLine()))
Console.WriteLine(InputString);
}
}
}
}`

该代码产生以下输出:


Four score and seven years ago, ...


多个资源和嵌套

using语句也可以用于相同类型的多个资源,用逗号分隔资源声明。语法如下:

        Only one type         Resource        Resource ↓          <ins>     ↓    </ins>    <ins>     ↓      </ins> using ( *ResourceType* Id1 = Expr1,  Id2 = Expr2, ... ) *EmbeddedStatement*

例如,在下面的代码中,每个using语句分配并使用两个资源:

`   static void Main()
{
using (TextWriter tw1 = File.CreateText("Lincoln.txt"),
tw2 = File.CreateText("Franklin.txt"))
{
tw1.WriteLine("Four score and seven years ago, ...");
tw2.WriteLine("Early to bed; Early to rise ...");
}

using (TextReader tr1 = File.OpenText("Lincoln.txt"),
tr2 = File.OpenText("Franklin.txt"))
{
string InputString;

while (null != (InputString = tr1.ReadLine()))
Console.WriteLine(InputString);

while (null != (InputString = tr2.ReadLine()))
Console.WriteLine(InputString);
}
}`

using语句也可以嵌套。在下面的代码中,除了嵌套using语句之外,还要注意没有必要对第二个using语句使用一个块,因为它只包含一个简单的语句。

`   using ( TextWriter tw1 = File.CreateText("Lincoln.txt") )
{
tw1.WriteLine("Four score and seven years ago, ...");

using ( TextWriter tw2 = File.CreateText("Franklin.txt") ) // Nested
tw2.WriteLine("Early to bed; Early to rise ...");       // Single
}`

using 语句的另一种形式

using语句的另一种形式如下:

  Keyword  Resource        Uses resource ↓      ↓                ↓ using ( *Expression* ) *EmbeddedStatement*

在这种形式中,资源是在using语句之前声明的。

`   TextWriter tw = File.CreateText("Lincoln.txt");           // Resource declared

using ( tw )                                              // using statement
tw.WriteLine("Four score and seven years ago, ...");`

尽管这种形式仍然可以确保在您使用完资源后总是调用Dispose方法,但是它并不能防止您在using语句释放其非托管资源后尝试使用该资源,使其处于不一致的状态。因此,它提供的保护较少,不被鼓励。该表格如图图 9-12 所示。

Image

***图 9-12。*using 语句前的资源声明

其他报表

还有一些与该语言的特定特征相关的陈述。这些陈述包含在处理这些特性的章节中。其他章节中涉及的声明如表 9-1 所示。

Image

十、结构

什么是结构?

结构是程序员定义的数据类型,非常类似于类。它们有数据成员和函数成员。尽管结构类似于类,但还是有许多重要的区别。最重要的如下:

  • Similar to reference type, structure is value type.
  • Structures are implicitly sealed, which means that they cannot be derived from.

声明结构的语法类似于声明类的语法:

`Keyword


struct StructName
{
MemberDeclarations
}`

例如,下面的代码声明了一个名为Point的结构。它有两个公共字段,名为XY。在Main中,声明了三个结构类型为Point的变量,它们的值被赋值并打印出来。

`   struct Point
{
public int X;
public int Y;
}

class Program
{
static void Main()
{
Point first, second, third;

first.X  = 10; first.Y = 10;
second.X = 20; second.Y = 20;
third.X  = first.X + second.X;
third.Y  = first.Y + second.Y;

Console.WriteLine("first:   {0}, {1}", first.X,  first.Y);
Console.WriteLine("second:  {0}, {1}", second.X, second.Y);
Console.WriteLine("third:   {0}, {1}", third.X,  third.Y);
}
}`

这段代码产生以下输出:


first:   10, 10 second:  20, 20 third:   30, 30


结构是值类型

与所有值类型一样,结构类型的变量包含自己的数据。因此

  • A variable of type struct cannot be null.
  • Two structs variables cannot refer to the same object.

例如,下面的代码声明了一个名为CSimple的类,一个名为Simple的结构,以及它们各自的一个变量。图 10-1 显示了这两者在内存中是如何排列的。

`   class CSimple
{
public int X;
public int Y;
}

struct Simple
{
public int X;
public int Y;
}

class Program
{
static void Main()
{
CSimple cs = new CSimple();
Simple  ss = new Simple();
...` Image

图 10-1 。类与结构的内存排列

分配给一个结构

将一个结构赋给另一个结构会将值从一个结构复制到另一个结构。这与从类变量复制截然不同,后者只复制引用。

图 10-2 显示了类变量和结构变量赋值的区别。注意,在类赋值之后,cs2指向堆中与cs1相同的对象。但是在 struct 赋值之后,ss2成员的值是那些在ss1中的值的副本。

`   class CSimple
{ public int X; public int Y; }

struct Simple
{ public int X; public int Y; }

class Program
{
static void Main()
{
CSimple cs1 = new CSimple(), cs2 = null;          // Class instances
Simple  ss1 = new Simple(),  ss2 = new Simple();  // Struct instances

cs1.X = ss1.X = 5;                  // Assign 5 to ss1.X and cs1.X.
cs1.Y = ss1.Y = 10;                 // Assign 10 to ss1.Y and cs1.Y.

cs2 = cs1;                          // Assign class instance.
ss2 = ss1;                          // Assign struct instance.
}
}` Image

图 10-2 。分配一个类变量和一个结构变量

构造函数和析构函数

结构可以有实例和静态构造函数,但不允许析构函数。

实例构造函数

该语言隐式地为每个结构提供了一个无参数的构造函数。此构造函数将结构的每个成员设置为该类型的默认值。值成员被设置为其默认值。参考成员被设置为null

每个结构都有预定义的无参数构造函数,您不能删除或重定义它。但是,您可以创建额外的构造函数,只要它们有参数。请注意,这与类不同。对于类,只有当没有其他构造函数被声明时,编译器才会提供一个隐式的无参数构造函数*。*

要调用一个构造函数,包括隐式的无参数构造函数,使用new操作符。注意,即使内存不是从堆中分配的,也使用了new操作符。

例如,下面的代码声明了一个简单的 struct,其构造函数带有两个int参数。Main创建该结构的两个实例——一个使用隐式无参数构造函数,另一个使用声明的双参数构造函数。

`   struct Simple
{
public int X;
public int Y;

public Simple(int a, int b)             // Constructor with parameters

}

class Program
{
static void Main()
{              Call implicit constructor
         Simple s1 = new Simple();
Simple s2 = new Simple(5, 10);

Call constructor
Console.WriteLine("{0},{1}", s1.X, s1.Y);
Console.WriteLine("{0},{1}", s2.X, s2.Y);
}
}`

你也可以不使用new操作符来创建一个结构的实例。但是,如果这样做,会有一些限制,如下所示:

  • The values of data members cannot be used unless explicitly set.
  • You can't call any function members of this structure until all data members are assigned values.

例如,下面的代码显示了在没有使用new操作符的情况下创建的 struct Simple的两个实例。当试图在没有显式设置数据成员值的情况下访问s1时,编译器会产生一条错误消息。在给其成员赋值后,从s2中读取没有问题。

`struct Simple
{
public int X;
public int Y;
}

class Program
{
static void Main()
{
No constructor calls
↓   ↓
Simple s1, s2;
Console.WriteLine("{0},{1}", s1.X, s1.Y);              // Compiler error
↑      ↑
s2.X = 5;                   Not yet assigned
s2.Y = 10;
Console.WriteLine("{0},{1}", s2.X, s2.Y);              // OK
}
}`

静态构造函数

与类一样,结构的静态构造函数创建并初始化静态数据成员,不能引用实例成员。结构的静态构造函数遵循与类相同的规则。

在以下两个操作中的第一个操作之前调用静态构造函数:

  • Call an explicitly declared constructor.
  • A static member of a reference structure

构造函数和析构函数概要

Image

不允许字段初始值设定项

结构声明中不允许字段初始值设定项,如下面的代码所示:

   struct Simple {            Not allowed <ins> ↓  </ins> public int x = 0;                // Compile error public int y <ins>= 10</ins>;               // Compile error } ↑ Not  allowed

结构是密封的

结构总是隐式密封的,因此不能从它们派生其他结构。

由于结构不支持继承,对结构成员使用几个类成员修饰符没有意义;因此,不能在它们的声明中使用它们。不能与结构一起使用的修饰符如下:

  • protected
  • inside
  • abstract
  • virtual

结构本身是从System.ValueType派生出来的,而System.ValueType又是从object派生出来的。

当创建一个与基类System.ValueType成员同名的成员时,你可以对结构成员使用的两个继承相关的关键字是newoverride修饰符,所有的结构都是从基类【】派生的。

装箱和拆箱

与其他值类型数据一样,如果要将 struct 实例用作引用类型对象,则必须进行装箱复制。装箱是制作值类型变量的引用类型副本的过程。装箱和拆箱在第十六章中有详细解释。

结构体作为返回值和参数

结构可以用作返回值和参数。

  • Return value : When struct is the return value, create a copy and return it from the function member.
  • Parameter: when a struct is used as Value parameter, a copy of the actual parameter struct is created. This copy is used for the execution of the method.
  • ref and out Parameters : If a structure is used as a ref or out parameter, the reference to the structure will be passed to the method so that the data member can be changed.

关于结构的附加信息

分配结构比创建类的实例需要更少的开销,所以使用结构而不是类有时可以提高性能——但是要小心装箱和拆箱的高成本。

最后,关于结构,您应该知道的最后一些事情如下:

  • Predefined simple types (int, short, long, etc.), although in. NET and C# are actually all in. NET as a structure.
  • You can declare the partial structure as you declare the partial class, as described in Chapter 6 of .

像类一样,结构也可以实现接口,这将在第十五章中讨论。

十一、枚举

枚举

枚举是程序员定义的类型,如类或结构。

像结构一样,枚举是值类型,因此直接存储它们的数据,而不是用引用和数据分开存储。* Enumeration has only one member type: named constants with integer values.

下面的代码展示了一个名为TrafficLight的新枚举类型的声明示例,它包含三个成员。请注意,成员声明列表是一个逗号分隔的列表;枚举声明中没有分号。

  Keyword      Enum name ↓        ↓ enum TrafficLight { Green,   ←  Comma separated—no semicolons Yellow,  ←  Comma separated—no semicolons Red }

每个枚举类型都有一个底层整数类型,默认情况下是int

  • Each enumeration member is assigned a constant value of the underlying type.
  • By default, the compiler assigns 0 to the first member, and gives each subsequent member a value greater than the value of the previous member by 1.

例如,在TrafficLight类型中,编译器将int012分别赋给成员GreenYellowRed。在下面代码的输出中,您可以通过将底层成员值转换为类型int来查看它们。图 11-1 显示了它们在堆栈上的排列。

`   TrafficLight t1 = TrafficLight.Green;
TrafficLight t2 = TrafficLight.Yellow;
TrafficLight t3 = TrafficLight.Red;

Console.WriteLine("{0},\t{1}",   t1, (int) t1);
Console.WriteLine("{0},\t{1}",   t2, (int) t2);
Console.WriteLine("{0},\t{1}\n", t3, (int) t3);

Cast to int`

这段代码产生以下输出:


Green,  0 Yellow, 1 Red,    2


Image

图 11-1 。枚举的成员常数由基础整数值表示。

可以将枚举值赋给枚举类型的变量。例如,下面的代码显示了三个类型为TrafficLight的变量的声明。请注意,您可以将成员文字赋给变量,也可以从同类型的另一个变量中复制值。

`class Program
{
static void Main()
{
Type   Variable     Member
↓       ↓           ↓       
TrafficLight t1 = TrafficLight.Red;        // Assign from member
TrafficLight t2 = TrafficLight.Green;      // Assign from member
TrafficLight t3 = t2;                      // Assign from variable

Console.WriteLine(t1);
Console.WriteLine(t2);
Console.WriteLine(t3);
}
}`

这段代码产生以下输出。请注意,成员名称被打印为字符串。


Red Green Green


设置底层类型和显式值

通过在枚举名后面加上冒号和类型名,可以使用除了int之外的整数类型。该类型可以是任何整数类型。所有成员常量都是枚举的基础类型。

                   Colon ↓ enum TrafficLight : ulong {                     ↑ ...           Underlying type

成员常量的值可以是基础类型的任何值。若要显式设置成员的值,请在枚举声明中在其名称后使用初始值设定项。可以有重复的值,但不能有重复的名称,如下所示:

   enum TrafficLight { Green  = 10, Yellow = 15,                 // Duplicate values Red    = 15                  // Duplicate values }

例如,图 11-2 中的代码显示了枚举TrafficLight的两个等价声明。

  • The code on the left accepts the default type and number.
  • The code on the right explicitly sets the underlying type to int and sets the member to the value corresponding to the default value.

Image

图 11-2 。等效枚举声明

隐含成员编号

您可以显式地为任何成员常量赋值。如果不初始化成员常量,编译器会隐式地给它赋值。图 11-3 展示了编译器用来分配这些值的规则。

  • The values associated with member names need not be different.

Image

图 11-3 。分配成员值的算法

例如,下面的代码声明了两个枚举。CardSuit接受成员的隐式编号,如注释中所示。FaceCards显式设置一些成员,并接受其他成员的隐式编号。

`   enum CardSuit
{
Hearts,                   // 0  - Since this is first
Clubs,                    // 1  - One more than the previous one
Diamonds,                 // 2  - One more than the previous one
Spades,                   // 3  - One more than the previous one
MaxSuits                  // 4  - A common way to assign a constant
}                             //      to the number of listed items

enum FaceCards
{
// Member                 // Value assigned
Jack              = 11,   // 11 - Explicitly set
Queen,                    // 12 - One more than the previous one
King,                     // 13 - One more than the previous one
Ace,                      // 14 - One more than the previous one
NumberOfFaceCards = 4,    // 4  - Explicitly set
SomeOtherValue,           // 5  - One more than the previous one
HighestFaceCard   = Ace   // 14 - Ace is defined above
}`

位标志

程序员长期以来使用单个字中的不同位作为表示一组开/关标志的简洁方式。在本节中,我将把这个单词称为标志单词。枚举提供了实现这一点的便捷方式。

一般步骤如下:

  1. Determine how many bit flags you need, and choose an unsigned integer type with enough bits to save them.
  2. Determine what each bit represents and name it. Declares an enumeration of the selected integer type, each member being represented by a bit position.
  3. Use the bitwise OR operator to set the appropriate bit in the word that holds the bit flag.
  4. Then, you can check whether a specific bit flag is set by using the HasFlag method or the bitwise AND operator.

例如,下面的代码显示了代表纸牌游戏中一副纸牌的选项的枚举声明。底层类型uint足以容纳所需的四个位标志。请注意以下关于代码的内容:

  • Members have names that represent binary options.
    • Each option is represented by a specific bit position in the word. The bit position remains 0 or 1.
    • You don't want to use 0 as a member value because the bit flag represents a set of bits that are either on or off. It already has a meaning-all bit flags are turned off.
  • In hexadecimal notation, each hexadecimal digit represents exactly four digits. Because of this direct relationship between bit pattern and hexadecimal representation, when using bit pattern, hexadecimal representation is usually used instead of decimal representation.
  • In fact, it is not necessary to decorate enumeration with Flags attribute, but it provides some extra convenience, which I will discuss soon. Property is displayed as a string in square brackets, just before the language construction. In this case, the attribute immediately precedes the enumeration declaration. I will talk about attributes in Chapter 24.

   [Flags] enum CardDeckSettings : uint { SingleDeck    = 0x01,            // Bit 0 LargePictures = 0x02,            // Bit 1 FancyNumbers  = 0x04,            // Bit 2 Animation     = 0x08             // Bit 3 }

图 11-4 说明了这种枚举。

Image

图 11-4 。标志位的定义(左),以及它们各自的表示(右)

要创建具有适当位标志的单词,请声明一个枚举类型的变量,并使用按位 OR 运算符来设置所需的位。例如,下面的代码设置了标志字中四个选项中的三个:

Enum type     Flag word       Bit flags ORed together ↓           ↓       <ins>         ↓                 </ins> CardDeckSettings ops =    CardDeckSettings.SingleDeck | CardDeckSettings.FancyNumbers | CardDeckSettings.Animation ;

要检查标志字是否设置了特定的位标志,可以使用 enum 类型的 Boolean HasFlag方法。您对标志字调用HasFlag方法,传入您正在检查的位标志,如下面的代码行所示。如果设置了指定的位标志,HasFlag返回true;否则,它返回false

   bool useFancyNumbers = ops.HasFlag(<ins>CardDeckSettings.FancyNumbers</ins>); ↑                         ↑ Flag word                    Bit flag

HasFlag方法也可以检查多个位标志。例如,以下代码检查标志字ops是否同时设置了AnimationFancyNumbers位。该代码执行以下操作:

  • The first statement creates a test word instance called testFlags with Animation and FancyNumbers bits set. It then passes testFlags as a parameter to the HasFlag method.
  • HasFlags Check whether all flags set in the test word are also set in the flag word ops. If yes, then HasFlag returns to true. Otherwise, return to false.

`   CardDeckSettings testFlags =
CardDeckSettings.Animation | CardDeckSettings.FancyNumbers;

bool useAnimationAndFancyNumbers = ops.HasFlag( testFlags );
↑                ↑
Flag word           Test word`

确定是否设置了一个或多个特定位的另一种方法是使用按位 AND 运算符。例如,像上面的代码一样,下面的代码检查一个标志字,以查看是否设置了FancyNumbers位标志。这是通过将标志字与位标志进行“与”运算,然后将结果与位标志进行比较来实现的。如果该位是在原始标志字中设置的,那么 AND 运算的结果将具有与位标志相同的位模式。

   bool useFancyNumbers = (ops & <ins>CardDeckSettings.FancyNumbers</ins>) == CardDeckSettings.FancyNumbers; ↑                   ↑ Flag word           Bit flag

图 11-5 说明了创建标志字,然后使用按位 and 运算来确定特定位是否被置位的过程。

Image

图 11-5 。产生一个标志字并检查它是否有特定的位标志

旗帜属性

前面的代码在声明枚举之前使用了Flags属性,如下所示:

   [Flags] enum CardDeckSettings : uint { ... }

Flags属性根本不会改变计算。然而,它确实提供了几个方便的特性。首先,它通知编译器、对象浏览器和其他查看代码的工具,枚举的成员应该作为位标志组合在一起,而不是仅作为单独的值使用。这允许浏览器更恰当地解释枚举类型的变量。

其次,它允许枚举的ToString方法为位标志的值提供更合适的格式。ToString方法获取一个枚举值,并将其与该枚举的常量成员的值进行比较。如果匹配其中一个成员,ToString返回该成员的字符串名称。

例如,检查下面的代码,其中枚举前面没有Flags属性。

`   enum CardDeckSettings : uint
{
SingleDeck     = 0x01,        // bit 0
LargePictures  = 0x02,        // bit 1
FancyNumbers   = 0x04,        // bit 2
Animation      = 0x08         // bit 3
}

class Program
{
static void Main( )
{
CardDeckSettings ops;

ops = CardDeckSettings.FancyNumbers;                   // Set one flag.
Console.WriteLine( ops.ToString() );
// Set two bit flags.
ops = CardDeckSettings.FancyNumbers | CardDeckSettings.Animation;
Console.WriteLine( ops.ToString() );                   // Print what?
}
}`

该代码产生以下输出:


FancyNumbers 12


在这段代码中,方法Main执行以下操作:

  • Create a variable of enumeration type CardDeckSettings, set a bit flag of it, and print out the value of the variable, namely the value FancyNumbers
  • Give the variable a new value, which consists of two set bit flags, and print out its value-12.

作为第二次赋值的结果显示的值12是作为intops的值,因为FancyNumbers为值4设置位,而Animation为值8设置位,从而给出值12int。在赋值后的WriteLine方法中,当ToString方法试图查找值为12的枚举成员的名称时,它发现不存在具有该值的成员——所以它只是打印出该值。

然而,如果我们要在声明枚举之前添加回Flags属性,这将告诉ToString方法可以单独考虑这些位。在查找该值时,ToString会发现12对应于两个单独的位标志成员——FancyNumbersAnimation——并会返回包含他们名字的字符串,用逗号和空格分隔。下面显示了使用Flags属性再次运行代码的结果:


FancyNumbers FancyNumbers, Animation


使用位标志的例子

以下代码将使用位标志的所有部分放在一起:

`[Flags]
enum CardDeckSettings : uint
{
SingleDeck     = 0x01,        // bit 0
LargePictures  = 0x02,        // bit 1
FancyNumbers   = 0x04,        // bit 2
Animation      = 0x08         // bit 3
}

class MyClass
{
bool UseSingleDeck               = false,
UseBigPics                  = false,
UseFancyNumbers             = false,
UseAnimation                = false,
UseAnimationAndFancyNumbers = false;

public void SetOptions( CardDeckSettings ops )
{
UseSingleDeck     = ops.HasFlag( CardDeckSettings.SingleDeck );
UseBigPics        = ops.HasFlag( CardDeckSettings.LargePictures );
UseFancyNumbers   = ops.HasFlag( CardDeckSettings.FancyNumbers );
UseAnimation      = ops.HasFlag( CardDeckSettings.Animation );

CardDeckSettings testFlags =
CardDeckSettings.Animation | CardDeckSettings.FancyNumbers;
UseAnimationAndFancyNumbers = ops.HasFlag( testFlags );
}

public void PrintOptions( )
{
Console.WriteLine( "Option settings:" );
Console.WriteLine( "  Use Single Deck                 - {0}", UseSingleDeck );
Console.WriteLine( "  Use Large Pictures              - {0}", UseBigPics );
Console.WriteLine( "  Use Fancy Numbers               - {0}", UseFancyNumbers );
Console.WriteLine( "  Show Animation                  - {0}", UseAnimation );
Console.WriteLine( "  Show Animation and FancyNumbers - {0}",
UseAnimationAndFancyNumbers );
}
}
Image    class Program
{
static void Main( )
{
MyClass mc = new MyClass( );
CardDeckSettings ops = CardDeckSettings.SingleDeck
| CardDeckSettings.FancyNumbers
| CardDeckSettings.Animation;
mc.SetOptions( ops );
mc.PrintOptions( );
}
}`

该代码产生以下输出:


Option settings: Use Single Deck                 - True Use Large Pictures              - False Use Fancy Numbers               - True Show Animation                  - True Show Animation and FancyNumbers - True


更多关于枚举

枚举只有一种成员类型:声明的成员常量。

  • You cannot use modifiers on members. They all implicitly have the same accessibility as enumeration.
  • Members are static, as you will remember, which means that even if there are no variables of enumerated type, they are accessible. As with all statics, use the type name followed by a dot and a member name to use members.

例如,下面的代码没有创建任何 enum TrafficLight类型的变量,但是因为成员是静态的,所以可以使用WriteLine访问和打印它们。

   static void Main() { Console.WriteLine("{0}", TrafficLight.Green); Console.WriteLine("{0}", TrafficLight.Yellow); Console.WriteLine("{0}", TrafficLight.Red); } ↑        ↑ Enum name   Member name

枚举是一种独特的类型。比较不同枚举类型的枚举成员会导致编译时错误。例如,下面的代码声明了两个不同的枚举类型,它们具有完全相同的结构和成员名称。

  • The first if statement is good because it compares different members from the same enumeration type.
  • The second if statement generates an error because it attempts to compare members from different enumeration types. This error occurs even if the structure and member names are identical.

`enum FirstEnum                        // First enum type
{
Mem1,
Mem2
}

enum SecondEnum                        // Second enum type
{
Mem1,
Mem2
}    class Program
{
static void Main()
{
if (FirstEnum.Mem1 < FirstEnum.Mem2)  // OK--members of same enum type
Console.WriteLine("True");

if (FirstEnum.Mem1 < SecondEnum.Mem1) // Error--different enum types
Console.WriteLine("True");
}
}`

还有几个有用的方法 .NET Enum类型,enum基于该类型:

  • GetName The method accepts an enumeration type object and an integer, and returns the name of the corresponding enumeration member.
  • GetNames The method accepts an enumeration type object and returns the names of all members in the enumeration.

下面的代码显示了正在使用的每种方法的示例。注意,您必须使用typeof操作符来获取枚举类型对象。

`   enum TrafficLight
{
Green,
Yellow,
Red
}

class Program
{
static void Main()
{
Console.WriteLine( "Second member of TrafficLight is {0}\n",
Enum.GetName( typeof( TrafficLight ), 1 ) );

foreach ( var name in Enum.GetNames( typeof( TrafficLight ) ) )
Console.WriteLine( name );
}
}`

该代码产生以下输出:


`Second member of TrafficLight is Yellow

Green
Yellow
Red`


十二、数组

数组

数组是由单个变量名表示的一组统一的数据元素。使用变量名以及方括号中的一个或多个索引来访问各个元素,如下所示:

   Array name   Index ↓    ↓ **MyArray[4]**

定义

让我们从 C# 中与数组有关的一些重要定义开始。

  • 元素:一个数组的各个数据项称为元素。数组的所有元素必须属于同一类型或从同一类型派生。
  • 秩/维数:数组可以有任意正的维数。一个数组的维数叫做它的
  • 维度长度:一个数组的每一个维度都有一个长度,是该方向的位置数。
  • 数组长度:在所有维度中,一个数组包含的元素总数,称为数组的长度

重要细节

以下是一些关于 C# 数组的重要事实:

  • 一旦创建了一个数组,它的大小就固定了。C# 不支持动态数组。
  • 数组索引是从 0 开始的。也就是说,如果一个维度的长度是 n ,那么索引值的范围是从 0 到n–1。例如,图 12-1 显示了两个示例数组的尺寸和长度。请注意,对于每个维度,索引范围从 0 到长度–1。

Image

图 12-1 。尺寸和大小

数组的类型

C# 提供了两种数组:

  • 一维数组可以被认为是一行元素,或者是元素的向量。
  • 多维数组的组成使得主向量中的每个位置都是一个数组,称为子数组。子阵列向量中的位置本身可以是子阵列。

此外,还有两种类型的多维数组:矩形数组交错数组;它们具有以下特征:

  • 矩形阵列
    • 是多维数组,其中特定维中的所有子数组长度相同
    • 无论尺寸有多少,始终使用一组方括号int x = myArray2[4, 6, 1]        // One set of square brackets
  • 交错阵列
    • 是多维数组,其中每个子数组都是独立的数组
    • 可以具有不同长度的子阵列
    • 对数组的每个维度使用一组单独的方括号

jagArray1[2][7][4]                  // Three sets of square brackets

图 12-2 显示了 C# 中可用的数组类型。

Image

图 12-2 。一维、矩形和锯齿状数组

一个数组作为一个对象

数组实例是一个类型从类System.Array派生的对象。因为数组是从这个 BCL 基类派生的,所以它们从这个基类继承了许多有用的成员,例如:

  • Rank:返回数组维数的属性
  • Length:返回数组长度(元素总数)的属性

数组是引用类型,和所有引用类型一样,它们既有对数据的引用,也有对数据对象本身的引用。引用要么在栈上,要么在堆中,而数据对象本身总是在堆中。图 12-3 显示了一个数组的内存配置和组件。

Image

图 12-3 。数组的结构

尽管数组总是引用类型,但数组的元素可以是值类型,也可以是引用类型。

  • 如果存储的元素是值类型,则数组称为值类型数组
  • 如果存储在数组中的元素是引用类型对象的引用,则该数组称为引用类型数组

图 12-4 显示了一个值类型数组和一个引用类型数组。

Image

图 12-4 。元素可以是值或引用。

一维矩形数组

从语法上来说,一维数组和矩形数组非常相似,所以我将它们放在一起处理。然后,我将分别处理交错数组。

声明一维或矩形数组

若要声明一维或矩形数组,请在类型和变量名之间使用一组方括号。

等级说明符是括号之间的逗号。它们指定数组将具有的维数。排名是逗号的数量加一。例如,没有逗号表示一维数组,一个逗号表示二维数组,依此类推。

基本类型和等级说明符是数组的类型。例如,下面一行代码声明了一个一维数组long s。数组的类型是long[],读作“一个长整型数组”。

      Rank specifiers = 1 ↓ <ins>long[ ]</ins> secondArray; ↑ Array type

下面的代码显示了矩形数组声明的示例。请注意以下事项:

  • 您可以根据需要拥有任意多个等级说明符。
  • 不能在数组类型部分中放置数组维数长度。秩是数组类型的一部分,但是维度的长度是类型的一部分。
  • 当声明一个数组时,维数的是固定的。然而,维度的长度直到数组被实例化才被确定。

`      Rank specifiers

int[,,]   firstArray;                     // Array type: 3D array of int
int[,]    arr1;                           // Array type: 2D array of int
long[,,]  arr3;                           // Array type: 3D array of long

Array type

long[3,2,6] SecondArray;                  // Wrong!  Compile error
↑ ↑ ↑
Dimension lengths not allowed!`

Image 注意与 C/C++不同,在 C# 中,括号跟在基类型后面,而不是变量名后面。

实例化一维或矩形数组

要实例化一个数组,可以使用一个数组创建表达式。数组创建表达式由new操作符、基本类型和一对方括号组成。每个维度的长度放在括号之间的逗号分隔列表中。

以下是一维数组声明的示例。

  • 数组arr2是由四个int组成的一维数组。
  • 数组mcArr是四个MyClass引用的一维数组。

图 12-5 显示了它们在内存中的布局。

                                                      Four elements ↓ int[]     arr2  = new int[4]; MyClass[] mcArr = <ins>new MyClass[4]</ins>; ↑ Array-creation expression

下面是一个矩形阵列的示例。

  • 数组arr3是一个三维数组。
  • 数组的长度是 3 * 6 * 2 = 36。

图 12-5 显示了它在内存中的布局。

                                          Lengths of the dimensions <ins>    ↓    </ins> int[,,] arr3 = new int[3,6,2] ; Image

图 12-5 。声明和实例化数组

Image 注意与对象创建表达式不同,数组创建表达式不包含括号——即使对于引用类型的数组也是如此。

访问数组元素

使用整数值作为数组的索引来访问数组元素。

  • 每个维度都使用从 0 开始的索引。
  • 索引放在数组名称后面的方括号中。

下面的代码显示了声明、写入和读取一维和二维数组的示例:

`   int[]  intArr1 = new int[15];        // Declare 1D array of 15 elements.
intArr1[2]     = 10;                 // Write to element 2 of the array.
int var1       = intArr1[2];         // Read from element 2 of the array.

int[,] intArr2 = new int[5,10];      // Declare 2D array.
intArr2[2,3]   = 7;                  // Write to the array.
int var2       = intArr2[2,3];       // Read from the array.`

下面的代码显示了创建和访问一维数组的完整过程:

`   int[] myIntArray;                            // Declare the array.

myIntArray = new int[4];                     // Instantiate the array.

for( int i=0; i<4; i++ )                     // Set the values.
myIntArray[i] = i*10;

// Read and display the values of each element.
for( int i=0; i<4; i++ )  
Console.WriteLine("Value of element {0} = {1}", i, myIntArray[i]);`

该代码产生以下输出:


Value of element 0 is 0 Value of element 1 is 10 Value of element 2 is 20 Value of element 3 is 30


初始化一个数组

每当创建数组时,每个元素都会自动初始化为该类型的默认值。预定义类型的默认值为:整数类型为0,浮点类型为0.0,布尔类型为false,引用类型为null

例如,下面的代码创建一个数组,并将其四个元素初始化为值0。图 12-6 说明了内存中的布局。

   int[] intArr = new int[4]; Image

图 12-6 。一维数组的自动初始化

一维数组的显式初始化

对于一维数组,您可以通过在数组实例化的数组创建表达式后立即包含一个初始化列表来设置显式初始值。

  • 初始化值必须用逗号分隔,并用一组花括号括起来。
  • 维度长度是可选的,因为编译器可以根据初始化值的数量来推断长度。
  • 请注意,数组创建表达式和初始化列表之间没有任何分隔。也就是说,没有等号或其他连接运算符。

例如,下面的代码创建一个数组,并将其四个元素初始化为花括号之间的值。图 12-7 显示了内存中的布局。

                                                                  Initialization list <ins>              ↓              </ins> int[] intArr = new int[] { 10, 20, 30, 40 }; ↑ No connecting operator Image

图 12-7 。一维数组的显式初始化

矩形数组的显式初始化

要显式初始化矩形数组,需要遵循以下规则:

  • 初始值的每个向量必须用花括号括起来。
  • 每个维度也必须嵌套在花括号中。
  • 除了初始值,每个维度的初始化列表和组件也必须用逗号分隔。

例如,下面的代码显示了带有初始化列表的二维数组的声明。图 12-8 显示了内存中的布局。

                                                                          Initialization lists separated by commas ↓                ↓ int[,] intArray2 = new int[,] { {10, 1}, {2, 10}, {11, 9} } ; Image

***图 12-8。*初始化矩形阵列

初始化矩形数组的语法点

矩形数组是用嵌套的逗号分隔的初始化列表初始化的。初始化列表嵌套在花括号中。这有时会令人困惑,因此要正确使用嵌套、分组和逗号,请考虑以下提示:

  • 逗号被用作所有元素之间的分隔符
  • 逗号从不放在左花括号之间。
  • 逗号从不放在右花括号之前。
  • 如果可能的话,使用缩进和回车来排列这些组,这样它们在视觉上是不同的。
  • 从左到右阅读等级规范,将最后一个数字指定为“元素”,将所有其他数字指定为“组”

例如,将下面的声明读作“intArray有四组三组两个元素。”

                                                                               Initialization lists, nested and separated by commas int[,,] intArray = new int[4,3,2] {                   ↓                  ↓                    ↓ { {8, 6},  {5,  2}, {12, 9} }, { {6, 4},  {13, 9}, {18, 4} }, { {7, 2},  {1, 13}, {9,  3} }, { {4, 6},  {3,  2}, {23, 8} } };

快捷语法

当在单个语句中组合声明、数组创建和初始化时,可以完全省略语法中的数组创建表达式部分,只提供初始化部分。图 12-9 显示了这种快捷语法。

Image

图 12-9 。数组声明、创建和初始化的快捷方式

隐式类型化数组

到目前为止,我们已经在所有数组声明的开头明确指定了数组类型。但是,像其他局部变量一样,局部数组也可以是隐式类型的。这意味着:

  • 初始化数组时,可以让编译器从初始化器的类型中推断出数组的类型。只要所有的初始值设定项都可以隐式转换为单一类型,这是允许的。
  • 就像隐式类型的局部变量一样,使用关键字var代替数组类型。

下面的代码显示了三个数组声明的显式和隐式版本。第一组是一维数组int s .第二组是二维数组int s .第三组是字符串数组。注意,在隐式类型intArr4的声明中,您仍然需要在初始化中包含秩说明符。

           Explicit                           Explicit <ins>     ↓    </ins>                                    ↓ int [] intArr1 = new int[] { 10, 20, 30, 40 }; var    intArr2 = new    [] { 10, 20, 30, 40 }; ↑                                           ↑ Keyword                                 Inferred int[,] intArr3 = new int[,] { { 10, 1 }, { 2, 10 }, { 11, 9 } }; var    intArr4 = new    [,] { { 10, 1 }, { 2, 10 }, { 11, 9 } }; ↑ Rank specifier string[] sArr1 = new string[] { "life", "liberty", "pursuit of happiness" }; var      sArr2 = new       [] { "life", "liberty", "pursuit of happiness" };

把所有的东西放在一起

下面的代码把我们到目前为止看到的所有部分放在了一起。它创建、初始化并使用一个矩形数组。

`   // Declare, create, and initialize an implicitly typed array.
var arr = new int[,] {{0, 1, 2}, {10, 11, 12}};

// Print the values.
for( int i=0; i<2; i++ )
for( int j=0; j<3; j++ )
Console.WriteLine("Element [{0},{1}] is {2}", i, j, arr[i,j]);`

该代码产生以下输出:


Element [0,0] is 0 Element [0,1] is 1 Element [0,2] is 2 Element [1,0] is 10 Element [1,1] is 11 Element [1,2] is 12


参差阵列

交错数组是数组的数组。与矩形阵列不同,交错阵列的子阵列可以有不同数量的元素。

例如,下面的代码声明了一个二维交错数组。图 12-10 显示了内存中数组的布局。

  • 第一维的长度是 3。
  • 声明可以读作“jagArr是一个由三个int组成的数组。”
  • 注意,图中显示了四个数组对象——一个用于顶层数组,三个用于子数组。

int[][] jagArr = new int[3][];   // Declare and create top-level array. ...                  // Declare and create subarrays. Image

***图 12-10。*参差阵列是阵列中的阵列。

声明一个交错的数组

交错数组的声明语法要求每个维度都有一组单独的方括号。数组变量声明中的方括号组数决定了数组的秩。

  • 交错数组可以是大于 1 的任意维数。
  • 与矩形数组一样,维数长度不能包含在声明的数组类型部分中。

   Rank specifiers <ins>      ↓</ins> int[][]   SomeArr;             // Rank = 2 <ins>int[][][]</ins> OtherArr;            // Rank = 3 ↑               ↑ Array type        Array name

快捷方式实例化

您可以使用数组创建表达式将交错数组声明与第一级数组的创建结合起来,如下面的声明所示。图 12-11 显示了结果。

                                                Three subarrays ↓ int[][] jagArr = new int[3][]; Image

***图 12-11。*快捷方式一级实例化

在声明语句中,不能实例化超过一级的数组。

                                                      Allowed ↓ int[][] jagArr = new int[3][4];              // Wrong! Compile error ↑ Not allowed

实例化一个交错的数组

与其他类型的数组不同,您无法在一个步骤中完全实例化交错数组。因为交错数组是由独立数组组成的数组,所以每个数组都必须单独创建。实例化完整的交错数组需要以下步骤:

  1. 实例化顶级数组。
  2. 分别实例化每个子数组,将新创建的数组的引用分配给其包含数组的适当元素。

例如,以下代码显示了二维交错数组的声明、实例化和初始化。请注意,在代码中,对每个子数组的引用都被赋给了顶级数组中的一个元素。代码中的步骤 1 至 4 对应于图 12-12 中的编号表示。

`   int[][] Arr = new int[3][];                  // 1. Instantiate top level.

Arr[0] = new int[] {10, 20, 30};             // 2. Instantiate subarray.
Arr[1] = new int[] {40, 50, 60, 70};         // 3. Instantiate subarray.
Arr[2] = new int[] {80, 90, 100, 110, 120};  // 4. Instantiate subarray.` Image

***图 12-12。*创建二维交错数组

交错排列的子阵列

因为交错数组中的子数组本身就是数组,所以交错数组中可能有矩形数组。例如,下面的代码创建一个由三个二维矩形数组组成的交错数组,并用值初始化它们。然后显示这些值。图 12-13 说明了该结构。

该代码使用从System.Array继承的数组的GetLength(int n)方法来获取数组的指定维度的长度。

`   int[][,] Arr;         // An array of 2D arrays
Arr = new int[3][,];  // Instantiate an array of three 2D arrays.

Arr[0] = new int[,] { { 10,  20  },
{ 100, 200 } };

Arr[1] = new int[,] { { 30,  40,  50  },
{ 300, 400, 500 }  };

Arr[2] = new int[,] { { 60,  70,  80,  90  },
{ 600, 700, 800, 900 } };

↓ Get length of dimension 0 of Arr.
for (int i = 0; i < Arr.GetLength(0); i++)
{
↓ Get length of dimension 0 of Arr[ i ].
for (int j = 0; j < Arr[i].GetLength(0); j++)
{
↓ Get length of dimension 1 of Arr[ i ].
for (int k = 0; k < Arr[i].GetLength(1); k++)
{
Console.WriteLine
("[{0}][{1},{2}] = {3}", i, j, k, Arr[i][j, k]);
}
Console.WriteLine();
}
Console.WriteLine(
);
}`

这段代码产生以下输出:


`[0][1,0] = 100
[0][1,1] = 200

[1][0,0] = 30
[1][0,1] = 40
[1][0,2] = 50

[1][1,0] = 300
[1][1,1] = 400
[1][1,2] = 500

[2][0,0] = 60
[2][0,1] = 70
[2][0,2] = 80
[2][0,3] = 90

[2][1,0] = 600
[2][1,1] = 700
[2][1,2] = 800
[2][1,3] = 900`


Image

***图 12-13。*交错排列的三个二维数组

比较矩形和锯齿状数组

矩形和锯齿状数组的结构有很大的不同。例如,图 12-14 显示了一个 3 乘 3 的矩形数组的结构,以及一个由三个长度为 3 的一维数组组成的锯齿状数组。

  • 两个数组都包含九个整数,但是正如你所看到的,它们的结构是完全不同的。
  • 矩形数组只有一个数组对象,而交错数组有四个数组对象。

Image

***图 12-14。*比较矩形和锯齿状阵列的结构

一维数组在 CIL 中有特定的指令,允许它们针对性能进行优化。矩形阵列没有这些指令,也没有优化到相同的水平。因此,有时使用一维数组的交错数组(可以优化)比使用矩形数组(不能优化)更有效。

另一方面,对于矩形阵列来说,编程复杂度可以显著降低,因为它可以被视为单个单元,而不是阵列的阵列。

foreach 语句

foreach语句允许你顺序访问数组中的每个元素。它实际上是一个更通用的构造,因为它也适用于其他集合类型——但是在这一节中,我将只讨论它在数组中的使用。第十八章讲述了它与其他收藏类型的使用。

foreach声明的要点如下:

  • 迭代变量是与数组元素类型相同的临时变量。foreach语句使用迭代变量顺序表示数组中的每个元素。
  • foreach语句的语法如下所示,其中
    • Type是数组元素的类型。您可以显式地提供它的类型,或者您可以使用var并让它被编译器隐式地类型化和推断,因为编译器知道数组的类型。
    • Identifier是迭代变量的名称。
    • ArrayName是要迭代的数组的名称。
    • Statement是对数组中的每个元素执行一次的简单语句或块。

`       Explicitly typed iteration variable declaration
              ↓               
foreach( Type Identifier in ArrayName )
Statement

Implicitly typed iteration variable declaration
             ↓              
foreach( var Identifier in ArrayName )
Statement`

在下面的文本中,我有时会使用隐式类型,有时会使用显式类型,这样您就可以看到所使用的确切类型。但是形式在语义上是等价的。

foreach语句的工作方式如下:

  • 它从数组的第一个元素开始,并将该值赋给迭代变量
  • 然后,它执行语句体。在主体内部,可以使用迭代变量作为数组元素的只读别名。
  • 执行完主体后,foreach语句选择数组中的下一个元素,并重复这个过程。

这样,它在数组中循环,允许您逐个访问每个元素。例如,以下代码显示了一个包含四个整数的一维数组的foreach语句的用法:

  • WriteLine语句是foreach语句的主体,对数组的每个元素执行一次。
  • 第一次循环时,迭代变量item具有数组第一个元素的值。每一次,它都有数组中下一个元素的值。

   int[] arr1 = {10, 11, 12, 13}; Iteration variable declaration <ins>       ↓        </ins>                                  Iteration variable use foreach( int item in arr1 )                          ↓ Console.WriteLine("Item Value: {0}", item);

该代码产生以下输出:


Item Value: 10 Item Value: 11 Item Value: 12 Item Value: 13


迭代变量为只读

因为迭代变量的值是只读的,很明显它不能被改变。但是这对值类型数组和引用类型数组有不同的影响。

对于值类型数组,这意味着当数组的元素由迭代变量表示时,不能改变它。例如,在下面的代码中,试图更改迭代变量中的数据会产生一条编译时错误信息:

`   int[] arr1 = {10, 11, 12, 13};

foreach( int item in arr1 )
item++;     // Compilation error. Changing variable value is not allowed.`

对于引用类型数组,您仍然不能更改迭代变量,但是迭代变量只保存对数据的引用,而不是数据本身。因此,尽管您不能更改引用,但是您可以通过迭代变量更改数据

下面的代码创建一个由四个MyClass对象组成的数组,并初始化它们。在第一个foreach语句中,每个对象中的数据都发生了变化。在第二个foreach语句中,从对象中读取更改的数据。

`   class MyClass

class Program
{
static void Main()
{
MyClass[] mcArray = new MyClass[4];            // Create array.
for (int i = 0; i < 4; i++)
{
mcArray[i] = new MyClass();                 // Create class objects.
mcArray[i].MyField = i;                     // Set field.
}
foreach (MyClass item in mcArray)
item.MyField += 10;                         // Change the data.

foreach (MyClass item in mcArray)
Console.WriteLine("{0}", item.MyField);     // Read the changed data.
}
}`

该代码产生以下输出:


10 11 12 13


带有多维数组的 foreach 语句

在多维数组中,按照最右边的索引增加最快的顺序处理元素。当索引从 0 到长度–1 时,左边的下一个索引递增,右边的索引重置为 0。

矩形阵列示例

以下示例显示了用于矩形数组的foreach语句:

`   class Program
{
static void Main()
{
int total = 0;
int[,] arr1 = { {10, 11}, {12, 13} };

foreach( var element in arr1 )
{
total += element;
Console.WriteLine
("Element: {0}, Current Total: {1}", element, total);
}
}
}`

该代码产生以下输出:


Element: 10, Current Total: 10 Element: 11, Current Total: 21 Element: 12, Current Total: 33 Element: 13, Current Total: 46


交错排列的例子

因为交错数组是数组的数组,所以必须为交错数组中的每个维度使用单独的foreach语句。foreach语句必须正确嵌套,以确保每个嵌套数组都得到正确处理。

例如,在下面的代码中,第一个foreach语句循环遍历顶级数组arr1,选择下一个要处理的子数组。内部的foreach语句处理该子数组的元素。

`   class Program
{
static void Main( )
{
int total    = 0;
int[][] arr1 = new int[2][];
arr1[0]      = new int[] { 10, 11 };
arr1[1]      = new int[] { 12, 13, 14 };

foreach (int[] array in arr1)       // Process the top level.
{
Console.WriteLine("Starting new array");
foreach (int item in array)      // Process the second level.
{
total += item;
Console.WriteLine("  Item: {0}, Current Total: {1}", item, total);
}
}
}
}`

该代码产生以下输出:


Starting new array Item: 10, Current Total: 10 Item: 11, Current Total: 21 Starting new array Item: 12, Current Total: 33 Item: 13, Current Total: 46 Item: 14, Current Total: 60


阵列协方差

在某些情况下,即使对象不是数组的基类型,也可以将对象分配给数组元素。数组的这个属性叫做数组协方差。如果满足以下条件,则可以使用数组协方差:

  • 该数组是引用类型数组。
  • 您正在分配的对象的类型和数组的基类型之间存在隐式或显式转换。

因为在派生类和它的基类之间总是有一个隐式转换,所以你总是可以把一个派生类的对象赋给一个为基类声明的数组。

例如,下面的代码声明了两个类,AB,其中类B派生自类A。最后一行通过将类型为B的对象分配给类型为A的数组元素来显示协方差。图 12-15 显示了代码的内存布局。

`   class A                                         // Base class
class B : A                                     // Derived class

class Program {
static void Main() {
// Two arrays of type A[]
A[] AArray1 = new A[3];
A[] AArray2 = new A[3];

// Normal--assigning objects of type A to an array of type A
AArray1[0] = new A(); AArray1[1] = new A(); AArray1[2] = new A();

// Covariant--assigning objects of type B to an array of type A
AArray2[0] = new B(); AArray2[1] = new B(); AArray2[2] = new B();
}
}` Image

***图 12-15。*显示协方差的数组

Image 注意值类型数组没有协方差。

有用继承的数组成员

我之前提到过 C# 数组是从类System.Array派生的。它们从基类继承了许多有用的属性和方法。表 12-1 列出了一些最有用的方法。

Image

例如,下面的代码使用了其中的一些属性和方法:

`   public static void PrintArray(int[] a)
{
foreach (var x in a)
Console.Write("{0}  ", x);

Console.WriteLine("");
}

static void Main()
{
int[] arr = new int[] { 15, 20, 5, 25, 10 };
PrintArray(arr);

Array.Sort(arr);
PrintArray(arr);

Array.Reverse(arr);
PrintArray(arr);

Console.WriteLine();
Console.WriteLine("Rank = {0}, Length = {1}",arr.Rank, arr.Length);
Console.WriteLine("GetLength(0)     = {0}",arr.GetLength(0));
Console.WriteLine("GetType()        = {0}",arr.GetType());
}`

该代码产生以下输出:


`15  20  5  25  10
5  10  15  20  25
25  20  15  10  5

Rank = 1, Length = 5
GetLength(0)     = 5
GetType()        = System.Int32[]`


克隆方法

方法执行数组的浅层复制。这意味着它只创建阵列本身的克隆。如果它是一个引用类型数组,它不会而不是复制元素引用的对象。这对于值类型数组和引用类型数组有不同的结果。

  • 克隆值类型数组会产生两个独立的数组。
  • 克隆引用类型数组会导致两个数组指向相同的对象。

Clone方法返回一个类型为object的引用,该引用必须转换为数组类型。

   int[] intArr1 = { 1, 2, 3 }; Array type                 Returns an object <ins>    ↓   </ins>                        <ins>     ↓    </ins> int[] intArr2 = ( int[] ) intArr1.Clone();

例如,下面的代码显示了一个克隆值类型数组的示例,生成两个独立的数组。图 12-16 说明了代码中显示的步骤。

`   static void Main()
{
int[] intArr1 = { 1, 2, 3 };                             // Step 1
int[] intArr2 = (int[]) intArr1.Clone();                 // Step 2

intArr2[0] = 100; intArr2[1] = 200; intArr2[2] = 300;    // Step 3
}` Image

***图 12-16。*克隆一个值类型数组产生两个独立的数组。

克隆一个引用类型数组导致两个数组指向相同的对象。下面的代码显示了一个示例。图 12-17 说明了代码中显示的步骤。

`   class A

class Program
{
static void Main()
{
A[] AArray1 = new A[3] { new A(), new A(), new A() };     // Step 1
A[] AArray2 = (A[]) AArray1.Clone();                      // Step 2

AArray2[0].Value = 100;
AArray2[1].Value = 200;
AArray2[2].Value = 300;                                   // Step 3
}
}` Image

***图 12-17。*克隆一个引用类型数组会产生两个引用相同对象的数组。

比较数组类型

表 12-2 总结了三种类型数组之间的一些重要的相似和不同之处。

Image

十三、委托

什么是委托?

您可以将委托想象成一个包含一个或多个方法的对象。当然,通常你不会想到“执行”一个对象,但是委托不同于一个典型的对象。您可以执行委托,当您这样做时,它会执行它“持有”的一个或多个方法

在这一章中,我将解释创建和使用委托的语法和语义。在后面的章节中,你将看到如何使用委托将可执行代码从一个方法传递到另一个方法——以及为什么这是一件有用的事情。

我们将从下一页的示例代码开始。如果在这一点上一切都不完全清楚,不要担心,因为我将在本章的其余部分解释委托的细节。

  • 代码从名为MyDel的委托类型的声明开始。(是的,一个委托——不是一个委托型对象。我们很快就会谈到这一点。)
  • Program声明了三个方法:PrintLowPrintHighMain。我们很快将创建的委托对象将持有PrintLowPrintHigh方法——但是使用哪一个要到运行时才能确定。
  • Main声明了一个名为del的局部变量,它将保存一个对MyDel类型的委托对象的引用。这并没有创建对象——它只是创建了一个变量,该变量将保存对 delegate 对象的引用,该对象将在下面几行创建并赋给它。
  • 创建一个 .NET 类Random,这是一个随机数生成器类。然后程序调用对象的Next方法,用99作为它的输入参数。这将返回一个 0 到 99 之间的随机整数,并将该值存储在本地变量randomValue中;。
  • 下一行检查返回和存储的随机值是否小于 50。(注意,我们在这里使用三元条件操作符来返回一个或另一个委托对象。)
    • 如果值小于 50,它创建一个MyDel委托对象并初始化它以保存对PrintLow方法的引用。
    • 否则,它会创建一个包含对PrintHigh方法的引用的MyDel委托对象。
  • 最后,Main 执行del委托对象,委托对象执行它持有的方法(PrintLowPrintHigh)。

Image 注意如果你来自 C背景,理解委托的最快方法就是把它们想象成类型安全的、面向对象的 C函数指针。

`   delegate void MyDel(int value);   // Declare delegate TYPE.

class Program
{
void PrintLow( int value )
{
Console.WriteLine( "{0} - Low Value", value );
}

void PrintHigh( int value )
{
Console.WriteLine( "{0} - High Value", value );
}

static void Main( )
{
Program program = new Program();

MyDel   del;            // Declare delegate variable.

// Create random-integer-generator object and get a random
// number between 0 and 99.
Random  rand    = new Random();
int randomValue = rand.Next( 99 );

// Create a delegate object that contains either PrintLow or
// PrintHigh, and assign the object to the del variable.
del = randomValue < 50
? new MyDel( program.PrintLow  )
: new MyDel( program.PrintHigh );

del( randomValue );    // Execute the delegate.
}
}`

因为我们使用的是随机数生成器,所以程序在不同的运行中会产生不同的值。该程序的一次运行产生了以下输出:


28 - Low Value


委托概述

现在让我们进入细节。委托是用户定义的类型,就像类是用户定义的类型一样。但是,类表示数据和方法的集合,而委托则包含一个或多个方法以及一组预定义的操作。

您可以通过执行以下步骤来使用委托。我将在下面的小节中详细介绍这些步骤。

  1. 声明一个委托类型。委托声明看起来像方法声明,只是它没有实现块。
  2. 声明委托类型的委托变量。
  3. 创建一个委托类型的对象,并将其赋给委托变量。新的委托对象包含对一个方法的引用,该方法必须具有与第一步中定义的委托类型相同的签名和返回类型。
  4. 您可以选择将其他方法添加到委托对象中。这些方法必须具有与第一步中定义的委托类型相同的签名和返回类型。
  5. 在整个代码中,您可以调用该委托,就像它是一个方法一样。当您调用委托时,它包含的每个方法都会被执行。

在查看前面的步骤时,您可能已经注意到它们类似于创建和使用类的步骤。图 13-1 比较了创建和使用类和委托的过程。

Image

***图 13-1。*委托是用户定义的引用类型,就像类一样。

你可以把委托想象成一个对象,它包含一个有序的方法列表,这些方法具有相同的签名和返回类型,如图 13-2 所示。

  • 方法列表被称为调用列表
  • 委托持有的方法可以来自任何类或结构,只要它们符合以下两个中的*:*
    • 委托的返回类型
    • 委托签名(包括refout修饰符)
  • 调用列表中的方法可以是实例方法,也可以是静态方法。
  • 当委托被调用时,它的调用列表中的每个方法都被执行。

Image

图 13-2 。作为方法列表的委托

声明委托类型

正如我在上一节中所说的,委托是类型,就像类是类型一样。与类一样,在使用委托类型创建变量和该类型的对象之前,必须声明委托类型。下面的代码示例声明了一个委托类型:

        Keyword      Delegate type name ↓                      ↓ delegate void <ins>MyDel( int x )</ins>; ↑        ↑ Return type     Signature

委托类型的声明看起来很像方法的声明,因为它既有一个返回类型又有一个签名。返回类型和签名指定委托将接受的方法的形式。

前面的声明指定了类型为MyDel的委托对象将只接受具有单个int参数并且没有返回值的方法。图 13-3 显示了左边的委托类型和右边的委托对象。

Image

图 13-3 。委托类型和对象

委托类型声明在两个方面不同于方法声明。委托类型声明

  • 以关键字delegate开头
  • 没有方法体

Image 注意即使委托类型声明看起来像方法声明,它也不需要在类内声明,因为它是类型声明。

创建代理对象

委托是一种引用类型,因此既有引用又有对象。声明委托类型后,可以声明变量并创建该类型的对象。下面的代码显示了委托类型变量的声明:

   Delegate type     Variable ↓              ↓ MyDel  delVar;

有两种方法可以创建委托对象。第一种是使用带有new操作符的对象创建表达式,如下面的代码所示。new运算符的操作数由以下内容组成:

  • 委托类型名称。
  • 一组括号,包含用作调用列表中第一个成员的方法的名称。方法可以是实例方法,也可以是静态方法。

                                            Instance method <ins>             ↓             </ins> delVar = new MyDel( myInstObj.MyM1 );       // Create delegate and save ref. dVar   = new MyDel( <ins>SClass.OtherM2</ins> );       // Create delegate and save ref. ↑ Static method

您也可以使用快捷语法,它只包含方法说明符,如下面的代码所示。这段代码和前面的代码在语义上是等价的。使用快捷语法是可行的,因为在方法名和兼容的委托类型之间存在隐式转换。

   delVar = myInstObj.MyM1;          // Create delegate and save reference. dVar   = SClass.OtherM2;          // Create delegate and save reference.

例如,下面的代码创建了两个委托对象:一个使用实例方法,另一个使用静态方法。图 13-4 显示了代理的实例。这段代码假设有一个名为myInstObj的对象,它是一个类的实例,该类定义了一个名为MyM1的方法,该方法不返回值,并以一个int作为参数。它还假设有一个名为SClass的类,该类有一个静态方法OtherM2,其返回类型和签名与委托MyDel的返回类型和签名相匹配。

   delegate void MyDel(int x);               // Declare delegate type. MyDel delVar, dVar;                       // Create two delegate variables. Instance method <ins>     ↓     </ins> delVar = new MyDel( myInstObj.MyM1 );     // Create delegate and save ref. dVar   = new MyDel( <ins>SClass.OtherM2</ins> );     // Create delegate and save ref. ↑ Static method Image

图 13-4 。实例化委托

除了为委托分配内存之外,创建委托对象还会将第一个方法放在委托的调用列表中。

还可以使用初始化器语法,在同一语句中创建变量并实例化对象。例如,以下语句也会产生与图 13-4 中所示相同的配置:

   MyDel delVar = new MyDel( myInstObj.MyM1 ); MyDel dVar   = new MyDel( SClass.OtherM2 );

以下语句使用快捷语法,但同样产生如图 13-4 所示的结果:

   MyDel delVar = myInstObj.MyM1; MyDel dVar   = SClass.OtherM2;

分配委托

因为委托是引用类型,所以可以通过给委托变量赋值来更改委托变量中包含的引用。旧的委托对象将被垃圾收集器(GC)处理掉。

例如,下面的代码设置然后改变delVar的值。图 13-5 说明了代码。

`   MyDel delVar;
delVar = myInstObj.MyM1;   // Create and assign the delegate object.

...
delVar = SClass.OtherM2;   // Create and assign the new delegate object.` Image

图 13-5 。给委托变量赋值

组合委托

到目前为止,您看到的所有委托在其调用列表中都只有一个方法。可以使用加法运算符“组合”委托。该操作的结果是创建一个新的委托,其调用列表是两个操作数委托的调用列表副本的串联。

例如,下面的代码创建三个委托。第三个委托是由前两个委托组合而成的。

`   MyDel delA = myInstObj.MyM1;
MyDel delB = SClass.OtherM2;

MyDel delC = delA + delB;                  // Has combined invocation list`

尽管术语组合委托可能给人一种操作数委托被修改的印象,但它们根本没有改变。事实上,委托是不可改变的。委托对象创建后,不能更改。

图 13-6 展示了前面代码的结果。注意,操作数委托保持不变。

Image

图 13-6 。组合委托

向代理添加方法

尽管您在上一节中看到了委托实际上是不可变的,但是 C# 提供了语法,使您看起来可以使用+=操作符向委托添加方法。

例如,下面的代码将两个方法“添加”到委托的调用列表中。这些方法被添加到调用列表的底部。图 13-7 显示了结果。

   MyDel delVar  = inst.MyM1;     // Create and initialize. delVar       += SCl.m3;        // Add a method. delVar       += X.Act;         // Add a method. Image

图 13-7 。向委托“添加”方法的结果。实际上,因为委托是不可变的,所以调用列表中有三个方法的结果委托是一个由变量指向的全新委托。

当然,实际发生的是,当使用+=操作符时,一个新的委托被创建,调用列表是左边的委托和右边列出的方法的组合。这个新的委托然后被分配给delVar变量。

可以多次向委托添加方法。每次添加它时,它都会在调用列表中创建一个新元素。

从委托中删除方法

您也可以使用-=操作符从委托中删除一个方法。下面一行代码显示了操作符的用法。图 13-8 显示了该代码应用于图 13-7 中所示委托时的结果。

   delVar -= SCl.m3;             // Remove the method from the delegate. Image

图 13-8 。从委托中移除方法的结果

与向委托添加方法一样,产生的委托实际上是一个新委托。新委托是旧委托的副本,但是它的调用列表不再包含对被移除的方法的引用。

以下是删除方法时要记住的一些事情:

  • 如果一个方法在调用列表中有多个条目,那么-=操作符将从列表的底部开始搜索,并删除找到的第一个匹配方法的实例。
  • 尝试删除不在调用列表中的方法没有任何效果。
  • 试图调用空委托会引发异常。您可以通过将委托与null进行比较来检查委托的调用列表是否为空。如果调用列表为空,则委托为null

调用委托

你通过调用委托来调用它,就好像它只是一个方法一样。用于调用委托的参数用于调用调用列表上的每个方法(除非其中一个参数是输出参数,我将很快介绍这一点)。

例如,如下面的代码所示,委托delVar接受一个整数输入值。用参数调用委托会导致它用相同的参数值(本例中为 55)调用其调用列表中的每个成员。图 13-9 说明了调用。

   MyDel delVar  = inst.MyM1; delVar       += SCl.m3; delVar       += X.Act; ... delVar( 55 );                              // Invoke the delegate. ... Image

***图 13-9。*当委托被调用时,它执行它的调用列表中的每个方法,使用与调用它时相同的参数。

如果一个方法不止一次出现在调用列表中,那么当委托被调用时,每次在列表中遇到该方法都会被调用。

委托示例

下面的代码定义并使用了一个没有参数和返回值的委托。请注意以下关于代码的内容:

  • Test定义了两个打印功能。
  • 方法Main创建委托的一个实例,然后再添加三个方法。
  • 然后程序调用委托,委托调用它的方法。然而,在调用委托之前,它检查以确保它不是null

`   // Define a delegate type with no return value and no parameters.
delegate void PrintFunction();

class Test
{
public void Print1()
{ Console.WriteLine("Print1 -- instance"); }

public static void Print2()
{ Console.WriteLine("Print2 -- static"); }
}

class Program
{
static void Main()
{
Test t = new Test();    // Create a test class instance.
PrintFunction pf;       // Create a null delegate.

pf = t.Print1;          // Instantiate and initialize the delegate.

// Add three more methods to the delegate.
pf += Test.Print2;
pf += t.Print1;
pf += Test.Print2;
// The delegate now contains four methods.

if( null != pf )           // Make sure the delegate isn't null.
pf();                   // Invoke the delegate.
else
Console.WriteLine("Delegate is empty");
}
}`

该代码产生以下输出:


Print1 -- instance Print2 -- static Print1 -- instance Print2 -- static


调用带有返回值的委托

如果委托在其调用列表中有一个返回值和多个方法,则会发生以下情况:

  • 调用列表中最后一个方法返回的值是委托调用返回的值。
  • 调用列表中所有其他方法的返回值都被忽略。

例如,下面的代码声明了一个返回int值的委托。创建委托的一个对象并添加两个额外的方法。然后,它调用WriteLine语句中的委托,并打印其返回值。图 13-10 显示了代码的图形表示。

`delegate int MyDel( );                 // Declare delegate with return value.
class MyClass {
int IntValue = 5;
public int Add2() { IntValue += 2; return IntValue;}
public int Add3() { IntValue += 3; return IntValue;}
}

class Program {
static void Main( ) {
MyClass mc = new MyClass();
MyDel mDel = mc.Add2;          // Create and initialize the delegate.
mDel += mc.Add3;               // Add a method.
mDel += mc.Add2;               // Add a method.
Console.WriteLine("Value: {0}", mDel() );
}

}                       Invoke the delegate and use the return value.`

该代码产生以下输出:


Value: 12


Image

图 13-10 。最后执行的方法的返回值是委托返回的值。

用引用参数调用委托

如果委托有一个引用参数,该参数的值可以在从调用列表中的一个或多个方法返回时更改。

  • 当调用调用列表中的下一个方法时,参数的新值——而不是初始值—是传递给下一个方法的值。

例如,下面的代码调用带有引用参数的委托。图 13-11 说明了代码。

`   delegate void MyDel( ref int X );

class MyClass
{
public void Add2(ref int x) { x += 2; }
public void Add3(ref int x) { x += 3; }
static void Main()
{
MyClass mc = new MyClass();

MyDel mDel = mc.Add2;
mDel += mc.Add3;
mDel += mc.Add2;

int x = 5;
mDel(ref x);

Console.WriteLine("Value: {0}", x);
}
}`

该代码产生以下输出:


Value: 12


Image

图 13-11 。引用参数的值可以在调用之间改变。

匿名方法

到目前为止,您已经看到可以使用静态方法或实例方法来实例化委托。无论哪种情况,方法本身都可以从代码的其他部分显式调用,当然,必须是某个类或结构的成员。

但是,如果该方法只使用一次——来实例化委托,那该怎么办呢?在这种情况下,除了创建委托的语法要求之外,并不真正需要单独的命名方法。匿名方法允许您省去单独的命名方法。

  • 匿名方法是在实例化委托时内联声明的方法。

例如,图 13-12 显示了同一个类的两个版本。左边的版本声明并使用了一个名为Add20的方法。右边的版本使用匿名方法。两个版本的无阴影代码是相同的。

Image

图 13-12 。比较命名方法和匿名方法

图 13-12 中的两组代码产生以下输出:


25 26


使用匿名方法

您可以在以下位置使用匿名方法:

  • 声明委托变量时作为初始值设定项表达式。
  • 组合委托时位于赋值语句的右侧。
  • 在赋值语句的右侧,向事件添加委托。第十四章报道事件。

匿名方法的语法

匿名方法表达式的语法包括以下组件:

  • 键入关键字delegate
  • 参数列表,如果语句块不使用任何参数,可以省略
  • 语句块,它包含匿名方法的代码

                                Parameter Keyword               list                         Statement block ↓                     ↓                     <ins>                     ↓                     </ins> delegate ( Parameters )  { ImplementationCode }

返回类型

匿名方法不显式声明返回类型。但是,实现代码本身的行为必须通过返回该类型的值来匹配委托的返回类型。如果委托的返回类型为void,那么匿名方法代码不能返回值。

例如,在下面的代码中,委托的返回类型是int。因此,匿名方法的实现代码必须在通过代码的所有路径上返回一个int

`              Return type of delegate type

delegate int OtherDel(int InParam);

static void Main()
{
OtherDel del = delegate(int x)
{
return x + 20 ;                   // Returns an int
};
...
}`

参数

除了数组参数之外,匿名方法的参数列表必须与委托的参数列表在以下三个方面相匹配:

  • 参数数量
  • 参数的类型和位置
  • 修饰语

您可以通过将括号留空或完全省略来简化匿名方法的参数列表,但前提是以下两个都为真:

  • 委托的参数列表不包含任何out参数。
  • 匿名方法不使用任何参数。

例如,下面的代码声明了一个没有任何out参数的委托和一个不使用任何参数的匿名方法。因为这两个条件都满足,所以可以从匿名方法中省略参数列表。

   delegate void SomeDel ( int X );                 // Declare the delegate type.    SomeDel SDel = delegate                          // Parameter list omitted { PrintMessage(); Cleanup(); };

params 参数

如果委托声明的参数列表包含一个params参数,那么params关键字将从匿名方法的参数列表中省略。例如,在下面的代码中:

  • 委托类型声明将最后一个参数指定为params类型参数。
  • 然而,匿名方法的参数列表必须省略params关键字。

              params keyword used in delegate type declaration ↓ delegate void SomeDel( int X, params int[] Y); params keyword omitted in matching anonymous method ↓ SomeDel mDel = delegate (int X, int[] Y) { ... };

变量和参数的范围

匿名方法中声明的参数和局部变量的作用域被限制在实现代码的主体内,如图图 13-13 所示。

例如,下面的匿名方法定义了参数y和局部变量z。匿名方法的主体关闭后,yz不再在作用域内。代码的最后一行会产生一个编译错误。

Image

图 13-13 。变量和参数的范围

外部变量

与委托的命名方法不同,匿名方法可以访问局部变量及其周围的环境。

  • 来自周围作用域的变量称为外部变量
  • 匿名方法的实现代码中使用的外部变量被称为由该方法捕获的。

例如,图 13-14 中的代码显示了在匿名方法之外定义的变量x。然而,方法中的代码可以访问x并打印其值。

Image

图 13-14 。使用外部变量

延长捕获变量的生命周期

只要捕获的外部变量的捕获方法是委托的一部分,该变量就会保持活动状态,即使该变量通常会超出范围。

例如,图 13-15 中的代码说明了一个被捕获变量生命周期的延长。

  • 局部变量x在块内声明并初始化。
  • 委托mDel然后被实例化,使用一个匿名方法捕获外部变量x
  • 当块关闭时,x超出范围。
  • 如果块结束后的WriteLine语句被取消注释,将会导致编译错误,因为它引用了x,而后者现在超出了范围。
  • 然而,委托mDel中的匿名方法在其环境中维护x,并在mDel被调用时打印其值。

Image

图 13-15 。匿名方法中捕获的变量

图中的代码产生以下输出:


Value of x: 5


λ表达式

C# 2.0 引入了匿名方法,我们刚才已经讨论过了。然而,匿名方法的语法有些冗长,并且需要编译器本身已经知道的信息。C# 3.0 没有要求您包含这些冗余信息,而是引入了 lambda 表达式,减少了匿名方法的语法。你可能想要使用 lambda 表达式而不是匿名方法。事实上,如果 lambda 表达式被首先引入,就不会有匿名方法。

在匿名方法语法中,delegate关键字是多余的,因为编译器已经可以看出您正在将方法分配给委托。通过执行以下操作,可以轻松地将匿名方法转换为 lambda 表达式:

  • 删除delegate关键字。
  • 将 lambda 运算符=>放在参数列表和匿名方法体之间。lambda 运算符读作“goes to”

下面的代码展示了这种转换。第一行显示了一个匿名方法被分配给变量del。第二行显示了相同的匿名方法,在被转换成 lambda 表达式后,被赋给变量le1

   MyDel del = delegate(int x)    { return x + 1; } ;     // Anonymous method MyDel le1 =         (int x) => { return x + 1; } ;     // Lambda expression

Image 术语λ表达式来源于数学家阿隆佐·邱奇等人在 20 世纪二三十年代发展起来的λ演算。lambda 演算是一个表示函数的系统,使用希腊字母 lambda()来表示一个无名函数。最近,函数式编程语言(如 Lisp 及其方言)使用该术语来表示表达式,这些表达式可用于直接描述函数的定义,而不是为函数命名。

这个简单的转换不太冗长,看起来更干净,但是它只节省了六个字符。然而,编译器可以推断出更多的东西,从而允许您进一步简化 lambda 表达式,如下面的代码所示。

  • 从委托的声明中,编译器也知道委托参数的类型,因此 lambda 表达式允许您省略参数类型,如对le2的赋值所示。
    • 与其类型一起列出的参数称为显式类型化的
    • 那些没有列出类型的被称为隐式类型
  • 如果只有一个隐式类型的参数,可以去掉括号,如对le3的赋值所示。
  • 最后,lambda 表达式允许表达式的主体是语句块或者表达式。如果语句块包含一个 return 语句,可以用跟在关键字return后面的表达式来替换语句块,如对le4的赋值所示。

   MyDel del = delegate(int x)    { return x + 1; } ;     // Anonymous method MyDel le1 =         (int x) => { return x + 1; } ;     // Lambda expression MyDel le2 =             (x) => { return x + 1; } ;     // Lambda expression MyDel le3 =              x  => { return x + 1; } ;     // Lambda expression MyDel le4 =              x  =>          x + 1    ;     // Lambda expression

lambda 表达式的最终形式大约只有原始匿名方法的四分之一,而且更清晰、更容易理解。

下面的代码展示了完整的转换。Main的第一行显示了一个匿名方法被分配给变量del。第二行显示了同样的匿名方法,在被转换成 lambda 表达式后,被赋给变量le1

`   delegate double MyDel(int par);

class Program
{   
static void Main()
{
MyDel del = delegate(int x)    { return x + 1; } ;  // Anonymous method

MyDel le1 =         (int x) ⇒ { return x + 1; } ;  // Lambda expression
MyDel le2 =             (x) ⇒ { return x + 1; } ;
MyDel le3 =              x  ⇒ { return x + 1; } ;
MyDel le4 =              x  ⇒          x + 1    ;

Console.WriteLine("{0}", del (12));
Console.WriteLine("{0}", le1 (12));  Console.WriteLine("{0}", le2 (12));
Console.WriteLine("{0}", le3 (12));  Console.WriteLine("{0}", le4 (12));
}
}`

该代码产生以下输出:


13 13 13 13 13


关于 lambda 表达式参数表的一些要点如下:

  • lambda 表达式的参数列表中的参数在数量、类型和位置上必须与委托的参数匹配。
  • 表达式的参数列表中的参数不必包含类型(即,它们是隐式类型化的),除非委托有refout参数——在这种情况下,类型是必需的(即,它们是显式类型化的)。
  • 如果只有一个参数,并且是隐式类型的,则可以省略括号。否则,它们是必需的。
  • 如果没有参数,则必须使用一组空括号。

图 13-16 显示了 lambda 表达式的语法。

Image

***图 13-16。*lambda 表达式的语法由 lambda 运算符组成,左边是参数部分,右边是 lambda 主体。

十四、事件

发布者和订阅者

许多程序中的一个常见需求是,当一个特定的程序事件发生时,程序的其他部分需要被通知该事件已经发生。

满足这个需求的一种模式叫做发布者/订阅者模式。在这种模式中,一个名为发布者的类定义了一组程序的其他部分可能感兴趣的事件。当这些事件发生时,其他类可以“注册”得到发布者的通知。这些订阅者类通过向发布者提供一个方法来“注册”通知。当事件发生时,发布者“引发事件”,订阅者提交的所有方法都被执行。

订阅者提供的方法被称为回调方法,因为发布者通过执行它们的方法“回调订阅者”。它们也被称为事件处理程序,因为它们是被调用来处理事件的代码。图 14-1 展示了这个过程,显示了一个事件的发布者和该事件的三个订阅者。

Image

图 14-1 。发布者和订阅者

以下是一些与事件相关的重要术语:

  • Publisher: 发布事件的类或结构,以便在事件发生时通知其他类。
  • Subscriber :一个类或结构,当事件发生时,它注册以得到通知。
  • 事件处理程序:由订阅者向发布者注册的方法,当发布者引发事件时执行。事件处理程序方法可以在与事件相同的类或结构中声明,也可以在不同的类或结构中声明。
  • 引发事件:是调用触发事件的术语。当引发一个事件时,所有用该事件注册的方法都会被调用。

前一章讨论了代表。事件的许多方面与代表的相似。事实上,事件就像一个更简单的委托,专门用于特定的用途。代表和事件的行为有很好的相似性。一个事件包含一个私有委托,如图图 14-2 所示。

关于事件的私人代表,需要了解的重要事项如下:

  • 一个事件给出了对其私有控制的委托的结构化访问。也就是说,您不能直接访问该委托。
  • 可用的操作比委托少。对于事件,您只能添加和移除事件处理程序,以及调用事件。
  • 当事件被引发时,它调用委托,委托依次调用其调用列表中的方法。

注意在图 14-2 中,只有+=-=操作符突出到事件框的左侧。这是因为它们是事件上唯一允许的操作(除了调用事件本身)。

Image

***图 14-2。*一个事件有一个封装的委托。

图 14-3 展示了一个名为Incrementer的程序,它执行某种计数。

  • Incrementer定义了一个名为CountedADozen的事件,每当它计算另外十几个项目时就会引发这个事件。
  • 订户类DozensSomeOtherClass都有一个用CountedADozen事件注册的事件处理程序。
  • 每次引发事件时,都会调用处理程序。

Image

***图 14-3。*一个事件类的结构和术语

源代码组件概述

使用事件需要五段代码。这些在图 14-4 中进行了说明。我将在接下来的章节中逐一介绍它们。这些代码如下所示:

  • 委托类型声明:事件和事件处理程序必须有共同的签名和返回类型,由委托类型描述。
  • 事件处理程序声明:这些是订阅者类中的声明,当事件被引发时,这些方法将被执行。这些不必是显式命名的方法;它们也可以是匿名方法或 lambda 表达式,如第十三章所述。
  • 事件声明:发布者类必须声明一个订阅者可以注册的事件成员。当一个类声明了一个public事件时,就说是发布了事件
  • 事件注册:订阅者必须注册一个事件,以便在事件发生时得到通知。这是将事件处理程序连接到事件的代码。
  • 引发事件的代码:这是发布者中“激发”事件的代码,导致它调用所有向它注册的事件处理程序。

Image

***图 14-4。*使用事件的五个源代码组件

宣告一个事件

发布者必须提供事件对象。创建事件很简单——它只需要一个委托类型和一个名称。事件声明的语法如下面的代码所示,它声明了一个名为CountedADozen的事件。请注意关于事件CountedADozen的以下内容:

  • 事件是在类中声明的。
  • 它需要委托类型的名称。任何附加到事件的事件处理程序(即注册到事件)都必须在签名和返回类型上与委托类型相匹配。
  • 它被声明为public,以便其他类和结构可以向它注册事件处理程序。
  • 您不能对事件使用对象创建表达式(一个new表达式)。

   class Incrementer {                      Keyword                            Name of event ↓                                           ↓       public event EventHandler CountedADozen; ↑   Delegate type

通过使用逗号分隔的列表,可以在声明语句中声明多个事件。例如,以下语句声明了三个事件:

   public event EventHandler <ins>MyEvent1, MyEvent2, OtherEvent</ins>; ↑ Three events

您还可以通过包含static关键字使事件成为静态的,如下面的声明所示:

public static event EventHandler CountedADozen; ↑                            Keyword

一个事件是一个成员

一个常见的错误是认为事件是一种类型,但事实并非如此。像方法或属性一样,事件是类或结构的成员,这有几个重要的分支:

  • 因为事件是成员
    • 不能在可执行代码块中声明事件。
    • 它必须在类或结构中用其他成员声明。
  • 事件成员与其他成员一起被隐式地自动初始化为null

要声明一个事件,您必须提供一个委托类型的名称。您可以声明一个或者使用一个已经存在的。如果声明委托类型,它必须指定将由事件注册的方法的签名和返回类型。

BCL 声明了一个名为EventHandler的委托,专门用于系统事件。我将在本章的后面描述EventHandler委托。

订阅一个事件

订阅者向事件添加事件处理程序。对于要添加到事件中的事件处理程序,该处理程序必须与事件的委托具有相同的返回类型和签名。

  • 使用+=运算符向事件添加事件处理程序,如下面的代码所示。事件处理程序放在操作符的右边。
  • 事件处理程序规范可以是以下任何一种:
    • 实例方法的名称
    • 静态方法的名称
    • 匿名方法
    • λ表达式

例如,下面的代码向事件CountedADozen添加了三个方法。第一个是实例方法。第二种是静态方法。第三种是实例方法,使用委托形式。

       Class instance                                           Instance method ↓                                                                ↓                   incrementer.CountedADozen += IncrementDozensCount;        // Method reference form incrementer.CountedADozen += <ins>ClassB.CounterHandlerB</ins>;      // Method reference form ↑                                           ↑   Event member                           Static method mc.CountedADozen += new EventHandler(cc.CounterHandlerC);  // Delegate form

就像委托一样,您可以使用匿名方法和 lambda 表达式来添加事件处理程序。例如,下面的代码首先使用 lambda 表达式,然后使用匿名方法。

`   // Lambda expression
incrementer.CountedADozen += () ⇒ DozensCount++;

// Anonymous method
incrementer.CountedADozen += delegate { DozensCount++; };`

引发一个事件

事件成员本身只保存需要调用的事件处理程序。除非引发事件,否则它们不会发生任何事情。您需要确保在适当的时候有代码可以做到这一点。

例如,以下代码引发事件CountedADozen。请注意以下关于代码的内容:

  • 在引发事件之前,代码将它与null进行比较,看它是否包含任何事件处理程序。如果事件为null,则为空,无法执行。
  • 引发事件的语法与调用方法的语法相同:
    • 使用事件的名称,后跟用括号括起来的参数列表。
    • 参数列表必须与事件的委托类型匹配。

   if (CountedADozen != null)              // Make sure there are methods to execute. CountedADozen (<ins>source, args</ins>);        // Raise the event. ↑                           ↑   Event name                   Parameter list

将事件声明和引发事件的代码放在一起,为发布者提供了以下类声明。代码包含两个成员:事件和一个名为DoCount的方法,该方法在适当的时候引发事件。

`   class Incrementer
{
public event EventHandler CountedADozen;   // Declare the event.

void DoCount(object source, EventArgs args)
{
for( int i=1; i < 100; i++ )
if( i % 12 == 0 )
if (CountedADozen != null)       // Make sure there are methods to execute.
CountedADozen(source, args);
}                                           ↑
Raise the event.
}`

图 14-5 中的代码显示了整个程序,发布者类Incrementer和订阅者类Dozens。关于代码需要注意的事项如下:

  • 在其构造函数中,类Dozens订阅事件,提供方法IncrementDozensCount作为其事件处理程序。
  • 在类Incrementer的方法DoCount中,每当该方法再增加 12 次时,就会引发事件CountedADozen

Image

**图 14-5。**一个完整的程序,有一个发布者和一个订阅者,展示了使用一个事件所需的五段代码

图 14-5 中的代码产生以下输出:


Number of dozens = 8


标准事件用法

GUI 编程是事件驱动的,这意味着当程序运行时,它可以随时被诸如按钮点击、按键或系统定时器之类的事件中断。当这种情况发生时,程序需要处理事件,然后继续它的进程。

显然,这种程序事件的异步处理是使用 C# 事件的最佳场合。Windows GUI 编程广泛使用事件,因此有一个标准 .NET 框架模式来使用它们。事件使用的标准模式的基础是在System名称空间中声明的EventHandler委托类型。下面一行代码显示了EventHandler委托类型的声明。关于声明需要注意的事项如下:

  • 第一个参数用于保存对引发事件的对象的引用。它的类型是object,因此可以匹配任何类型的任何实例。
  • 第二个参数用于保存适用于应用的任何类型的状态信息。
  • 返回类型为void

   public delegate void EventHandler(object sender, EventArgs e);

EventHandler委托类型中的第二个参数是类EventArgs的一个对象,它在System命名空间中声明。您可能会想,既然第二个参数是用来传递数据的,那么一个EventArgs类对象应该能够存储某种类型的数据。你就错了。

  • EventArgs类被设计成不携带数据。它用于不需要传递数据的事件处理程序,通常被它们忽略。
  • 如果你想传递数据,你必须声明一个从EventArgs派生出的类*,用适当的字段来保存你想传递的数据。*

尽管EventArgs类实际上并不传递数据,但它是使用EventHandler委托模式的重要组成部分。这些类型为objectEventArgs的参数是用作参数的任何实际类型的基类。这允许EventHandler委托提供一个签名,它是所有事件和事件处理程序的最小公分母,允许所有事件正好有两个参数,而不是每个事件有不同的签名。

如果我们修改Incrementer程序来使用EventHandler委托,我们就有了如图图 14-6 所示的程序。请注意以下关于代码的内容:

  • 委托Handler的声明已被删除,因为事件使用系统定义的EventHandler委托。
  • subscriber 类中事件处理程序声明的签名必须与事件委托的签名(和返回类型)相匹配,事件委托现在使用类型为objectEventArgs的参数。在事件处理程序IncrementDozensCount的情况下,该方法只是忽略了形式参数。
  • 引发事件的代码必须使用适当参数类型的对象来调用事件。

Image

***图 14-6。*increment er 程序修改为使用系统定义的 EventHandler 委托

通过扩展 EventArgs 传递数据

要在事件处理程序的第二个参数中传递数据并遵守标准约定,您需要声明一个从EventArgs派生的自定义类,它可以存储您需要传递的数据。类名应该以EventArgs结尾。例如,下面的代码声明了一个自定义类,它可以在名为Message的字段中存储一个字符串:

                                    Custom class name              Base class ↓                                ↓    public class IncrementerEventArgs : EventArgs { public int IterationCount { get; set; }  // Stores an integer }

现在,您已经有了一个用于在事件处理程序的第二个参数中传递数据的自定义类,您需要一个使用新自定义类的委托类型。要实现这一点,请使用委托的通用版本EventHandler<>。第十七章详细介绍了 C# 泛型,所以现在你只能看着。若要使用泛型委托,请执行下列操作,如后续代码所示:

  • 将自定义类的名称放在尖括号之间。
  • 在应该使用自定义委托类型名称的地方使用整个字符串。例如,event声明应该是这样的:

                                       Generic delegate using custom class <ins>                                ↓                               </ins> public event EventHandler<IncrementerEventArgs> CountedADozen; ↑              Event name

在处理事件的其他四段代码中使用自定义类和自定义委托。例如,下面的代码更新了Incrementer代码,以使用名为IncrementerEventArgs的定制EventArgs类和通用EventHandler<IncrementerEventArgs>委托。

`public class IncrementerEventArgs : EventArgs   // Custom class derived from EventArgs
{
public int IterationCount { get; set; }      // Stores an integer
}

class Incrementer      Generic delegate using custom class
{                              ↓               
public event EventHandler CountedADozen;

public void DoCount()
Object of custom class
{
↓     
IncrementerEventArgs args = new IncrementerEventArgs();
for ( int i=1; i < 100; i++ )
if ( i % 12 == 0 && CountedADozen != null )
{
args.IterationCount = i;                CountedADozen( this, args );
}                     ↑
}                                      Pass parameters when raising the event
}

class Dozens
{
public int DozensCount { get; private set; }

public Dozens( Incrementer incrementer )
{
DozensCount = 0;
incrementer.CountedADozen += IncrementDozensCount;
}

void IncrementDozensCount( object source, IncrementerEventArgs e )
{
Console.WriteLine( "Incremented at iteration: {0} in {1}",
e.IterationCount, source.ToString() );
DozensCount++;
}
}

class Program
{
static void Main()
{
Incrementer incrementer = new Incrementer();
Dozens dozensCounter    = new Dozens( incrementer );

incrementer.DoCount();
Console.WriteLine( "Number of dozens = {0}",
dozensCounter.DozensCount );
}
}`

这个程序产生下面的输出,它显示了调用它时的迭代和源对象的完全限定类名。我将在第二十一章中讨论全限定类名。


Incremented at iteration: 12 in Counter.Incrementer Incremented at iteration: 24 in Counter.Incrementer Incremented at iteration: 36 in Counter.Incrementer Incremented at iteration: 48 in Counter.Incrementer Incremented at iteration: 60 in Counter.Incrementer Incremented at iteration: 72 in Counter.Incrementer Incremented at iteration: 84 in Counter.Incrementer Incremented at iteration: 96 in Counter.Incrementer Number of dozens = 8


删除事件处理程序

当您完成一个事件处理程序时,您可以将它从事件中移除。使用-=操作符从事件中移除事件处理程序,如下面的代码行所示:

   p.SimpleEvent -= s.MethodB;;         // Remove handler MethodB.

例如,以下代码向事件SimpleEvent添加两个处理程序,然后引发事件。每个处理程序都被调用并打印出一行文本。然后从事件中移除MethodB处理程序,当事件再次被引发时,只有MethodB处理程序打印出一行。

`   class Publisher
{
public event EventHandler SimpleEvent;

public void RaiseTheEvent() { SimpleEvent( this, null ); }
}

class Subscriber
{
public void MethodA( object o, EventArgs e ) { Console.WriteLine( "AAA" ); }
public void MethodB( object o, EventArgs e ) { Console.WriteLine( "BBB" ); }
}

class Program
{
static void Main( )
{
Publisher  p = new Publisher();
Subscriber s = new Subscriber();

p.SimpleEvent += s.MethodA;
p.SimpleEvent += s.MethodB;
p.RaiseTheEvent();

Console.WriteLine( "\r\nRemove MethodB" );
p.SimpleEvent -= s.MethodB;
p.RaiseTheEvent();
}
}`

该代码产生以下输出:


`AAA
BBB

Remove MethodB
AAA`


如果一个处理程序在一个事件中注册了多次,那么当您发出删除该处理程序的命令时,只会从列表中删除该处理程序的最后一个实例。

事件访问器

本章的最后一个主题是事件访问器。我前面提到过,+=-=操作符是事件中唯一允许的操作符。这些操作符具有你在本章中已经看到的定义良好的行为。

但是,可以改变这些操作符的行为,让事件在使用它们时执行您喜欢的任何自定义代码。然而,这是一个高级的话题,所以我在这里只提一下,不涉及太多的细节。

若要更改这些运算符的操作,必须为事件定义事件访问器。

  • 有两个访问器:addremove
  • 带有访问器的事件声明看起来类似于属性声明。

下面的示例显示了带有访问器的事件声明的形式。这两个访问器都有一个名为value的隐式值参数,它接受对实例方法或静态方法的引用。

`   public event EventHandler CountedADozen
{
add
{
...                            // Code to implement the =+ operator
}

remove
{
...                            // Code to implement the -= operator
}
}`

当声明事件访问器时,事件不包含嵌入的委托对象。您必须实现自己的存储机制来存储和移除用事件注册的方法。

事件访问器充当 void 方法,这意味着它们不能使用返回值的 return 语句。

十五、接口

什么是接口?

一个接口指定一组函数成员但不实现它们的引用类型。这留给了实现接口的类和结构。这种描述听起来很抽象,所以让我首先向您展示接口有助于解决的问题,以及它是如何解决的。

以下面的代码为例。如果你看看类Program中的方法Main,你会看到它创建并初始化了类CA的一个对象,并将该对象传递给方法PrintInfoPrintInfo需要一个类型为CA的对象,并打印出类对象中包含的信息。

`   class CA
{
public string Name;
public int    Age;
}

class CB
{
public string First;
public string Last;
public double PersonsAge;
}

class Program
{
static void PrintInfo( CA item ) {
Console.WriteLine( "Name: {0}, Age {1}", item.Name, item.Age );
}

static void Main() {
CA a = new CA() { Name = "John Doe", Age = 35 };
PrintInfo( a );
}
}`

只要你给方法传递类型为CA的对象,方法PrintInfo就能很好地工作,但是如果你给它传递类型为CB的对象,它就不能工作(也显示在上面的代码中)。然而,假设方法PrintInfo中的算法非常有用,以至于您希望能够将其应用于许多不同类的对象。

有几个原因使它不能与当前的代码一起工作。首先,PrintInfo的形参指定实参必须是类型为CA的对象,所以传入类型为CB或任何其他类型的对象都会产生编译错误。但是,即使我们可以绕过这个障碍,以某种方式传入类型为CB的对象,我们仍然会有一个问题,因为CB的结构不同于CA的结构。它的字段和CA有不同的名称和类型,PrintInfo对这些字段一无所知。

但是,如果我们能够以这样一种方式创建类,它们可以被成功地传递给PrintInfo,并且PrintInfo能够处理它们,而不管类的结构如何,那会怎么样呢?接口使这成为可能。

图 15-1 中的代码通过使用一个接口解决了这个问题。您还不需要了解细节,但一般来说,它会执行以下操作:

  • 首先,它声明了一个名为IInfo的接口,该接口包含两个方法——GetNameGetAge——每个方法返回一个string
  • CACB分别通过在其基类列表中列出接口IInfo来实现接口,然后实现接口所需的两个方法。
  • Main然后创建CACB的实例,并将它们传递给PrintInfo
  • 因为类实例实现了接口,PrintInfo可以调用方法,并且每个类实例执行它在类声明中定义的方法。

Image

***图 15-1。*使用一个接口使 PrintInfo 方法可以被任意数量的类使用

该代码产生以下输出:


Name: John Doe, Age 35 Name: Jane Doe, Age 33


使用 IComparable 接口的例子

现在,您已经看到了一些通过接口解决的问题,我们将看第二个例子,并进行更详细的讨论。首先看一下下面的代码,它接受一个未排序的整数数组,并按升序对它们进行排序。该代码执行以下操作:

  • 第一行创建了一个由五个没有特定顺序的整数组成的数组。
  • 第二行使用Array类的静态Sort方法对元素进行排序。
  • foreach循环将它们打印出来,显示整数现在是升序的。

`   var myInt = new [] { 20, 4, 16, 9, 2 };    // Create an array of ints.

Array.Sort(myInt);                         // Sort elements by magnitude.

foreach (var i in myInt)                   // Print them out.
Console.Write("{0} ", i);`

该代码产生以下输出:


2 4 9 16 20


Array类的Sort方法显然在一组int上工作得很好,但是如果你试图在你自己的一个类上使用它,会发生什么呢,如下所示?

`   class MyClass                            // Declare a simple class.
{
public int TheValue;
}
...
MyClass[] mc = new MyClass[5];          // Create an array of five elements.
...                                  // Create and initialize the elements.

Array.Sort(mc);                         // Try to use Sort--raises exception.`

当您尝试运行这段代码时,它会引发一个异常,而不是对元素进行排序。Sort不能与MyClass对象数组一起工作的原因是它不知道如何比较用户定义的对象以及如何排列它们的顺序。数组类的Sort方法依赖于一个名为IComparable的接口,该接口在 BCL 中声明。IComparable有一个叫CompareTo的单一方法。

下面的代码显示了IComparable接口的声明。注意,接口体包含方法CompareTo的声明,指定它接受类型object的单个参数。同样,尽管该方法有名称、参数和返回类型,但没有实现。相反,实现由分号表示。

                       Keyword      Interface name ↓                    ↓           public interface IComparable { int CompareTo( object obj ); } ↑ Semicolon in place of method implementation

图 15-2 显示了界面IComparableCompareTo方法以灰色显示,说明它不包含实现。

Image

图 15-2 。接口 IComparable 的表示

尽管接口声明没有提供方法CompareTo的实现。接口IComparable的. NET 文档描述了当你创建一个实现接口的类或结构时,这个方法应该做什么。它说当方法CompareTo被调用时,它应该返回下列值之一:

  • 如果当前对象小于参数对象,则为负值
  • 如果当前对象大于参数对象,则为正值
  • 如果在比较中认为两个对象相等,则为零

Sort使用的算法取决于它可以使用元素的CompareTo方法来确定两个元素的顺序。int类型实现了IComparable,但是MyClass没有,所以当Sort试图调用MyClass的不存在的CompareTo方法时,它会引发一个异常。

通过让类实现IComparable,你可以让Sort方法处理MyClass类型的对象。要实现接口,类或结构必须做两件事:

  • 它必须在其基类列表中列出接口名称。
  • 它必须为接口的每个成员提供一个实现。

例如,下面的代码更新MyClass来实现接口IComparable。请注意以下关于代码的内容:

  • 接口的名称列在类声明的基类列表中。
  • 该类实现了一个名为CompareTo的方法,其参数类型和返回类型与接口成员相匹配。
  • 实现方法CompareTo是为了满足接口文档中给出的定义。也就是说,它返回负 1、正 1 或 0,具体取决于它的值与传递到方法中的对象的比较。

`               Interface name in base class list
↓                   
class MyClass : IComparable
{
public int TheValue;

public int CompareTo(object obj)   // Implementation of interface method
{
MyClass mc = (MyClass)obj;
if (this.TheValue < mc.TheValue) return -1;
if (this.TheValue > mc.TheValue) return  1;
return 0;
}
}`

图 15-3 显示了更新后的类别。从阴影接口方法指向类方法的箭头表示接口方法不包含代码,而是由类级方法实现的。

Image

***图 15-3。*在 MyClass 中实现 I comparable

现在MyClass实现了IComparable , Sort会很好地处理它。顺便说一下,仅仅声明CompareTo方法是不够的——它必须是实现接口的一部分,这意味着将接口名称放在基类列表中。

下面显示了完整的更新代码,现在可以使用Sort方法对一组MyClass对象进行排序。Main创建并初始化一个MyClass对象的数组,然后打印出来。然后它调用Sort并再次打印出来,显示它们已经被排序。

`   class MyClass : IComparable                     // Class implements interface.
{
public int TheValue;
public int CompareTo(object obj)             // Implement the method.
{
MyClass mc = (MyClass)obj;
if (this.TheValue < mc.TheValue) return -1;
if (this.TheValue > mc.TheValue) return 1;
return 0;
}
}

class Program
{
static void PrintOut(string s, MyClass[] mc)
{
Console.Write(s);
foreach (var m in mc)
Console.Write("{0} ", m.TheValue);
Console.WriteLine("");
}

static void Main()
{
var myInt = new [] { 20, 4, 16, 9, 2 };

MyClass[] mcArr = new MyClass[5];     // Create array of MyClass objs.
for (int i = 0; i < 5; i++)           // Initialize the array.
{
mcArr[i] = new MyClass();
mcArr[i].TheValue = myInt[i];
}
PrintOut("Initial Order:  ", mcArr);  // Print the initial array.
Array.Sort(mcArr);                    // Sort the array.
PrintOut("Sorted Order:   ", mcArr);  // Print the sorted array.
}
}`

该代码产生以下输出:


Initial Order:  20 4 16 9 2 Sorted Order:   2 4 9 16 20


声明一个接口

上一节使用了一个已经在 BCL 中声明的接口。在这一节中,您将看到如何声明接口。关于声明接口,需要知道的重要事情如下:

  • 接口声明不能包含以下内容:
  • 数据成员
  • 静态成员
  • 接口声明只能包含以下类型的非静态函数成员的声明:
    • 方法
    • 性能
    • 事件
    • 索引器
  • 这些函数成员的声明不能包含任何实现代码。相反,必须用分号来代替每个成员声明的主体。
  • 按照惯例,接口名称以大写的 I 开头(例如ISaveable)。
  • 像类和结构一样,接口声明也可以被分成部分接口声明,如第六章的“部分类和部分类型”一节所述。

下面的代码展示了一个用两个方法成员声明一个接口的例子:

    Keyword      Interface name ↓                   ↓                 interface IMyInterface1                                     Semicolon in place of body {                                             ↓ int    DoStuff     ( int nVar1, long lVar2 ); double DoOtherStuff( string s, long x ); }                                        ↑ Semicolon in place of body

接口的可访问性和接口成员的可访问性之间有一个重要的区别:

  • 一个接口声明可以有任意的访问修饰符publicprotectedinternalprivate
  • 然而,接口的成员是隐式公共的,并且不允许有访问修饰符,包括public

Access modifiers are allowed on interfaces. ↓      public interface IMyInterface2 { private int Method1( int nVar1, long lVar2 );          // Error }     ↑ Access modifiers are NOT allowed on interface members.

实现一个接口

只有类或结构可以实现接口。如Sort示例所示,要实现一个接口,一个类或结构必须

  • 在其基类列表中包含接口的名称
  • 为接口的每个成员提供实现

例如,下面的代码显示了类MyClass的一个新声明,它实现了上一节中声明的接口IMyInterface1。请注意,接口名称列在基类列表中的冒号后面,并且该类为接口成员提供了实际的实现代码。

`                              Colon   Interface name
↓              ↓                         
class MyClass: IMyInterface1
{
int    DoStuff     ( int nVar1, long lVar2 )
                                          // Implementation code

double DoOtherStuff( string s, long x )
                                          // Implementation code
}`

关于实现接口需要知道的一些重要事情如下:

  • 如果一个类实现了一个接口,它必须实现该接口的所有成员。
  • 如果一个类是从基类派生的,并且还实现了接口,基类的名称必须在基类列表中列在任何接口之前,如下所示。(请记住,只能有一个基类,因此列出的任何其他类型都必须是接口的名称。)

                            Base class must be first         Interface names ↓              <ins>                          ↓                                            </ins> class Derived : MyBaseClass, IIfc1, IEnumerable, IComparable { ... }

简单界面示例

下面的代码声明了一个名为IIfc1的接口,其中包含一个名为PrintOut的方法。类MyClass通过在其基类列表中列出接口IIfc1并提供一个名为PrintOut的方法来实现接口IIfc1,该方法与接口成员的签名和返回类型相匹配。Main创建该类的一个对象,并从该对象调用方法。

`   interface IIfc1   Semicolon in place of body              // Declare interface.
{                        ↓
void PrintOut(string s);
}
Implement interface

class MyClass : IIfc1                                // Declare class.
{
public void PrintOut(string s)                    // Implementation
{
Console.WriteLine("Calling through:  {0}", s);
}
}

class Program
{
static void Main()
{
MyClass mc = new MyClass();                    // Create instance.
mc.PrintOut("object");                         // Call method.
}
}`

该代码产生以下输出:


Calling through:  object


一个接口是一个引用类型

接口不仅仅是要实现的类或结构的成员列表。它是一个参考类型。

您不能通过类对象的成员直接访问接口。然而,您可以通过将类对象引用转换为接口类型来获得对接口的引用。一旦有了对接口的引用,就可以对引用使用点语法表示法来调用接口成员。

例如,下面的代码显示了一个从类对象引用获取接口引用的示例。

  • 在第一条语句中,变量mc是对实现接口IIfc1的类对象的引用。该语句将该引用转换为对接口的引用,并将其赋给变量ifc
  • 第二条语句使用对接口的引用来调用实现方法。

      Interface      Cast to interface ↓                     ↓      IIfc1 ifc = (IIfc1) mc;              // Get ref to interface. ↑                          ↑ Interface ref        Class object ref <ins>ifc.PrintOut</ins> ("interface");          // Use ref to interface to call member. ↑             Use dot-syntax notation to call through the interface reference.

例如,下面的代码声明了一个接口和一个实现该接口的类。Main中的代码创建了类的一个对象,并通过类对象调用实现方法。它还创建一个接口类型的变量,将类对象的引用强制转换为接口类型,并通过对接口的引用调用实现方法。图 15-4 说明了类和对接口的引用。

`   interface IIfc1
{
void PrintOut(string s);
}

class MyClass: IIfc1
{
public void PrintOut(string s)
{
Console.WriteLine("Calling through:  {0}", s);
}
}

class Program
{
static void Main()
{
MyClass mc = new MyClass();  // Create class object.
mc.PrintOut("object");       // Call class object implementation method.

IIfc1 ifc = (IIfc1)mc;       // Cast class object ref to interface ref.
ifc.PrintOut("interface");   // Call interface method.
}
}`

该代码产生以下输出:


Calling through:  object Calling through:  interface


Image

图 15-4 。类对象的引用和接口的引用

使用 as 运算符与接口

在上一节中,您看到了可以使用 cast 操作符来获取对对象接口的引用。更好的想法是使用as操作符。在第十六章中详细介绍了as操作符,但是我在这里也要提到它,因为它是一个与接口一起使用的好选择。

如果您试图将类对象引用强制转换为该类未实现的接口的引用,则强制转换操作将引发异常。您可以通过使用as操作符来避免这个问题。它的工作原理如下:

  • 如果类别实作介面,运算式会传回介面的参考。
  • 如果类没有实现接口,表达式返回null而不是引发异常。(异常是代码中的意外错误。我将在第二十二章中详细讨论异常——但是你应该避免异常,因为它们会显著降低代码速度,并会使程序处于不一致的状态。)

下面的代码演示了as操作符的用法。第一行使用as操作符从一个类对象获得一个接口引用。表达式的结果将b的值设置为null或对ILiveBirth接口的引用。

第二行检查b的值,如果不是null,则执行调用接口成员方法的命令。

               Class object ref      Interface name ↓                 ↓               ILiveBirth b = a as ILiveBirth;        // Acts like cast: (ILiveBirth)a ↑            ↑ Interface    Operator ref if (b != null) Console.WriteLine("Baby is called: {0}", b.BabyCalled());

实现多个接口

在到目前为止展示的例子中,这些类已经实现了一个接口。

  • 一个类或结构可以实现任意数量的接口。
  • 所有实现的接口都必须在基类列表中列出,并用逗号分隔(跟在基类名称后面,如果有的话)。

例如,下面的代码展示了类MyData,它实现了两个接口:IDataStoreIDataRetrieve。图 15-5 展示了MyData类中多个接口的实现。

`   interface IDataRetrieve { int GetData(); }             // Declare interface.
interface IDataStore    { void SetData( int x ); }     // Declare interface.
Interface             Interface
↓                       ↓                
class MyData: IDataRetrieve, IDataStore                // Declare class.
{
int Mem1;                                           // Declare field.
public int  GetData()        { return Mem1; }
public void SetData( int x )
}

class Program
{
static void Main()                                  // Main
{
MyData data = new MyData();
data.SetData( 5 );
Console.WriteLine("Value = {0}", data.GetData());
}
}`

该代码产生以下输出:


Value = 5


Image

***图 15-5。*实现多个接口的类

用重复成员实现接口

因为一个类可以实现任意数量的接口,所以两个或更多的接口成员可能具有相同的签名和返回类型。那么,编译器如何处理这种情况呢?

例如,假设您有两个接口——IIfc1IIfc2——如下所示。每个接口都有一个名为PrintOut的方法,具有相同的签名和返回类型。如果要创建一个实现这两个接口的类,应该如何处理这些重复的接口方法?

`   interface IIfc1
{
void PrintOut(string s);
}

interface IIfc2
{
void PrintOut(string t);
}`

答案是,如果一个类实现多个接口,其中几个接口的成员具有相同的签名和返回类型,则该类可以实现一个成员,该成员满足包含该重复成员的所有接口。

例如,下面的代码显示了类MyClass的声明,它实现了IIfc1IIfc2。它的方法PrintOut的实现满足了两个接口的需求。

`   class MyClass : IIfc1, IIfc2             // Implement both interfaces.
{
public void PrintOut(string s)        // Single implementation for both
{
Console.WriteLine("Calling through:  {0}", s);
}
}

class Program
{
static void Main()
{
MyClass mc = new MyClass();
mc.PrintOut("object");
}
}`

这段代码产生以下输出:


Calling through:  object


图 15-6 展示了由单个类级方法实现实现的重复接口方法。

Image

***图 15-6。*同一个类成员实现的多个接口

引用多个接口

您之前已经看到接口是引用类型,您可以通过使用as操作符或者通过将对象引用强制转换为接口类型来获得对接口的引用。如果一个类实现了多个接口,你可以为每个接口获得单独的引用。

例如,下面的类用一个方法PrintOut实现了两个接口。Main中的代码以三种方式调用方法PrintOut:

  • 通过类对象
  • 通过引用IIfc1接口
  • 通过引用IIfc2接口

图 15-7 说明了类对象以及对IIfc1IIfc2的引用。

`interface IIfc1                              // Declare interface.
{
void PrintOut(string s);
}

interface IIfc2                              // Declare interface
{
void PrintOut(string s);
}

class MyClass : IIfc1, IIfc2                 // Declare class.
{
public void PrintOut(string s)
{
Console.WriteLine("Calling through:  {0}", s);
}
}    class Program
{
static void Main()
{
MyClass mc = new MyClass();

IIfc1 ifc1 = (IIfc1) mc;                 // Get ref to IIfc1.
IIfc2 ifc2 = (IIfc2) mc;                 // Get ref to IIfc2.

mc.PrintOut("object");                  // Call through class object.

ifc1.PrintOut("interface 1");           // Call through IIfc1.
ifc2.PrintOut("interface 2");           // Call through IIfc2.
}
}`

该代码产生以下输出:


Calling through:  object Calling through:  interface 1 Calling through:  interface 2


Image

***图 15-7。*对类中不同接口的单独引用

作为实现的一个继承成员

实现接口的类可以从其基类之一继承实现的代码。例如,下面的代码阐释了一个从基类继承实现代码的类。

  • IIfc1是一个带有名为PrintOut的方法成员的接口。
  • MyBaseClass包含一个名为PrintOut的方法,它匹配IIfc1的方法声明。
  • Derived有一个空的声明体,但它是从类MyBaseClass派生的,并且在其基类列表中包含IIfc1
  • 即使Derived的声明体为空,基类中的代码也满足实现接口方法的要求。

`   interface IIfc1 { void PrintOut(string s); }

class MyBaseClass                                   // Declare base class.
{
public void PrintOut(string s)                   // Declare the method.
{
Console.WriteLine("Calling through:  {0}", s);
}
}
class Derived : MyBaseClass, IIfc1                 // Declare class.

class Program {
static void Main()
{
Derived d = new Derived();                    // Create class object.
d.PrintOut("object.");                        // Call method.
}
}`

图 15-8 说明了前面的代码。注意从IIfc1开始的箭头向下指向基类中的代码。

Image

图 15-8 。在基类中实现

显式接口成员实现

在前面的章节中你已经看到,一个类可以实现多个接口所需的所有成员,如图 15-5 和 15-6 所示。

但是,如果您希望每个接口都有单独的实现,该怎么办呢?在这种情况下,您可以创建所谓的显式接口成员实现。显式接口成员实现具有以下特征:

  • 像所有接口实现一样,它被放在实现接口的类或结构中。
  • 它是使用一个限定的接口名声明的,该接口名由接口名和成员名组成,用点分隔。

下面的代码显示了声明显式接口成员实现的语法。由MyClass实现的两个接口中的每一个都实现了自己版本的方法PrintOut

`   class MyClass : IIfc1, IIfc2
{                  Qualified interface name
               ↓                
void IIfc1.PrintOut (string s)                  // Explicit implementation

void IIfc2.PrintOut (string s)                  // Explicit implementation

}`

图 15-9 说明了类和接口。请注意,表示显式接口成员实现的方框没有以灰色显示,因为它们现在表示实际的代码。

Image

图 15-9 。显式接口成员实现

例如,在下面的代码中,类MyClass为两个接口的成员声明了显式接口成员实现。注意,在这个例子中,只有显式的接口成员实现。没有类级别的实现。

`   interface IIfc1 { void PrintOut(string s); }   // Declare interface.
interface IIfc2 { void PrintOut(string t); }   // Declare interface.

class MyClass : IIfc1, IIfc2
{
Qualified interface name
             ↓                
void IIfc1.PrintOut(string s)            // Explicit interface member
{                                        //     implementation
Console.WriteLine("IIfc1:  {0}", s);
}
Qualified interface name
             ↓                
void IIfc2.PrintOut(string s)            // Explicit interface member
{                                        //     implementation
Console.WriteLine("IIfc2:  {0}", s);
}
}

class Program
{
static void Main()
{
MyClass mc = new MyClass();           // Create class object.

IIfc1 ifc1 = (IIfc1) mc;              // Get reference to IIfc1.
ifc1.PrintOut("interface 1");         // Call explicit implementation.

IIfc2 ifc2 = (IIfc2) mc;              // Get reference to IIfc2.
ifc2.PrintOut("interface 2");         // Call explicit implementation.
}
}`

该代码产生以下输出:


IIfc1:  interface 1 IIfc2:  interface 2


图 15-10 说明了代码。请注意图中的接口方法并不指向类级别的实现,而是包含它们自己的代码。

Image

***图 15-10。*引用具有显式接口成员实现的接口

当有一个显式接口成员实现时,类级实现是允许的,但不是必需的。显式实现满足了类或结构必须实现方法的要求。因此,您可以选择以下三种实施方案中的任何一种:

  • 类级实现
  • 显式接口成员实现
  • 类级和显式接口成员实现

访问显式接口成员实现

显式接口成员实现只能通过对接口的引用来访问。这意味着即使其他类成员也不能直接访问它们。

例如,下面的代码显示了类MyClass的声明,它通过显式实现实现了接口IIfc1。注意,即使是同为MyClass成员的Method1,也不能直接访问显式实现。

  • Method1的前两行产生编译错误,因为该方法试图直接访问实现。
  • 只有Method1中的最后一行会编译,因为它将对当前对象(this)的引用强制转换为对接口类型的引用,并使用该接口引用来调用显式接口实现。

`   class MyClass : IIfc1
{
void IIfc1.PrintOut(string s)       // Explicit interface implementation
{
Console.WriteLine("IIfc1");
}

public void Method1()
{
PrintOut("...");                 // Compile error
this.PrintOut("...");            // Compile error

((IIfc1)this).PrintOut("...");   // OK, call method.
}       ↑
Cast to a reference to the interface
}`

这种限制对继承有重要的影响。由于其他类成员不能直接访问显式接口成员实现,从该类派生的类成员显然也不能直接访问它们。它们必须总是通过对接口的引用来访问。

接口可以继承接口

您之前看到接口实现可以从基类继承。但是接口本身可以从一个或多个其他接口继承。

  • 要指定一个接口从其他接口继承,请将基接口的名称放在逗号分隔的列表中,并放在接口声明中接口名称后面的冒号后面,如下所示:                                        Colon           Base interface list ↓   <ins>                         ↓                       </ins>                interface IDataIO : IDataRetrieve, IDataStore { ...
  • 与基类列表中只能有一个类名的类不同,接口在其基类列表中可以有任意数量的接口。
    • 列表中的接口本身可以有继承的接口。
    • 结果接口包含它声明的所有成员,以及它的所有基接口。

图 15-11 中的代码展示了三个接口的声明。接口IDataIO继承了前两个。右图显示了包含其他两个接口的IDataIO

Image

图 15-11 。接口继承多个接口的类

不同类实现一个接口的例子

下面的代码演示了已经讨论过的接口的几个方面。程序声明了一个名为Animal的类,它被用作其他几个代表各种动物的类的基类。它还声明了一个名为ILiveBirth的接口。

CatDogBird都是从基类Animal派生出来的。CatDog都实现了ILiveBirth接口,但是类Bird没有。

Main中,程序创建一个Animal对象的数组,并用三种动物类中每一种的类对象填充它。然后程序遍历数组,使用as操作符,检索每个对象的ILiveBirth接口的引用,并调用它的BabyCalled方法。

`interface ILiveBirth                           // Declare interface.
{
string BabyCalled();
}

class Animal                                // Base class Animal

class Cat : Animal, ILiveBirth                 // Declare class Cat.
{
string ILiveBirth.BabyCalled()
{ return "kitten"; }
}
class Dog : Animal, ILiveBirth                 // Declare class Dog.
{
string ILiveBirth.BabyCalled()
{ return "puppy"; }
}

class Bird : Animal                            // Declare class Bird.
   class Program
{
static void Main()
{
Animal[] animalArray = new Animal[3];   // Create Animal array.
animalArray[0] = new Cat();             // Insert Cat class object.
animalArray[1] = new Bird();            // Insert Bird class object.
animalArray[2] = new Dog();             // Insert Dog class object.
foreach( Animal a in animalArray )      // Cycle through array.
{
ILiveBirth b = a as ILiveBirth;      // if implements ILiveBirth...
if (b != null)
Console.WriteLine("Baby is called: {0}", b.BabyCalled());
}
}
}`

该代码产生以下输出:


Baby is called: kitten Baby is called: puppy


图 15-12 说明了内存中的数组和对象。

Image

图 15-12 。基类 Animal 的不同对象类型散布在数组中。

十六、转换策略

什么是转换?

为了理解什么是转换,让我们从考虑一个简单的例子开始,在这个例子中,你声明了两个不同类型的变量,然后将其中一个变量的值()赋给另一个变量(目标)。在赋值之前,源值必须转换为目标类型的值。图 16-1 说明了类型转换。

  • 转换是取一种类型的值,用它作为另一种类型的等值的过程。
  • 转换得到的值应该与源值相同,但属于目标类型。

Image

***图 16-1。*类型转换

例如,图 16-2 中的代码显示了两个不同类型变量的声明。

  • var1的类型是short,一个初始化为5的 16 位有符号整数。var2属于sbyte类型,一个 8 位有符号整数,被初始化为值10
  • 代码的第三行将var1的值赋给var2。因为这是两种不同的类型,所以在赋值之前,var1的值必须转换成与var2相同类型的值。这是使用 cast 表达式执行的,您很快就会看到。
  • 还要注意的是,var1的值和类型没有改变。虽然它被称为转换,但这仅意味着源值被用作目标类型的值,而不是源值被更改为目标类型。

Image

图 16-2 。从 short 转换为 sbyte

隐式转换

对于某些类型的转换,不会丢失数据或精度。例如,很容易将一个 8 位值填充到一个 16 位类型中,而不会丢失数据。

  • 该语言将自动为您完成这些转换。这些被称为隐式转换
  • 当从具有较少位的源类型转换到具有较多位的目标类型时,目标中的额外位需要用 0 或 1 来填充。
  • 当从较小的无符号类型转换为较大的无符号类型时,目标的额外最高有效位用 0 填充。这叫做零延伸

图 16-3 显示了一个 8 位值 10 转换成 16 位值 10 的零扩展的例子。

Image

图 16-3 。无符号转换中的零扩展

对于有符号类型之间的转换,额外的最高有效位用源表达式的符号位填充。

  • 这保持了转换值的正确符号和大小。
  • 这被称为符号扩展,如图图 16-4 所示,首先是 10,然后是–10。

Image

***图 16-4。*带符号转换中的符号扩展

显式转换和强制转换

当您从较短的类型转换为较长的类型时,较长的类型很容易保存较短类型的所有位。但是,在其他情况下,目标类型可能无法在不丢失数据的情况下容纳源值。

例如,假设您想将一个ushort值转换成一个byte

  • A ushort可以保存 0 到 65,535 之间的任何值。
  • 一个byte只能保存一个 0 到 255 之间的值。
  • 只要你要转换的ushort值小于 256,就不会有任何数据丢失。然而,如果它更大,最高有效位将会丢失。

例如,图 16-5 显示试图将值为 1365 的ushort转换为byte,导致数据丢失。并非源值的所有有效位都适合目标类型,这会导致溢出和数据丢失。源值是 1,365,但目标可以容纳的最大值是 255。字节中的结果值是 85,而不是 1,365。

Image

图 16-5 。试图将 ushort 转换为字节

显然,在可能的无符号 16 位ushort值中,只有相对较少的一部分(0.4%)可以安全地转换为无符号 8 位byte类型,而不会丢失数据。其余的导致数据溢出,产生不同的值。

铸造

对于预定义的类型,C# 将自动从一种数据类型转换为另一种数据类型,但只在那些在源类型和目标类型之间不可能丢失数据的类型之间转换。也就是说,如果源类型的任何值在转换为目标类型时会丢失数据,那么该语言不提供两种类型之间的自动转换。如果你想进行这种类型的转换,你必须使用一个叫做转换表达式显式转换*。*

下面的代码显示了一个强制转换表达式的示例。它将var1的值转换为类型sbyte。强制转换表达式由以下内容组成:

  • 包含目标类型名称的一组匹配括号
  • 括号后面的源表达式

                   Target type ↓               (sbyte) var1; ↑ Source expression

当使用强制转换表达式时,您明确地承担了执行可能丢失数据的操作的责任。本质上,你是在说,“尽管有数据丢失的可能性,我知道我在做什么,所以无论如何都要进行转换。”(不过,要确保你 do 知道自己在做什么。)

例如,图 16-6 显示了将类型ushort的两个值转换为类型byte的转换表达式。在第一种情况下,没有数据丢失。在第二种情况下,最高有效位丢失,给出的值为 85,这显然不等于源值 1,365。

Image

***图 16-6。*将一个 ushort 强制转换为一个字节

图中代码的输出显示了结果的十进制和十六进制值,如下所示:


sb:  10 = 0xA sb:  85 = 0x55


转换类型

对于数值和引用类型,有许多标准的预定义转换。这些类别在图 16-7 中说明。

  • 除了标准转换之外,还可以为用户定义的类型定义隐式和显式转换。
  • 还有一种预定义的转换类型,称为装箱,它将任何值类型转换为以下两种类型之一:
    • 类型object
    • 类型System.ValueType
  • 取消装箱会将装箱的值转换回其原始类型。

Image

***图 16-7。*转换类型

数字转换

任何数值类型都可以转换成任何其他数值类型,如图图 16-8 所示。有些转换是隐式转换,有些必须是显式转换。

Image

图 16-8 。数字转换

隐式数值转换

隐式数值转换如图 16-9 中的所示。

  • 如果存在从源类型到目标类型的路径,则存在从源类型到目标类型的隐式转换。
  • 任何没有从源类型到目标类型的箭头路径的数字转换都必须是一个显式转换

如图所示,正如您所料,占用较少位的数值类型与占用较多位的数值类型之间存在隐式转换。

Image

***图 16-9。*隐式数值转换

溢出检查上下文

您已经看到,显式转换有可能丢失数据,并且无法在目标类型中等效地表示源值。对于整数类型,C# 允许您选择运行时在进行这些类型的转换时是否应该检查溢出结果。它通过checked操作符和checked语句来实现这一点。

  • 一段代码是否被检查称为它的溢出检查上下文
    • 如果将一个表达式或一段代码指定为checked,如果转换产生溢出,CLR 将引发一个OverflowException异常。
    • 如果代码不是checked,无论是否有溢出,转换都会继续进行。
  • 不检查默认溢出检查上下文。
选中的和未选中的运算符

checkedunchecked操作符控制表达式的溢出检查上下文,它放在一对括号中。表达式不能是方法。语法如下:

   checked   ( *Expression* ) unchecked ( *Expression* )

例如,下面的代码执行相同的转换——首先在一个checked操作符中,然后在一个unchecked操作符中。

  • unchecked上下文中,溢出被忽略,产生值208
  • checked上下文中,引发了一个OverflowException异常。

`   ushort sh = 2000;
byte   sb;

sb = unchecked ( (byte) sh );         // Most significant bits lost
Console.WriteLine("sb: {0}", sb);

sb =   checked ( (byte) sh );         // OverflowException raised
Console.WriteLine("sb: {0}", sb);`

该代码产生以下输出:


`sb: 208

Unhandled Exception: System.OverflowException: Arithmetic operation resulted in an overflow. at Test1.Test.Main() in C:\Programs\Test1\Program.cs:line 21`


已检查和未检查的报表

您刚才看到的checkedunchecked 操作符作用于括号之间的单个表达式。checkedunchecked 语句执行相同的功能,但是控制代码块中的所有转换,而不是单个表达式。

  • checkedunchecked语句可以嵌套到任何级别。

例如,下面的代码使用了checkedunchecked语句,并产生了与前面使用了checkedunchecked表达式的示例相同的结果。然而,在这种情况下,受影响的不仅仅是表达式,还有代码块。

`   byte   sb;
ushort sh = 2000;

unchecked                                            // Set unchecked
{
sb = (byte) sh;
Console.WriteLine("sb: {0}", sb);

checked                                           // Set checked
{
sb = (byte) sh;
Console.WriteLine("sb: {0}", sh);
}
}`

显式数值转换

您已经看到隐式转换会自动从源表达式转换为目标类型,因为不会丢失数据。然而,使用显式转换,有可能会丢失数据,因此作为程序员,了解转换如何处理发生的数据丢失非常重要。

在这一节中,我们将研究各种类型的显式数值转换。图 16-10 显示了图 16-8 中显示的显式转换的子集。

Image

***图 16-10。*显式数值转换

整数类型到整数类型

图 16-11 显示了整数到整数显式转换的行为。在checked的情况下,如果转换丢失数据,操作会引发OverflowException异常。在unchecked事件中,任何丢失的比特都没有被报告。

Image

***图 16-11。*整数到整数的显式转换

浮点或双精度到整数类型

将浮点类型转换为整数类型时,该值将向 0 舍入为最接近的整数。图 16-12 说明了转换条件。如果舍入值不在目标类型的范围内,则

  • 如果溢出检查上下文是checked,CLR 会引发一个OverflowException异常。
  • 如果上下文是unchecked,C# 没有定义它的值应该是什么。

Image

***图 16-12。*将浮点数或双精度数转换为整数类型

小数到整数类型

当从decimal转换为整数类型时,如果结果值不在目标类型的范围内,CLR 会引发一个OverflowException异常。图 16-13 说明了转换条件。

Image

图 16-13 。将小数转换成整数类型

双飘

类型float的值占用 32 位,类型double的值占用 64 位。当double被舍入到float时,double类型值被舍入到最接近的float类型值。图 16-14 说明了转换条件。

  • 如果该值太小而不能用float表示,则该值被设置为正 0 或负 0。
  • 如果该值太大而无法用float表示,则该值被设置为正无穷大或负无穷大。

Image

***图 16-14。*将双精度转换为浮点

浮点或双精度到十进制

图 16-15 显示了从浮点型转换到decimal的转换条件。

  • 如果值太小而不能用decimal类型来表示,则结果被设置为 0。
  • 如果该值太大,CLR 会引发一个OverflowException异常。

Image

图 16-15 。将浮点数或双精度数转换成十进制数

十进制为浮点数或双精度数

decimal到浮点类型的转换总是成功的。然而,可能会损失精确度。图 16-16 显示了转换条件。

Image

图 16-16 。将十进制转换成浮点数或双精度数

参考转换

正如你现在所知道的,引用类型对象在内存中由两部分组成:引用和数据。

  • 引用保存的部分信息是它指向的数据的类型。
  • 引用转换接受源引用并返回指向堆中相同位置的引用,但将该引用“标记”为不同的类型。

例如,下面的代码显示了两个引用变量,myVar1myVar2,它们指向内存中的同一个对象。代码如图图 16-17 所示。

  • 对于myVar1,它所引用的对象看起来像是一个类型为B的对象——事实也的确如此。
  • 对于myVar2,同一个对象看起来像一个类型为A的对象。
    • 即使myVar2实际上是指向一个类型为B的对象,它也看不到B延伸A的部分,因此也看不到Field2
    • 因此,第二个WriteLine语句会导致编译错误。

注意“转换”不会改变myVar1

`   class A    { public int Field1; }

class B: A { public int Field2; }

class Program
{
static void Main( )
{
B myVar1 = new B();
Return the reference to myVar1 as a reference to a class A.
         ↓         
A myVar2 = (A) myVar1;

Console.WriteLine("{0}", myVar2.Field1);            // Fine
Console.WriteLine("{0}", myVar2.Field2);            // Compile error!
}                                    ↑
}                                  myVar2 can't see Field2.` Image

***图 16-17。*引用转换返回与对象相关联的不同类型。

隐式引用转换

正如语言会自动为您执行隐式数值转换一样,也有隐式引用转换。这些在图 16-18 中进行了说明。

  • 所有引用类型都有到类型object的隐式转换。
  • 任何接口都可以隐式转换为派生它的接口。
  • 类可以隐式转换为
    • 衍生它的链中的任何类
    • 它实现的任何接口

Image

***图 16-18。*类和接口的隐式转换

委托可以隐式转换为 .NET BCL 类和接口如图图 16-19 所示。具有类型为 Ts 的元素的数组 ArrayS ,可以隐式转换为以下内容:

  • 那个 .NET BCL 类和接口如图图 16-19 所示。
  • 另一个数组, ArrayT ,元素类型为 Tt ,如果以下所有都为真:
    • 两个数组具有相同的维数。
    • 元素类型 TsTt 是引用类型,而不是值类型。
    • 类型 TsTt 之间有一个隐式转换。

Image

***图 16-19。*代表和数组的隐式转换

显式引用转换

显式引用转换是从一般类型到更特殊类型的引用转换。

  • 显式转换包括:
    • object到任何引用类型的转换
    • 从基类到从基类派生的类的转换
  • 通过反转图 16-18 和图 16-19 中的箭头来说明显式参考转换。

如果允许这种类型的转换而没有限制,那么您可以很容易地尝试引用实际上不在内存中的类成员。然而,编译器允许这些类型的转换。但是当系统在运行时遇到它们时,它会引发一个异常。

例如图 16-20 中的代码将基类A的引用转换为它的派生类B并赋给变量myVar2

  • 如果myVar2试图访问Field2,它将试图访问对象的“B部分”中的一个字段,该字段并不存在,从而导致内存故障。
  • 运行时将捕捉这种不适当的强制转换,并引发一个InvalidCastException异常。然而,请注意,是而不是 导致了编译错误。

Image

***图 16-20。*无效的强制转换引发运行时异常。

有效的显式引用转换

在三种情况下,显式引用转换会在运行时成功,也就是说,不会引发InvalidCastException异常。

第一种情况是不需要显式转换,也就是说,语言已经为您执行了隐式转换。例如,在下面的代码中,显式转换是不必要的,因为总是存在从派生类到其基类之一的隐式转换。

   class A { } class B: A { } ... B myVar1 = new B(); A myVar2 = (A) myVar1;     // Cast is unnecessary; A is the base class of B.

第二种情况是源参考为null。例如,在下面的代码中,尽管将基类的引用转换为派生类的引用通常是不安全的,但是这种转换是允许的,因为源引用的值是null

   class A { } class B: A { } ... A myVar1 = null; B myVar2 = (B) myVar1;     // Allowed because myVar1 is null

第三种情况是由源引用指向的实际数据可以被安全地隐式转换。下面的代码显示了一个例子,图 16-21 说明了该代码。

  • 第二行中的隐式转换使得myVar2“认为”它指向类型A的数据,而实际上它指向类型B的数据对象。
  • 第三行中的显式转换是将一个基类的引用强制转换为它的一个派生类的引用。通常这将引发一个异常。然而,在这种情况下,被指向的对象实际上是一个类型为B的数据项。

   B myVar1 = new B(); A myVar2 = myVar1;     // Implicitly cast myVar1 to type A. B myVar3 = (B)myVar2;  // This cast is fine because the data is of type B. Image

***图 16-21。*铸造到安全型

拳击转换

所有 C# 类型,包括值类型,都是从类型object派生的。然而,值类型是高效的轻量级类型,默认情况下,在堆中不包含它们的object组件。然而,当需要object组件时,您可以使用装箱,这是一种隐式转换,它接受值类型值,在堆中从中创建一个完整的引用类型对象,并返回对该对象的引用。

例如,图 16-22 显示了三行代码。

  • 前两行代码声明并初始化值类型变量i和引用类型变量oi
  • 在第三行代码中,您希望将变量i的值赋给oi。但是oi是一个引用类型变量,必须被赋予一个对堆中对象的引用。然而,变量i是一个值类型,并且没有对堆中对象的引用。
  • 因此,系统通过执行以下操作将i的值装箱:
    • 在堆中创建类型为int的对象
    • i的值复制到int对象
    • int对象的引用返回给oi以存储为其引用

Image

***图 16-22。*装箱从值类型创建一个完全引用类型对象。

装箱创建副本

关于装箱的一个常见误解是它作用于被装箱的物品。它没有。它返回一个引用类型值的副本。装箱过程之后,有两个值的副本——值类型原始副本和引用类型副本——每个副本都可以单独操作。

例如,下面的代码显示了值的每个副本的单独操作。图 16-23 说明了代码。

  • 第一行定义值类型变量i,并将其值初始化为10
  • 第二行创建引用类型变量oi,并用变量i的装箱副本初始化它。
  • 最后三行代码显示了分别操作的ioi

`   int i = 10;                     // Create and initialize value type
Box i and assign its reference to oi.
     ↓     
object oi = i;                  // Create and initialize reference type
Console.WriteLine("i: {0}, io: {1}", i, oi);

i  = 12;
oi = 15;
Console.WriteLine("i: {0}, io: {1}", i, oi);`

该代码产生以下输出:


i: 10, io: 10 i: 12, io: 15


Image

***图 16-23。*拳击创造了一个可以单独操纵的复制品。

拳击转换

图 16-24 显示了装箱转换。如果 ValueTypeS 实现了 InterfaceT ,任何值类型 ValueTypeS 都可以隐式转换为任意类型objectSystem.ValueTypeInterfaceT

Image

图 16-24 。装箱是值类型到引用类型的隐式转换。

取消装箱转换

取消装箱是将装箱的对象转换回其值类型的过程。

  • 取消装箱是一种显式转换。
  • 将值解装箱到 ValueTypeT 时,系统执行以下步骤:
    • 它检查被取消装箱的对象实际上是类型为 ValueTypeT 的装箱值。
    • 它将对象的值复制到变量中。

例如,下面的代码显示了取消装箱值的示例。

  • 值类型变量i被装箱并赋给引用类型变量oi
  • 然后变量oi被取消装箱,其值被赋给值类型变量j

   static void Main() { int i = 10; Box i and assign its reference to oi. <ins>     ↓     </ins>          object oi = i; Unbox oi and assign its value to j. <ins>       ↓       </ins>      int j = (int) oi; Console.WriteLine("i: {0},   oi: {1},   j: {2}", i,  oi, j); }

这段代码产生以下输出:


i: 10,   oi: 10,   j: 10


试图将一个值取消装箱为原始类型以外的类型会引发一个InvalidCastException异常。

拆箱转换

图 16-25 显示了拆箱转换。

Image

图 16-25 。拆箱转换

用户自定义换算

除了标准转换,您还可以为自己的类和结构定义隐式和显式转换。

下面的代码显示了用户定义转换的语法。

  • 隐式和显式转换声明的语法是相同的,除了关键字implicitexplicit
  • publicstatic修改器都是必需的。

            Required                              Operator          Keyword                         Source <ins>          ↓             </ins>                           ↓                     ↓              <ins>                     ↓                      </ins> public static implicit operator  *TargetType* ( *SourceType  Identifier* ) {                 ↑ Implicit or explicit ... return *ObjectOfTargetType*; }

例如,下面显示了将类型为Person的对象转换为int的转换方法的语法示例:

   public static implicit operator int(Person p) { return p.Age; }

用户定义转换的约束

对用户定义的转换有一些重要的约束。最重要的如下:

  • 您只能为类和结构定义用户定义的转换。
  • 不能重定义标准的隐式或显式转换。
  • 对于源类型 S 和目标类型 T 来说,以下是正确的:
    • S T 必须是不同的类型。
    • ST 不能有继承关系。即 S 不能从*T**T*不能从 S 派生。
    • ST 都不能是接口类型或者类型object
    • 转换运算符必须是 ST 的成员。
  • 不能用相同的源类型和目标类型声明两个转换,一个是隐式的,另一个是显式的。

用户自定义转换的例子

下面的代码定义了一个名为Person的类,其中包含一个人的姓名和年龄。该类还定义了两个隐式转换。第一个函数将一个Person对象转换成一个int值。目标int值是人的年龄。第二个将一个int转换成一个Person对象。

`   class Person
{
public string Name;
public int    Age;
public Person(string name, int age)

public static implicit operator int(Person p)   // Convert Person to int.
{
return p.Age;
}

public static implicit operator Person(int i)   // Convert int to Person.
{
return new Person("Nemo", i);        // ("Nemo" is Latin for "No one".)
}
}

class Program
{
static void Main( )
{
Person bill = new Person( "bill", 25);

Convert a Person object to an int.
         ↓         
int age = bill;
Console.WriteLine("Person Info: {0}, {1}", bill.Name, age);

Convert an int to a Person object.
         ↓        
Person anon = 35;
Console.WriteLine("Person Info: {0}, {1}", anon.Name, anon.Age);
}
}`

这段代码产生以下输出:


Person Info: bill, 25 Person Info: Nemo, 35


如果您将相同的转换操作符定义为explicit而不是implicit,那么您将需要使用转换表达式来执行转换,如下所示:

`                   Explicit
...           ↓   
public static explicit operator int( Person p )
{
return p.Age;
}

...

static void Main( )
{
...  Requires cast expression
         ↓         
int age = (int) bill;
...`

评估用户定义的转换

到目前为止讨论的用户自定义转换已经在一个单一的步骤中将源类型直接转换为目标类型的对象,如图图 16-26 所示。

Image

***图 16-26。*单步用户自定义转换

但是用户定义的转换在完整转换中最多可以有三个步骤。图 16-27 说明了这些阶段,包括以下内容:

  • 初步标准转换
  • 用户定义的转换
  • 以下标准转换

在这个链中永远不会超过一个用户定义的转换。

Image

***图 16-27。*多步用户自定义转换

多步用户定义转换示例

下面的代码声明了从类Person派生的类Employee

  • 几节之前,代码示例声明了从类Personint的用户定义转换。所以如果有一个从EmployeePerson的标准转换和一个从intfloat的标准转换,你可以从Employee转换到float
    • EmployeePerson有一个标准的转换,因为Employee来源于Person
    • 有一个从intfloat的标准转换,因为这是一个隐式的数字转换。
  • 由于链的所有三个部分都存在,您可以从Employee转换到float。图 16-28 说明了编译器是如何执行转换的。

`   class Employee : Person

class Person
{
public string Name;
public int    Age;

// Convert a Person object to an int.
public static implicit operator int(Person p)
{
return p.Age;
}
}

class Program
{
static void Main( )
{
Employee bill = new Employee();
bill.Name = "William";
bill.Age  = 25;
Convert an Employee to a float.
          ↓          
float fVar = bill;

Console.WriteLine("Person Info: {0}, {1}", bill.Name, fVar);
}
}`

该代码产生以下输出:


Person Info: William, 25


Image

***图 16-28。*员工转浮动

是运算符

如前所示,一些转换尝试不成功,并在运行时引发一个InvalidCastExcept ion 异常。您可以使用is操作符来检查转换是否会成功完成,而不是盲目地尝试转换。

is运算符的语法如下,其中 Expr 是源表达式:

               Returns a bool <ins>                 ↓                 </ins> Expr is TargetType

如果 Expr 可以通过以下任何一种方式成功转换为目标类型,则操作员返回true:

  • 参考转换
  • 拳击的转变
  • 取消装箱转换

例如,下面的代码使用is操作符来检查类型为Employee的变量bill是否可以转换为类型Person,然后采取适当的操作。

`   class Employee : Person
class Person
{
public string Name = "Anonymous";
public int Age     = 25;
}

class Program
{
static void Main()
{
Employee bill = new Employee();
Person p;

// Check if variable bill can be converted to type Person
if( bill is Person )
{
p = bill;
Console.WriteLine("Person Info: {0}, {1}", p.Name, p.Age);
}
}
}`

is运算符只能用于引用转换以及装箱和取消装箱转换。它不能用于用户定义的转换。

as 运算符

除了不引发异常之外,as操作符与 cast 操作符相似。如果转换失败,它不会引发异常,而是返回null

as操作符的语法如下,其中

  • Expr 是源表达式。
  • TargetType 是目标类型,必须是引用类型。

             Returns a reference <ins>                 ↓                 </ins> Expr as TargetType

因为as操作符返回一个引用表达式,所以它可以用作赋值的源。

例如,使用as运算符将类型为Employee的变量bill转换为类型为Person,并将其赋给类型为Person的变量p。然后代码在使用之前检查p是否为null

`   class Employee : Person

class Person
{
public string Name = "Anonymous";
public int Age     = 25;
}

class Program
{
static void Main()
{
Employee bill = new Employee();
Person p;

p = bill as Person;
if( p != null )
{
Console.WriteLine("Person Info: {0}, {1}", p.Name, p.Age);
}
}
}`

is操作符一样,as操作符只能用于引用转换和装箱转换。它不能用于用户定义的转换或值类型的转换。

十七、泛型

什么是泛型?

使用到目前为止您所学的语言构造,您可以构建许多不同类型的强大对象。这主要是通过声明封装了所需行为的类,然后创建这些类的实例来实现的。

到目前为止,类声明中使用的所有类型都是特定类型——要么是程序员定义的,要么是语言或 BCL 提供的。然而,有些时候,如果您能够“提取”或“重构”出一个类的动作,并且不仅将它们应用于为其编码的数据类型,而且还应用于其他类型,那么这个类会更有用。

泛型允许你这样做。您可以重构您的代码,并添加一个额外的抽象层,以便对于某些类型的代码,数据类型不是硬编码的。这是专门为有多个代码段执行相同的指令,但数据类型不同的情况而设计的。

这听起来可能很抽象,所以我们将从一个例子开始,这个例子将使事情变得更清楚。

一个堆栈示例

首先假设您已经创建了以下代码,它声明了一个名为MyIntStack的类,该类实现了一个int的堆栈。它允许您将int推到堆栈上并弹出它们。顺便说一下,这不是系统堆栈。

`   class MyIntStack                        // Stack for ints
{
int   StackPointer = 0;
int[] StackArray;                    // Array of int
↑                 int
int                 ↓  
public void Push( int x )            // Input type: int
       int

public int Pop()                     // Return type: int

...
}`

现在假设您想要对类型float的值使用相同的功能。有几种方法可以实现这一点。一种方法是执行以下步骤来生成后续代码:

  1. And cut and paste the code of MyIntStack class.
  2. Change the class name to MyFloatStack.
  3. Change the appropriate int declaration to float declaration in the whole class declaration.

`   class MyFloatStack                     // Stack for floats
{
int   StackPointer = 0;
float [] StackArray;                // Array of float
↑                float
float                 ↓
public void Push( float x )         // Input type: float

float

public float Pop()                  // Return type: float

...

}`

这种方法当然有效,但是容易出错,并且有以下缺点:

  • You need to carefully check every part of the class to determine which type declarations need to be changed and which ones don't.
  • You need to repeat this process (long, double, string and so on) for each new type of stack class you need. After this process, you will get multiple copies of almost the same code, occupying extra space. And debugging and maintenance are not elegant and error-prone.

c# 中的泛型

泛型特性提供了一种更优雅的方式来使用一组具有多种类型的代码。泛型允许你声明类型参数化的代码,你可以用不同的类型实例化它。这意味着您可以编写带有“类型占位符”的代码,然后在创建该类的实例时提供实际的类型。

到目前为止,你应该非常熟悉类型不是对象而是对象模板的概念。同样,泛型类型不是类型,而是类型的模板。图 17-1 说明了这一点。

Image

***图 17-1。*泛型类型是类型的模板。

C# 提供了五种泛型:类、结构、接口、委托和方法。注意,前四个是类型,方法是成员。

图 17-2 显示了泛型是如何与其他类型相适应的。

Image

***图 17-2。*泛型和用户自定义类型

继续堆栈示例

在栈的例子中,对于类MyIntStackMyFloatStack,类的声明体是相同的,除了在处理由栈保存的值的类型的位置。

  • In MyIntStack, these positions are occupied by type int.
  • At MyFloatStack, they were occupied by float.

通过执行以下操作,您可以从MyIntStack创建一个通用类:

  1. Take MyIntStack class declaration, and replace float with int with type placeholder T.
  2. Change the class name to MyStack.
  3. Place the string <T> after the class name.

结果是下面的泛型类声明。由带T的尖括号组成的字符串意味着T是一个类型的占位符。(不一定是字母T——可以是任何标识符。)在T所在的整个类声明体中,编译器需要替换一个实际类型。

`   class MyStack
{
int StackPointer = 0;
T [] StackArray;


public void Push(T x )


public T Pop()
...
}`

类属

现在你已经看到了一个泛型类,让我们更详细地看看泛型类,看看它们是如何被创建和使用的。

如您所知,创建和使用您自己的常规、非泛型类有两个步骤:声明类和创建类的实例。然而,泛型类不是实际的类,而是类的模板——所以您必须首先从它们构造实际的类类型。然后,您可以从这些构造的类类型中创建引用和实例。

图 17-3 从高层次上说明了该过程。如果还不完全清楚,不要担心——我将在接下来的章节中介绍每个部分。

  1. Declare a class and use placeholders for certain types.
  2. Provide the actual type to replace the placeholder. This gives you an actual class definition, and all the "blanks" are filled in. This is called structural type .
  3. Create an instance of the constructed type.

Image

***图 17-3。*从通用类型创建实例

声明泛型类

声明一个简单的泛型类很像声明一个常规类,但有以下区别:

在类名后面放置一组匹配的尖括号。* Between the angle brackets, you put a comma-separated list of placeholder strings, which represent types and will be provided as needed. These are called type parameters . You use type parameters in the declarant of generic classes to represent the types that should be replaced.

例如,下面的代码声明了一个名为SomeClass的泛型类。类型参数列在尖括号之间,然后在整个声明体中使用,就像它们是实类型一样。

                  Type parameters <ins>   ↓  </ins> class SomeClass < T1, T2 > {   Normally, types would be used in these positions. ↓               ↓ public T1 SomeVar  = new T1(); public T2 OtherVar = new T2(); }         ↑                                    ↑ Normally, types would be used in these positions.

没有标记泛型类声明的特殊关键字。相反,用尖括号分隔的类型参数列表的存在将泛型类声明与常规类声明区分开来。

创建构造类型

一旦声明了泛型类型,就需要告诉编译器应该用什么实际类型替换占位符(类型参数)。编译器接受这些实际类型并创建一个构造类型,构造类型是一个模板,它从这个模板创建实际的类对象。

创建构造类型的语法如下所示,包括列出类名和在尖括号之间提供真实类型,以代替类型参数。被类型参数替代的实类型被称为类型 实参。

            Type arguments <ins>   ↓      </ins> SomeClass< short, int >

编译器接受类型实参,并用它们替换泛型类主体中相应的类型形参,从而生成构造类型——实际的类实例就是从构造类型中创建的。

图 17-4 显示了左边的泛型类SomeClass的声明。在右边,它显示了使用类型参数shortint创建的构造类。

Image

***图 17-4。*为一个泛型类的所有类型参数提供类型实参允许编译器生成一个构造类,从这个构造类中可以创建实际的类对象。

图 17-5 说明了类型参数和类型变量之间的区别。

  • Generic class declaration has type parameter , which acts as a placeholder for the type.
  • Type parameter is the actual type that you provided when you created the constructed type.

Image

***图 17-5。*类型参数与类型实参

创建变量和实例

在创建引用和实例时,构造的类类型就像常规类型一样使用。例如,下面的代码显示了两个类对象的创建。

  • The first line shows the creation of an object from a regular non-generic class. This is a form that you should be fully familiar with by now.
  • The second line of code shows that an object is created from the generic class SomeClass and instantiated with the types short and int. This form is completely similar to the above line, and the conventional class name is replaced by the constructed class form.
  • The third line is semantically the same as the second line, but instead of listing the types of constructions on both sides of the equal sign, it uses the var keyword to let the compiler use type inference.

   MyNonGenClass         myNGC = new MyNonGenClass        (); Constructed class                    Constructed class <ins>         ↓           </ins>              <ins>         ↓           </ins> SomeClass<short, int>  mySc1 = new SomeClass<short  int>(); var                    mySc2 = new SomeClass<short, int>();

与非通用类一样,引用和实例可以分别创建,如图图 17-6 所示。该图还显示了内存中发生的事情与非泛型类相同。

  • Class declares that the first behavior variable myInst below allocates a reference in the stack. Its value is null.
  • The second line allocates an instance in the heap and assigns its reference to the variable.

Image

***图 17-6。*使用构造类型创建引用和实例

许多不同的类类型可以从同一个泛型类中构造。每一个都是独立的类类型,就像它有自己独立的非泛型类声明一样。

例如,下面的代码显示了从泛型类SomeClass创建两个类型。代码如图图 17-7 所示。

  • One type consists of types short and int.
  • The other is composed of int and long.

`   class SomeClass< T1, T2 >                            // Generic class

class Program
{
static void Main()
{
var first  =  new SomeClass<short, int >();    // Constructed type
var second =  new SomeClass<int,   long>();    // Constructed type

...` Image

***图 17-7。*从一个泛型类创建了两个不同的构造类

使用泛型的堆栈示例

下面的代码显示了使用泛型实现的堆栈示例。方法Main定义了两个变量:stackIntstackString。使用intstring作为类型参数创建两个构造类型。

`class MyStack
{
T[] StackArray;
int StackPointer = 0;

public void Push(T x)
{
if ( !IsStackFull )
StackArray[StackPointer++] = x;
}

public T Pop()
{
return ( !IsStackEmpty )
? StackArray[--StackPointer]
: StackArray[0];
}

const int MaxStack = 10;
bool IsStackFull  { get{ return StackPointer >= MaxStack; } }
bool IsStackEmpty { get{ return StackPointer ⇐ 0; } }

public MyStack()
{
StackArray = new T[MaxStack];
}

public void Print()
{
for (int i = StackPointer-1; i >= 0 ; i--)
Console.WriteLine("   Value: {0}", StackArray[i]);
}
}    class Program
{
static void Main( )
{
MyStack    StackInt    = new MyStack();
MyStack StackString = new MyStack();

StackInt.Push(3);
StackInt.Push(5);
StackInt.Push(7);
StackInt.Push(9);
StackInt.Print();

StackString.Push("This is fun");
StackString.Push("Hi there!  ");
StackString.Print();
}
}`

该代码产生以下输出:


`Value: 9
Value: 7
Value: 5
Value: 3

Value: Hi there!
Value: This is fun`


比较泛型和非泛型堆栈

表 17-1 总结了栈的初始非泛型版本和最终泛型版本之间的一些差异。图 17-8 说明了其中的一些差异。

Image

Image

***图 17-8。*非泛型栈与泛型栈

对类型参数的约束

在通用堆栈示例中,堆栈除了存储和弹出项目之外,没有对它包含的项目做任何事情。它没有尝试添加它们,比较它们,或者做任何其他需要使用项目本身的操作的事情。这是有充分理由的。因为通用堆栈不知道它将要存储的项的类型,所以它不能知道这些类型实现了什么成员。

然而,所有的 C# 对象最终都是从类object中派生出来的,所以堆栈可以确定的一件事就是它们实现了类object的成员。这些方法包括ToStringEqualsGetType。除此之外,它无法知道哪些成员可用。

只要你的代码不访问它处理的类型的对象(或者只要它坚持类型object的成员),你的泛型类可以处理任何类型。满足这个约束的类型参数叫做无界类型参数。但是,如果您的代码试图使用任何其他成员,编译器将产生错误信息。

例如,下面的代码用一个名为LessThan的方法声明了一个名为Simple的类,该方法采用两个相同泛型类型的变量。LessThan试图返回使用小于运算符的结果。但是并不是所有的类都实现小于操作符,所以你不能用任何一个类来代替T。因此,编译器会产生一条错误信息。

   class Simple<T> { static public bool LessThan(T i1, T i2) { return i1 < i2;                      // Error } ... }

为了使泛型更有用,你需要能够向编译器提供关于什么类型的类型可以作为参数的附加信息。这些额外的信息位被称为约束。只有满足约束的类型才能替换给定的类型参数,以生成构造类型。

Where 从句

约束被列为where子句。

  • Each constrained type parameter has its own where clause.
  • If a parameter has multiple constraints, they are listed in the where clause, separated by commas.

where子句的语法如下:

         Type parameter          Constraint list ↓       <ins>           ↓                </ins> where  TypeParam : constraint, constraint, ... ↑              ↑ Keyword          Colon

关于where条款的要点如下:

  • They are listed after the closing angle bracket of the type parameter list.
  • They are not separated by commas or any other symbols.
  • They can be listed in any order.
  • Token ] is a contextual keyword, so it can be used in other contexts.

例如,下面的泛型类有三个类型参数。T1无界。对于T2,只有Customer类型的类或者从 Customer派生的类可以用作类型参数。对于T3,只有实现接口IComparable的类才能被用作类型参数。

             Unbounded   With constraints ↓   <ins>   ↓  </ins>     No separators class MyClass < T1, T2, T3 >      ↓ where T2: Customer                   // Constraint for T2 where T3: IComparable                // Constraint for T3 {                                    ↑ ...                           No separators }

约束类型和顺序

有五种类型的约束。这些在表 17-2 中列出。

Image

where子句可以按任何顺序列出。然而,where子句中的约束必须以特定的顺序放置,如图图 17-9 所示。

  • There can only be one master constraint at most, and if there is one, it must be listed first.
  • There can be any number of InterfaceName constraints.
  • If the constructor constraint exists, it must be listed last.

Image

***图 17-9。*如果一个类型参数有多个约束,它们必须按照这个顺序。

以下声明显示了where子句的示例:

`   class SortedList
where S: IComparable

class LinkedList<M,N>
where M : IComparable
where N : ICloneable    

class MyDictionary<KeyType, ValueType>
where KeyType : IEnumerable,
new()                   `

通用方法

与其他泛型不同,方法不是类型,而是成员。你可以在泛型和非泛型类中,以及在结构和接口中声明泛型方法,如图 17-10 所示。

Image

***图 17-10。*泛型方法可以在泛型和非泛型类型中声明。

声明一个泛型方法

泛型方法有一个类型参数列表和可选约束。

  • Generic methods have two parameter lists:
    • List of method parameters , enclosed in brackets.
    • List of type parameters , enclosed in angle brackets.
  • To declare a generic method, do the following:
    • Put the type parameter list after the method name and before the method parameter list.
    • Put any constraint clauses after the method parameter list.

                    Type parameter list           Constraint clauses <ins> ↓  </ins>               <ins>     ↓         </ins> public void PrintData<S, T> (<ins>S p, T t</ins>) where S: Person {                                ↑ ...                       Method parameter list }

Image 注意记住类型参数列表在方法名之后,方法参数列表之前。

调用通用方法

若要调用泛型方法,请为方法调用提供类型参数,如下所示:

           Type arguments <ins>    ↓      </ins> MyMethod<short, int>(); MyMethod<int, long >();

图 17-11 显示了一个名为DoStuff的泛型方法的声明,它有两个类型参数。在它下面有两个调用该方法的地方,每个地方都有一组不同的类型参数。编译器使用这些构造的实例中的每一个来产生该方法的不同版本,如图右侧所示。

Image

***图 17-11。*具有两个实例化的通用方法

推断类型

如果将参数传递给一个方法,编译器有时可以从方法参数的类型中推断出哪些类型应该被用作泛型方法的类型参数。这可以使方法调用更简单,更容易阅读。

例如,下面的代码声明了MyMethod,它采用与类型参数相同类型的方法参数。

   public void MyMethod <T> (T myVal) { ... } ↑   ↑ Both are of type T

如果用类型为int的变量调用MyMethod,如下面的代码所示,方法调用的类型参数中的信息是多余的,因为编译器可以从方法参数中看出它是一个int

   int myInt = 5; MyMethod <int> (myInt); ↑      ↑ Both are ints

因为编译器可以从方法参数中推断出类型参数,所以可以在调用中省略类型参数及其尖括号,如下所示:

   MyMethod(myInt);

泛型方法的例子

下面的代码在名为Simple的非泛型类中声明了一个名为ReverseAndPrint的泛型方法。该方法将任何类型的数组作为其参数。Main声明了三种不同的数组类型。然后,它对每个数组调用该方法两次。第一次使用特定数组调用方法时,它显式使用类型参数。第二次,推断类型。

`   class Simple                                          // Non-generic class
{
static public void ReverseAndPrint(T[] arr)     // Generic method
{
Array.Reverse(arr);
foreach (T item in arr)                         // Use type argument T.
Console.Write("{0}, ", item.ToString());
Console.WriteLine("");
}
}

class Program
{
static void Main()
{
// Create arrays of various types.
var intArray    = new int[]    { 3, 5, 7, 9, 11 };
var stringArray = new string[] { "first", "second", "third" };
var doubleArray = new double[] { 3.567, 7.891, 2.345 };

Simple.ReverseAndPrint(intArray);        // Invoke method.
Simple.ReverseAndPrint(intArray);             // Infer type and invoke.

Simple.ReverseAndPrint(stringArray);  // Invoke method.
Simple.ReverseAndPrint(stringArray);          // Infer type and invoke.

Simple.ReverseAndPrint(doubleArray);  // Invoke method.
Simple.ReverseAndPrint(doubleArray);          // Infer type and invoke.
}
}`

该代码产生以下输出:


11, 9, 7, 5, 3, 3, 5, 7, 9, 11, third, second, first, first, second, third, 2.345, 7.891, 3.567, 3.567, 7.891, 2.345,


用泛型类扩展方法

扩展方法在第七章中有详细描述,并且和一般类一样有效。它们允许您将一个类中的静态方法与不同的泛型类相关联,并调用该方法,就像它是该类的构造实例上的实例方法一样。

与非泛型类一样,泛型类的扩展方法必须满足以下约束:

static

  • It must be a member of a static class.
  • It must contain the keyword this as its first parameter type, followed by the name of the generic class it extends.

下面的代码展示了一个名为Holder<T>的泛型类上名为Print的扩展方法的例子:

`    static class ExtendHolder
{
public static void Print(this Holder h)
{
T[] vals = h.GetValues();
Console.WriteLine("{0},\t{1},\t{2}", vals[0], vals[1], vals[2]);
}
}

class Holder
{
T[] Vals = new T[3];

public Holder(T v0, T v1, T v2)
{ Vals[0] = v0; Vals[1] = v1; Vals[2] = v2; }

public T[] GetValues() { return Vals; }    
}

class Program
{
static void Main(string[] args) {
var intHolder    = new Holder(3, 5, 7);
var stringHolder = new Holder("a1", "b2", "c3");
intHolder.Print();
stringHolder.Print();
}
}`

该代码产生以下输出:


3,      5,      7 a1,     b2,     c3


通用结构

像泛型类一样,泛型结构可以有类型参数和约束。泛型结构的规则和条件与泛型类的规则和条件相同。

例如,下面的代码声明了一个名为PieceOfData的通用结构,它存储和检索一段数据,数据的类型是在构造类型时确定的。Main创建两种构造类型的对象——一种使用int,另一种使用string

`   struct PieceOfData                             // Generic struct
{
public PieceOfData(T value)
private T _data;
public  T Data
{
get { return _data; }
set
}
}

class Program
{
static void Main()          Constructed type
{                                ↓           
var intData    = new PieceOfData(10);
var stringData = new PieceOfData("Hi there.");

Constructed type
Console.WriteLine("intData    = {0}", intData.Data);
Console.WriteLine("stringData = {0}", stringData.Data);
}
}`

该代码产生以下输出:


intData    = 10 stringData = Hi there.


通用代理人

泛型委托与非泛型委托非常相似,只是类型参数决定了将接受哪些方法的特征。

  • To declare a generic delegate, put the type parameter list in angle brackets after the delegate name and before the delegate parameter list.                      Type parameters <ins> ↓   </ins> delegate R MyDelegate<T, R>( <ins>T value</ins> ); ↑                      ↑ Return type         Delegate formal parameter
  • Note that there are two parameter lists: delegate parameter list and type parameter list.
  • The range of parameters includes the following:
    • Return type
    • Parameter list
    • Constraint clause

下面的代码展示了一个泛型委托的例子。在Main中,通用委托MyDelegate用类型string的参数实例化,并用方法PrintString初始化。

`   delegate void MyDelegate(T value);             // Generic delegate

class Simple
{
static public void PrintString(string s)       // Method matches delegate
{
Console.WriteLine(s);
}

static public void PrintUpperString(string s)  // Method matches delegate
{
Console.WriteLine("{0}", s.ToUpper());
}
}

class Program
{
static void Main( )
{
var myDel =                                 // Create inst of delegate.
new MyDelegate(Simple.PrintString);
myDel += Simple.PrintUpperString;           // Add a method.

myDel("Hi There.");                         // Call delegate.
}
}`

该代码产生以下输出:


Hi There. HI THERE.


另一个通用委托示例

由于 C# 的 LINQ 特性广泛使用了泛型委托,所以在此之前有必要展示另一个例子。我将在第十九章中讲述 LINQ 本身,以及更多关于它的一般代表。

下面的代码声明了一个名为Func的泛型委托,它采用带有两个参数和返回值的方法。方法返回类型表示为TR,方法参数类型表示为T1T2

`                           Delegate parameter type
↓  ↓      ↓     ↓
public delegate TR Func<T1, T2, TR>(T1 p1, T2 p2);  // Generic delegate
↑                ↑
class Simple     Delegate return type
{
static public string PrintString(int p1, int p2) // Method matches delegate
{
int total = p1 + p2;
return total.ToString();
}
}

class Program
{
static void Main()
{
var myDel =                                   // Create inst of delegate.
new Func<int, int, string>(Simple.PrintString);

Console.WriteLine("Total: {0}", myDel(15, 13));  // Call delegate.
}
}`

该代码产生以下输出:


Total: 28


通用接口

泛型接口允许您编写接口,其中接口成员的形参和返回类型是泛型类型参数。泛型接口声明类似于非泛型接口声明,但在接口名称后的尖括号中有类型参数列表。

例如,下面的代码声明了一个名为IMyIfc的通用接口。

  • Simple is a general class that implements the general interface IMyIfc.
  • Main Instantiate two objects of the generic class: one type is int and the other type is string.

`                Type parameter

interface IMyIfc                          // Generic interface
{
T ReturnIt(T inValue);
}
Type parameter   Generic interface
↓       ↓     
class Simple : IMyIfc                  // Generic class
{
public S ReturnIt(S inValue)              // Implement generic interface.
{ return inValue; }
}

class Program
{
static void Main()
{
var trivInt    = new Simple();
var trivString = new Simple();

Console.WriteLine("{0}", trivInt.ReturnIt(5));
Console.WriteLine("{0}", trivString.ReturnIt("Hi there."));
}
}`

该代码产生以下输出:


5 Hi there.


一个使用通用接口的例子

以下示例说明了通用接口的两个附加功能:

  • Like other generics, instances of generic interfaces instantiated with different type parameters are different interfaces.
  • You can implement a generic interface in the non-generic type .

例如,下面的代码类似于上一个例子,但是在这个例子中,Simple是一个非泛型类,它实现了一个泛型接口。事实上,它实现了IMyIfc的两个实例。一个实例用类型int实例化,另一个用类型string实例化。

`   interface IMyIfc                            // Generic interface
{
T ReturnIt(T inValue);
}
Two different interfaces from the same generic interface
   ↓            ↓         
class Simple : IMyIfc, IMyIfc     // Nongeneric class
{
public int ReturnIt(int inValue)            // Implement interface using int.
{ return inValue; }

public string ReturnIt(string inValue)      // Implement interface using string.
{ return inValue; }
}

class Program
{
static void Main()
{
Simple trivial = new Simple();

Console.WriteLine("{0}", trivial.ReturnIt(5));
Console.WriteLine("{0}", trivial.ReturnIt("Hi there."));
}
}`

该代码产生以下输出:


5 Hi there.


通用接口实现必须是唯一的

当在泛型类型中实现接口时,不能有可能在该类型中创建重复接口的类型参数组合。

例如,在下面的代码中,类Simple使用了接口IMyIfc的两个实例。

  • The first is the construction type, which is instantiated with the type int.
  • The second one has type parameters but no arguments.

第二个接口本身没有问题,因为使用通用接口完全没问题。然而,这里的问题是,它允许一个可能的冲突,因为如果在第二个接口中使用int作为类型参数来替换S,那么Simple将有两个相同类型的接口——这是不允许的。

`   interface IMyIfc
{
T ReturnIt(T inValue);
}
Two interfaces
     ↓           ↓    
class Simple : IMyIfc, IMyIfc     // Error!
{
public int ReturnIt(int inValue)   // Implement first interface.
{
return inValue;
}

public S ReturnIt(S inValue)       // Implement second interface,
{                                  // but if it's int, it would be
return inValue;                 // the same as the one above.
}
}`

Image 注意通用接口的名称不会与非通用接口冲突。例如,在前面的代码中,我们也可以声明一个名为IMyIfc的非泛型接口。

协方差

正如你在本章看到的,当你创建一个泛型类型的实例时,编译器接受泛型类型声明和类型参数并创建一个构造类型。然而,人们经常犯的一个错误是,认为可以将派生类型的委托赋给基类型委托的变量。在接下来的几节中,我们将看看这个主题,它叫做方差。方差有三种类型——协方差方差不变性

我们将从回顾你已经学过的一些东西开始:每个变量都有一个分配给它的类型,你可以分配一个更派生类型的对象给它的一个基本类型的变量。这被称为赋值兼容性。下面的代码演示了与基类Animal和从Animal派生的类Dog的赋值兼容性。在Main中,您可以看到代码创建了一个类型为Dog的对象,并将其赋给类型为Animal的变量a2

`   class Animal

class Dog : Animal

class Program
{
static void Main( )
{
Animal a1 = new Animal( );
Animal a2 = new Dog( );

Console.WriteLine( "Number of dog legs: {0}", a2.NumberOfLegs );
}
}`

该代码产生以下输出:


Number of dog legs: 4


图 17-12 说明了赋值兼容性。在该图中,显示DogAnimal对象的方框也显示了它们的基类。

Image

***图 17-12。*赋值兼容性意味着你可以将一个更多派生类型的引用赋给一个更少派生类型的变量。

现在让我们通过以下方式扩展代码来看一个更有趣的例子,如下面的代码所示:

  • This code adds a generic delegate named Factory, which only accepts one type parameter T and no method parameter, and returns an object of type T.
  • I added a method named MakeDog, which has no parameters and returns a Dog object. Therefore, if we use Dog as the type parameter, this method matches the delegate Factory.
  • The first line of Main creates a delegate object of type delegate Factory<Dog> and assigns its reference to variable dogMaker of the same type.
  • The second line attempts to assign the delegate of type delegate Factory<Dog> to the delegate type variable animalMaker of type delegate Factory<Animal>.

然而,Main中的第二行引起了一个问题,编译器产生了一个错误消息,说它不能隐式地将右边的类型转换成左边的类型。

`   class Animal         // Base class
class Dog : Animal                        // Derived class

delegate T Factory( );       ← delegate Factory

class Program
{
static Dog MakeDog( )        ← Method that matches delegate Factory
{
return new Dog( );
}

static void Main( )
{
Factory    dogMaker    = MakeDog;   ← Create delegate object.
Factory animalMaker = dogMaker;  ← Attempt to assign delegate object.

Console.WriteLine( animalMaker( ).Legs.ToString( ) );
}
}`

用基类型构造的委托应该能够保存用派生类型构造的委托,这似乎是有道理的。那么为什么编译器会给出错误信息呢?赋值兼容原则不成立吗?

原则确实成立,但不适用于这种情况!问题是,虽然Dog派生自Animal,但是委托Factory<Dog>并不而不是派生自委托Factory<Animal>。相反,这两个委托对象是对等的,从类型 d elegate派生,从类型object派生,如图图 17-13 所示。这两个委托都不是从另一个委托派生的,因此赋值兼容性不适用。

Image

***图 17-13。*赋值兼容性不适用,因为这两个委托没有继承关系。

尽管委托类型的不匹配不允许将一种类型赋给另一种类型的变量,但在这种情况下这太糟糕了,因为在示例代码中,任何时候我们执行委托animalMaker,调用代码都会期望返回对一个Animal对象的引用。如果它返回一个对Dog对象的引用,那也没问题,因为根据赋值兼容性,对Dog的引用就是对Animal的引用。

更仔细地观察这种情况,我们可以看到,对于任何泛型委托,如果类型参数仅用作输出值,那么同样的情况也适用。在所有这些情况下,您将能够使用用派生类创建的构造委托类型,并且它将工作得很好,因为调用代码将总是期望对基类的引用——这正是它将得到的。

这种派生类型仅作为输出值的使用和构造的委托的有效性之间的常数关系被称为协方差。为了让编译器知道这是您想要的,您必须在委托声明中用out关键字标记类型参数。

如果我们通过添加关键字out来更改示例中的委托声明,如下所示,代码会编译并正常运行:

   delegate T Factory<out T>( ); ↑ Keyword specifying covariance of the type parameter

图 17-14 说明了本例中协方差的组成部分:

  • The variables on the left stack are of type delegate T Factory<out T>(), in which the type variable T is of type Animal.
  • The delegate actually constructed in the right heap is declared by the type variable of class Dog, which is derived from class Animal. This is acceptable because when the delegate is called, the calling code receives the object of type Dog instead of the expected object of type Animal. The calling code can freely operate the Animal part of the object, just as it expected.

Image

***图 17-14。*协变关系允许更派生的类型在返回和输出位置。

逆变

既然你理解了协方差,我们来看一个相关的情况。下面的代码声明了一个名为Action1的委托,它接受一个类型参数和一个方法参数,方法参数的类型是类型参数的类型,并且不返回值。

代码还包含一个名为ActOnAnimal的方法,其签名和void返回类型与委托声明相匹配。

Main中的第一行使用类型Animal和方法ActOnAnimal创建一个构造的委托,其签名和void返回类型与委托声明相匹配。然而,在第二行中,代码试图将对这个委托的引用分配给一个名为dog1、类型为delegate Action1<Dog>的堆栈变量。

`   class Animal
class Dog : Animal

class Program         Keyword for contravariance
{                        ↓
delegate void Action1( T a );

static void ActOnAnimal( Animal a ) { Console.WriteLine( a.NumberOfLegs ); }

static void Main( )
{
Action1 act1 = ActOnAnimal;
Action1    dog1 = act1;
dog1( new Dog() );
}
}`

该代码产生以下输出:


4


和前面的情况一样,默认情况下,你不能分配这两个不兼容的类型。但是也和前面的情况一样,有些情况下这个任务会很好的完成。

事实上,每当类型参数仅用作委托中方法的输入参数时,这都是正确的。这样做的原因是,即使调用代码传入一个对更多派生类的引用,委托中的方法也只是期望一个对更少派生类的引用——当然,它接收并知道如何操作这个类。

这种关系,允许一个派生程度更高的对象出现在一个派生程度更低的对象出现的地方,被称为逆变。要使用它,您必须使用带有类型参数的in关键字,如代码所示。

图 17-15 说明了Main第二行的逆变成分。

  • The variables on the left stack are of type delegate void Action1<in T>(T p), and the type variables here are of type Dog.
  • The delegate actually constructed on the right is declared by the type variable of class Animal, which is the base class of class Dog.
  • This is good, because when the delegate is called, the calling code passes an object of type Dog to method ActOnAnimal, which requires an object of type Animal. This method can freely operate the Animal part of the object, just as it is expected.

Image

***图 17-15。*逆变关系允许更多的派生类型作为输入参数。

图 17-16 总结了一般委托中协方差和逆变的区别。

  • The figure above illustrates the covariance.
    • The variable on the left stack is of type delegate F<out T>( ), where the type parameter is a class named Base.
    • The delegate actually constructed on the right is declared by the type parameter of class Derived, which is derived from class Base. This is good because when the delegate is called, the method returns a reference to the object of the derived type, which is also a reference to the base class, which is exactly what the calling code expects.
  • The following figure illustrates the comparison.
    • The variables on the left stack are of type delegate void F<in T>(T p), and the type parameters here are of type Derived. The delegate actually constructed on the right is declared by the type parameter of class Base, which is the base class of class Derived. This is good because when the delegate is called, the calling code passes an object of derived type to the method, which requires an object of base type. The method can operate freely on the basic part of the object as expected.

Image

***图 17-16。*协方差和逆变的比较

界面中的协变和逆变

现在,您应该对应用于代理的协变和逆变有所了解。同样的原则也适用于接口,包括在接口声明中使用关键字outin的语法。

以下代码显示了一个将协方差用于接口的示例。关于代码需要注意的事项如下:

  • A generic interface is declared with the type parameter T. out The type parameter specified by the keyword is covariant.
  • The generic class SimpleReturn implements the generic interface.
  • Method DoSomething shows how a method takes interface as a parameter. This method takes the general IMyIfc interface constructed by type Animal as its parameter.

该代码的工作方式如下:

  • The first two lines of Main use class Dog to create and initialize the construction instance of generic class SimpleReturn.
  • The next line assigns the object to a variable on the stack, which is declared as the constructor interface type IMyIfc<Animal>. Pay attention to some things about this statement:
    • The type on the left of the assignment is an interface type-not a class.
    • Even if the interface types do not exactly match, the compiler allows them, because there is a covariant out specifier in the interface declaration.
  • Finally, the code calls the method DoSomething with the constructed covariant class that implements the interface.

`    class Animal { public string Name; }
class Dog: Animal;
Keyword for covariance

interface IMyIfc
{
T GetFirst();
}

class SimpleReturn: IMyIfc
{
public T[] items = new T[2];
public T GetFirst() { return items[0]; }
}

class Program
{
static void DoSomething(IMyIfc returner)
{
Console.WriteLine(returner.GetFirst().Name);
}

static void Main( )
{
SimpleReturn dogReturner = new SimpleReturn();
dogReturner.items[0] = new Dog() ;

IMyIfc animalReturner = dogReturner;

DoSomething(dogReturner);
}
}`

该代码产生以下输出:


Avonlea


更多关于方差

前两节解释了显式协方差和逆变。还有一种情况,编译器自动识别某个构造的委托是协变的或逆变的,并自动进行类型强制。当对象还没有分配类型时,就会发生这种情况。下面的代码显示了一个示例。

Main的第一行从一个返回类型是Dog对象而不是Animal对象的方法创建了一个Factory<Animal>类型的构造委托。当Main创建这个委托时,赋值操作符右边的方法名还不是委托对象,因此没有委托类型。此时,编译器可以确定该方法匹配委托的类型,除了它的返回类型是类型Dog而不是类型Animal。编译器足够聪明,能够意识到这是一个协变关系,并创建构造类型,将其赋给变量。

比较一下Main第三行和第四行的赋值。在这些情况下,等号右边的表达式已经是委托,因此具有委托类型。因此,这些需要委托声明中的out说明符来通知编译器允许它们协变。

`   class Animal                // Base class
class Dog : Animal                               // Derived class

class Program
{
delegate T Factory();

static Dog MakeDog() { return new Dog(); }

static void Main()
{
Factory animalMaker1 = MakeDog;      // Coerced implicitly

Factory    dogMaker     = MakeDog;
Factory animalMaker2 = dogMaker;     // Requires the out specifier

Factory animalMaker3
= new Factory(MakeDog);       // Requires the out specifier
}
}`

关于方差,你还应该知道其他一些重要的事情:

  • As you can see, it is a safe problem for variance processing to replace a derived type with a basic type, and vice versa. Therefore, variance only applies to reference types-because other types cannot be derived from value types.
  • Explicit differences using the keywords in and out only apply to delegates and interfaces-not to classes, structures or methods.
  • Delegates and interface type parameters that don't contain in or out keywords are called invariants . These types cannot be used in covariant or inversion.

                         Contravariant <ins>  ↓  </ins> delegate T Factory<<ins>out R</ins>, in S, T>( ); ↑          ↑ Covariant        Invariant

十八、枚举器和迭代器

枚举器和可枚举类型

在《??》第十二章中,你看到了你可以使用一个foreach语句循环遍历一个数组的元素。在这一章中,你将仔细观察数组,看看为什么它们可以被foreach语句处理。您还将看到如何使用迭代器将这种能力添加到您自己的用户定义的类中。

使用 foreach 语句

当您对一个数组使用foreach语句时,该语句会一个接一个地显示数组中的每个元素,允许您读取它的值。例如,下面的代码声明了一个包含四个元素的数组,然后使用一个foreach循环打印出各项的值:

`   int[] arr1 = { 10, 11, 12, 13 };                // Define the array.

foreach (int item in arr1)                      // Enumerate the elements.
Console.WriteLine("Item value:  {0}", item);`

该代码产生以下输出:


Item value:  10 Item value:  11 Item value:  12 Item value:  13


为什么这适用于数组?原因是数组可以根据请求产生一个名为枚举器的对象。枚举器是一个对象,它可以根据请求依次返回数组的元素。枚举器“知道”项目的顺序,并跟踪它在序列中的位置。然后,当被请求时,它返回当前项。

对于具有枚举数的类型,必须有一种检索它的方法。检索对象的枚举数的方法是调用对象的GetEnumerator方法。实现了GetEnumerator方法的类型被称为可枚举类型,或者简称为可枚举类型。数组是可枚举的。

图 18-1 说明了枚举数和枚举数的关系。

Image

图 18-1 。枚举器和枚举器概述

foreach构造被设计成使用可枚举的。只要要迭代的对象是可枚举类型,比如数组,它就会执行以下操作:

  • Get the enumerator of the object by calling its GetEnumerator method.
  • It requests each item from the enumerator and uses it as iteration variable for your code to read (but not change).

                           Must be enumerable ↓ foreach( *Type VarName* in *EnumerableObject* ) { ... }

IEnumerator 接口

一个枚举器实现了IEnumerator接口,该接口包含三个函数成员:CurrentMoveNextReset

  • Current is the attribute of the item at the current position in the return sequence.
    • It is a read-only property.
    • It returns a reference of type object, so it can return any type of object.
  • MoveNext is a method to advance the position of the enumerator to the next item in the collection. It also returns a Boolean value indicating whether the new position is a valid position or beyond the end of the sequence.
    • If the new position is valid, the method returns to true.
    • If the new position is invalid (that is, the current position exceeds the end point), the method returns false.
    • The initial position of the enumerator is before the first item in the sequence, so MoveNext must call before accesses Current for the first time.
  • Reset is a method to reset the position to the initial state.

图 18-2 展示了一个由三个项目组成的集合,显示在图的左边,以及它的枚举器,显示在右边。在图中,枚举器是一个名为ArrEnumerator的类的实例。

Image

图 18-2 。小集合的枚举器

枚举器跟踪序列中当前项目的方式完全依赖于实现。它可能被实现为对一个对象的引用、一个索引值或者其他完全不同的东西。对于内置的一维数组类型,它只是项目的索引。

图 18-3 显示了三个项目集合的枚举器的状态。这些状态被标记为 1 到 5。

  • Note that in state 1, the initial position of the enumerator is 1 (that is, before the first element of the collection).
  • Every transition between states is caused by a call to MoveNext, which will raise the position in the sequence. Every call to MoveNext between state 1 and state 4 returns true. However, in the transition between States 4 and 5, the position ends at the last item in the set, so the method returns false.
  • In the final state, any further call to MoveNext returns false.

Image

图 18-3 。计数器的状态

给定一个集合的枚举器,您应该能够通过使用MoveNextCurrent成员循环遍历集合中的项目来模拟一个foreach循环。例如,您知道数组是可枚举的,所以下面的代码手动执行foreach语句自动执行的操作。事实上,当你写一个foreach循环时,C# 编译器会生成与此非常相似的代码(当然是在 CIL)。

   static void Main() { int[] MyArray = { 10, 11, 12, 13 };           // Create an array. Get and store the enumerator. <ins>          ↓            </ins> IEnumerator ie = MyArray.GetEnumerator(); Move to the next position. <ins>       ↓     </ins> while ( ie.MoveNext() ) {               Get the current item. <ins>    ↓    </ins> int i = (int) ie.Current; Console.WriteLine("{0}", i);               // Write it out. } }

这段代码产生以下输出,就像您使用了内置的foreach语句一样:


10 11 12 13


图 18-4 说明了代码示例中数组的结构。

Image

***图 18-4。*美国 .NET 数组类实现 IEnumerable。

IEnumerable 接口

可枚举类是实现IEnumerable接口的类。接口只有一个成员,方法GetEnumerator,它返回对象的枚举数。

图 18-5 显示了类MyClass,该类有三项要枚举,通过实现GetEnumerator方法来实现IEnumerable接口。

Image

图 18-5 。GetEnumerator 方法返回该类的枚举器对象。

下面的代码显示了可枚举类的声明形式:

   using System.Collections; Implements the IEnumerable interface ↓ class MyClass : IEnumerable { public IEnumerator GetEnumerator { ... } ...     ↑ }     Returns an object of type IEnumerator

下面的代码给出了一个可枚举类的例子,它使用了一个名为ColorEnumerator的枚举器类,它实现了IEnumerator。我将在下一节展示ColorEnumerator的实现。

`   using System.Collections;

class MyColors: IEnumerable
{
string[] Colors = { "Red", "Yellow", "Blue" };

public IEnumerator GetEnumerator()
{
return new ColorEnumerator(Colors);
}                      ↑
}            An instance of the enumerator class`

使用 IEnumerable 和 IEnumerator 的例子

下面的代码展示了一个名为Spectrum的可枚举类及其枚举器类ColorEnumerator的完整示例。类Program在方法Main中创建了一个MyColors的实例,并在foreach循环中使用它。

`using System;
using System.Collections;

class ColorEnumerator : IEnumerator
{
string[] _colors;
int      _position = -1;

public ColorEnumerator( string[] theColors )        // Constructor
{
_colors = new string[theColors.Length];
for ( int i = 0; i < theColors.Length; i++ )
_colors[i] = theColors[i];
}

public object Current                               // Implement Current.
{
get
{
if ( _position == -1 )
throw new InvalidOperationException();
if ( _position >= _colors.Length )
throw new InvalidOperationException();

return _colors[_position];
}
}

public bool MoveNext()                              // Implement MoveNext.
{
if ( _position < _colors.Length - 1 )
{
_position++;
return true;
}
else
return false;
}

public void Reset()                                 // Implement Reset.

}    class Spectrum : IEnumerable
{
string[] Colors = { "violet", "blue", "cyan", "green", "yellow", "orange", "red" };

public IEnumerator GetEnumerator()
{
return new ColorEnumerator( Colors );
}
}

class Program
{
static void Main()
{
Spectrum spectrum = new Spectrum();
foreach ( string color in spectrum )
Console.WriteLine( color );
}
}`

该代码产生以下输出:


violet blue cyan green yellow orange red


通用枚举接口

到目前为止,我描述的枚举接口都是非通用版本。实际上,您应该主要使用接口的通用版本,即IEnumerable<T>IEnumerator<T>。它们被称为泛型是因为它们使用 C# 泛型,这在第十七章中有所涉及。使用它们与使用非泛型形式基本相同。

两者之间的本质区别如下:

  • In the form of non-universal interface
    • The GetEnumerator method of interface IEnumerable returns an enumerator class instance that implements IEnumerator.
    • The class that implements IEnumerator implements the property Current, which returns a reference of type object, and then you must convert it to the actual type of the object.
  • Use common interface form.
    • The GetEnumerator method of interface IEnumerable<T> returns an instance of the class that implements IEnumerator<T>.
    • The class that implements IEnumerator<T> implements the property Current, which returns an instance of the actual type instead of a reference to the base class object.

然而,需要注意的最重要的一点是,到目前为止,我们看到的非泛型接口实现不是类型安全的。它们返回对类型object的引用,然后必须将其转换为实际类型。

然而,使用通用接口,枚举器是类型安全的,返回对实际类型的引用。如果您通过实现接口来创建自己的可枚举数,这是您应该采用的方法。非泛型接口形式适用于 C# 2.0 引入泛型之前开发的遗留代码。

虽然通用版本与非通用版本相同或者更容易使用,但是它们的结构稍微复杂一些。图 18-6 和 18-7 说明了它们的结构。

Image

图 18-6 。实现 IEnumerator < T >接口的类的结构

Image

**图 18-7。**实现 IEnumerable < T >接口的类的结构

迭代器

可枚举类和枚举器在 .NET 集合类,所以熟悉它们的工作方式很重要。但是现在您已经知道了如何创建自己的可枚举类和枚举器,您可能会很高兴地了解到,从 C# 2.0 开始,该语言提供了一种更简单的创建枚举器和枚举器的方法。事实上,编译器会为您创建它们。产生它们的结构被称为迭代器。您可以在任何需要使用手动编码的枚举器或枚举器的地方使用由迭代器生成的枚举器和枚举器。

在我解释细节之前,我们先来看两个例子。下面的方法声明实现了一个迭代器,该迭代器生成并返回一个枚举数。

  • Iterator returns a universal enumerator, which returns three items of type string.

yield return

这是枚举

        Return an enumerator that returns strings. <ins>        ↓              </ins> public IEnumerator<string> BlackAndWhite()                 // Version 1 { yield return "black";                                   // yield return yield return "gray";                                    // yield return yield return "white";                                   // yield return }

下面的方法声明是产生相同结果的另一个版本:

`        Return an enumerator that returns strings.
           ↓       
public IEnumerator BlackAndWhite()                  // Version 2
{
string[] theColors = { "black", "gray", "white" };

for (int i = 0; i < theColors.Length; i++)
yield return theColors[i];                            // yield return
}`

我还没有解释yield return语句,但是在检查这些代码段时,您可能会觉得这段代码有些不同。好像不太对。yield return语句到底是做什么的?

例如,在第一个版本中,如果方法在第一个yield return语句上返回,那么最后两个语句永远不会到达。如果它没有在第一个语句中返回,而是继续到方法的末尾,那么值会发生什么呢?在第二个版本中,如果循环体中的yield return语句在第一次迭代时返回,那么循环将永远不会到达任何后续迭代。

除此之外,枚举器并不只是一次性返回所有的元素——它会在每次访问Current属性时返回一个新值。那么,这如何给你一个枚举数呢?显然,这段代码不同于之前展示的任何代码。

迭代器块

一个迭代器块是一个包含一个或多个yield语句的代码块。以下三种类型的代码块都可以是迭代器块:

  • A legal system
  • A legal system
  • A legal system

迭代器块的处理方式不同于其他块。其他块包含被强制处理的语句序列。也就是说,执行块中的第一条语句,然后执行后续语句,最后控制权离开块。

另一方面,迭代器块不是一次执行的命令序列。相反,它是声明性的;它描述了您希望编译器为您生成的枚举数类的行为。迭代器块中的代码描述了如何枚举元素。

迭代器块有两个特殊的语句:

yield return

yield break

编译器获取如何枚举项的描述,并使用它来构建枚举器类,包括所有必需的方法和属性实现。结果类嵌套在声明迭代器的类中。

根据迭代器块使用的返回类型,你可以让迭代器产生一个枚举器或者一个可枚举器,如图图 18-8 所示。

Image

***图 18-8。*你可以让一个迭代器块产生一个枚举器或者一个可枚举器,这取决于你指定的返回类型。

使用迭代器创建枚举器

下面的代码演示了如何使用迭代器创建一个可枚举的类。

  • Method BlackAndWhite is an iterator block that generates a method that returns the enumerator of class MyClass.
  • MyClass also implements the method GetEnumerator, which just calls BlackAndWhite and returns the enumerator returned by BlackAndWhite.
  • Note that in Main, you can directly use the instance of this class in foreach statement, because this class implements GetEnumerator, so it is enumerable.

`   class MyClass
{
public IEnumerator GetEnumerator()
{
return BlackAndWhite();                   // Returns the enumerator
}
Returns an enumerator
        ↓          
public IEnumerator BlackAndWhite()  // Iterator
{
yield return "black";
yield return "gray";
yield return "white";
}
}

class Program
{
static void Main()
{
MyClass mc = new MyClass();
Use the instance of MyClass.

foreach (string shade in mc)
Console.WriteLine(shade);
}
}`

该代码产生以下输出:


black gray white


图 18-9 左边显示了MyClass的代码,右边显示了结果对象。注意有多少是由编译器自动构建的。

  • The code of the iterator is shown on the left side of the figure, showing that its return type is IEnumerator<string>.
  • On the right side of the diagram, the diagram shows that the nested class implements IEnumerator<string>.

Image

图 18-9 。生成枚举器的迭代器块

使用迭代器创建一个可枚举

前面的例子创建了一个包含两部分的类:产生返回枚举数的方法的迭代器和返回枚举数的GetEnumerator方法。在这个例子中,迭代器产生一个可枚举的而不是一个的枚举器。这个例子和上一个例子有一些重要的区别:

  • In the example, the iterator method BlackAndWhite returns a IEnumerator<string>, and MyClass implements the method GetEnumerator by returning BlackAndWhite. In this example, the iterator method BlackAndWhite returns a IEnumerable<string> instead of a IEnumerator<string>. Therefore, MyClass implements its GetEnumerator method by first calling the method BlackAndWhite to obtain an enumerable object, then calling the GetEnumerator method of the object and returning the result.
  • Note that in the foreach statement of Main, you can use an instance of the class or call BlackAndWhite directly, because it returns an enumerable one. There are both ways.

`   class MyClass
{
public IEnumerator GetEnumerator()
{
IEnumerable myEnumerable = BlackAndWhite(); // Get enumerable.
return myEnumerable.GetEnumerator();                // Get enumerator.
}        Returns an enumerable
        ↓           
public IEnumerable BlackAndWhite()
{
yield return "black";
yield return "gray";
yield return "white";
}
}

class Program
{
static void Main()
{
MyClass mc = new MyClass();
Use the class object.

foreach (string shade in mc)
Console.Write("{0}  ", shade);
Use the class iterator method.
     ↓       
foreach (string shade in mc.BlackAndWhite())
Console.Write("{0}  ", shade);
}
}`

这段代码产生以下输出:


black  gray  white  black  gray  white


图 18-10 说明了代码中可枚举迭代器产生的泛型可枚举。

  • The code of the iterator is shown on the left side of the figure, showing that its return type is IEnumerable<string>.
  • On the right side of the diagram, the diagram shows that the nested class implements both IEnumerator<string> and IEnumerable<string>.

Image

***图 18-10。*编译器生成一个既是枚举器又是枚举器的类。它还产生返回可枚举对象的方法 BlackAndWhite。

常见迭代器模式

前两节展示了您可以创建一个迭代器来返回一个枚举数或一个枚举数。图 18-11 总结了如何使用常见的迭代器模式。

  • When you implement an iterator that returns an enumerator, you must make class enumerable by implementing GetEnumerator so that it returns the enumerator returned by the iterator. This is shown on the left side of the diagram.
  • In a class, when you implement an iterator that returns enumerable, you can make the class itself enumerable or un-enumerable by making the class implement GetEnumerator or not.
    • If you implement GetEnumerator, let it call the iterator method to get an instance of the automatically generated class that implements IEnumerable. Next, return the enumerator built by GetEnumerator from this IEnumerable object, as shown in the right figure.
    • If you decide not to make the class itself enumerable, you can still use the enumerable returned by the iterator by not implementing GetEnumerator, and call the iterator method directly, as shown in the second foreach statement on the right.

Image

图 18-11 。常见的迭代器模式

产生多个可枚举数

在下面的例子中,类Spectrum有两个可枚举的迭代器——一个从紫外端到红外端枚举光谱的颜色,另一个从相反的方向枚举。请注意,尽管它有两个返回可枚举的方法,但该类本身是不可枚举的,因为它没有实现GetEnumerator

`   using System;
using System.Collections.Generic;

class Spectrum
{
string[] colors = { "violet", "blue", "cyan", "green", "yellow", "orange", "red" };
Returns an enumerable

public IEnumerable UVtoIR()
{
for ( int i=0; i < colors.Length; i++ )
yield return colors[i];
}
Returns an enumerable

public IEnumerable IRtoUV()
{
for ( int i=colors.Length - 1; i >= 0; i-- )
yield return colors[i];
}
}

class Program
{
static void Main()
{
Spectrum spectrum = new Spectrum();

foreach ( string color in spectrum.UVtoIR() )
Console.Write( "{0}  ", color );
Console.WriteLine();

foreach ( string color in spectrum.IRtoUV() )
Console.Write( "{0}  ", color );
Console.WriteLine();
}
}`

该代码产生以下输出:


violet  blue  cyan  green  yellow  orange  red red  orange  yellow  green  cyan  blue  violet


迭代器作为属性

前面的例子使用迭代器产生一个具有两个可枚举数的类。这个例子说明了两件事。首先,它使用迭代器产生一个具有两个枚举器的类。第二,它展示了迭代器如何实现为属性而不是方法。

代码声明了两个属性,这两个属性定义了两个不同的枚举数。根据布尔变量_listFromUVtoIR的值,GetEnumerator方法返回两个枚举器中的一个或另一个。如果_listFromUVtoIRtrue,则返回UVtoIR枚举数。否则,返回IRtoUV枚举器。

`using System;
using System.Collections.Generic;

class Spectrum
{
bool _listFromUVtoIR;

string[] colors = { "violet", "blue", "cyan", "green", "yellow", "orange", "red" };

public Spectrum( bool listFromUVtoIR )

public IEnumerator GetEnumerator()
{
return _listFromUVtoIR
? UVtoIR
: IRtoUV;
}

public IEnumerator UVtoIR
{
get
{
for ( int i=0; i < colors.Length; i++ )
yield return colors[i];
}
}

public IEnumerator IRtoUV
{
get
{
for ( int i=colors.Length - 1; i >= 0; i-- )
yield return colors[i];
}
}
}    class Program
{
static void Main()
{
Spectrum startUV = new Spectrum( true );
Spectrum startIR = new Spectrum( false );

foreach ( string color in startUV )
Console.Write( "{0}  ", color );
Console.WriteLine();

foreach ( string color in startIR )
Console.Write( "{0}  ", color );
Console.WriteLine();
}
}`

该代码产生以下输出:


violet  blue  cyan  green  yellow  orange  red red  orange  yellow  green  cyan  blue  violet


幕后用迭代器

以下是关于迭代器需要知道的一些其他重要的事情:

  • Iterator needs System.Collections.Generic namespace, so it should be included with using instruction.
  • Reset method is not supported in enumerator generated by compiler. It is implemented because the interface needs it, but if it is called, the implementation will throw a System.NotSupportedException exception. Note that the Reset method is shown in gray in Figure 18-9 .

在幕后,编译器生成的枚举器类是一个状态机,有四种状态:

  • Before : the initial state before MoveNext is called for the first time.
    ** Running : the state entered when MoveNext is called. In this state, the enumerator determines and sets the position of the next item. When it encounters a yield return, yield break or the end of the iterator body, it exits the state.* Pending : The state machine waits for the next call of MoveNext.* After : There are no more items to enumerate.*

**如果状态机处于之前的或暂停状态,并且调用了MoveNext方法,它将进入运行状态。在运行状态下,确定集合中的下一个项目并设定位置。

如果有更多项目,状态机进入暂停状态。如果没有更多的项目,它将在后进入状态,并保持不变。图 18-12 显示了状态机。

Image

**图 18-12。迭代器状态机

十九、LINQ 简介

什么是 LINQ?

在关系数据库系统中,数据被很好地组织成规范化的表,并使用一种非常简单但功能强大的查询语言——SQL 来访问。SQL 可以处理数据库中的任何数据集,因为数据是按照严格的规则组织到表中的。

然而,在程序中,与数据库相反,数据存储在完全不同的类对象或结构中。因此,还没有通用的查询语言来从数据结构中检索数据。从对象中检索数据的方法一直是作为程序的一部分定制设计的。然而,LINQ 使查询对象集合变得很容易。

以下是 LINQ 的重要高级特征:

  • LINQ stands for language comprehensive query , pronounced as link .
  • It's an extension of LINQ. NET Framework, and allows you to query the data collection in a way similar to querying the database with SQL. With LINQ, you can query data from databases, program object collections, XML documents, etc.

下面的代码显示了一个使用 LINQ 的简单示例。在这段代码中,被查询的数据源只是一个由int组成的数组。查询的定义是带有fromselect关键字的语句。虽然在这个语句中查询是由定义的,但是直到底部的foreach语句中需要结果时才真正执行。

`   static void Main()
{
int[] numbers = { 2, 12, 5, 15 };         // Data source

IEnumerable lowNums =                // Define and store the query.
from n in numbers
where n < 10
select n;

foreach (var x in lowNums)                // Execute the query.
Console.Write("{0}, ", x);
}`

该代码产生以下输出:


2, 5,


LINQ 供应商

在前面的例子中,数据源只是一个由int组成的数组,它是程序的内存对象。然而,LINQ 可以处理许多不同类型的数据源,比如 SQL 数据库、XML 文档和许多其他数据源。然而,对于每种数据源类型,都必须有一个代码模块根据该数据源类型实现 LINQ 查询。这些代码模块被称为 LINQ 提供者。关于 LINQ 提供商的要点如下:

  • Microsoft provides LINQ providers for some common data source types, as shown in figure and figure 19-1 .
  • You can use any LINQ-supported language (C# in our example) to query any data source type with LINQ provider. Third parties are constantly developing new LINQ providers for various data source types.

Image

图 19-1 。LINQ、支持 LINQ 的语言和 LINQ 提供者

有整本书致力于 LINQ 的所有形式和微妙之处,但这显然超出了本章的范围。相反,本章将向您介绍 LINQ,并解释如何将它用于程序对象(LINQ 到对象)和 XML (LINQ 到 XML)。

匿名类型

在详细介绍 LINQ 的查询特性之前,我将首先介绍一种语言特性,它允许您创建未命名的类类型。毫不奇怪,这些被称为匿名类型。匿名类型通常用于 LINQ 查询的结果。

第六章讲述了对象初始化器,它允许你在使用对象创建表达式时初始化一个新类实例的字段和属性。提醒您一下,这种对象创建表达式由三部分组成:关键字new、类名或构造函数以及对象初始化器。对象初始化器由一组花括号之间的逗号分隔的成员初始化器列表组成。

创建匿名类型的变量使用相同的形式——但是没有类名或构造函数。以下代码行显示了匿名类型的对象创建表达式形式:

   No class name        Anonymous object initializer ↓ <ins>                      ↓                      </ins> new  { <ins>FieldProp = InitExpr</ins>, <ins>FieldProp = InitExpr</ins>, ...} ↑                     ↑ Member initializer          Member initializer

下面的代码显示了一个创建和使用匿名类型的示例。它创建了一个名为student的变量,其匿名类型有两个string属性和一个int属性。注意在WriteLine语句中,实例的成员被访问,就像它们是一个命名类型的成员一样。

`   static void Main( )
{
var student = new {Name="Mary Jones", Age=19, Major="History"};
↑                                         ↑
Must use var                            Anonymous object initializer

Console.WriteLine("{0}, Age {1}, Major: {2}",
student.Name, student.Age, student.Major);
}`

该代码产生以下输出:


Mary Jones, Age 19, Major: History


关于匿名类型,需要了解的重要事项如下:

  • Anonymous types can only be used for local variables, not class members.
  • Because anonymous types have no name, the keyword var must be used as the variable type.
  • You can't assign attributes to an object of anonymous type. Attributes created by the compiler for anonymous types are read-only.

当编译器遇到匿名类型的对象初始化器时,它用自己构造的私有名称创建一个新的类类型。对于每个成员初始化器,它推断其类型并创建一个只读属性来访问其值。属性与成员初始值设定项同名。一旦构造了匿名类型,编译器就创建该类型的对象。

除了成员初始值设定项的赋值形式,匿名类型对象初始值设定项还允许其他两种形式:简单标识符和成员访问表达式。这些形式称为投影初始化器,不使用赋值表达式。相反,它们使用标识符或被访问对象的成员名称作为匿名类型成员的名称。下面的变量声明显示了所有三种形式。第一个成员初始化器是赋值形式的。第二个是成员访问表达式,第三个是标识符。

   var student = new { Age = 19, Other.Name, Major };

例如,下面的代码显示了如何使用它。请注意,投影初始值设定项必须在匿名类型声明之前定义。Major是局部变量,NameOther类的静态字段。

`   class Other
{
static public string Name = "Mary Jones";
}

class Program
{
static void Main()
{
string Major = "History";
Assignment form             Identifier
   ↓                    ↓
var student = new { Age = 19, Other.Name, Major};

Member access
Console.WriteLine("{0}, Age {1}, Major: {2}",
student.Name, student.Age, student.Major);
}
}`

该代码产生以下输出:


Mary Jones, Age 19, Major: History


刚刚显示的对象初始值设定项的投影初始值设定项形式与此处显示的赋值形式具有完全相同的结果:

   var student = new { Age = Age, Name = Other.Name, Major = Major};

如果编译器遇到另一个具有相同参数名、相同推断类型和相同顺序的匿名对象初始值设定项,它将重用已经创建的匿名类型,并只创建一个新实例,而不是新的匿名类型。

方法语法和查询语法

LINQ 为指定查询提供了两种语法形式:查询语法和方法语法。

  • Method syntax uses standard method calls. These methods come from a set of standard query operators, which I will describe later in this chapter.
  • The query syntax looks very much like an SQL statement. Query syntax is written in the form of query expression.
  • You can combine the two forms in a single query.

查询语法是一种声明性形式,这意味着您的查询描述了您想要返回的内容,但没有指定如何执行查询。方法语法是一种命令式形式,它指定了调用查询方法的确切顺序。使用查询语法表达的查询由 C# 编译器翻译成方法调用形式。这两种形式的运行时性能没有区别。

Microsoft 建议使用查询语法,因为它可读性更强,更清楚地表达了您的查询意图,因此不容易出错。但是,有些运算符只能使用方法语法编写。

下面的代码显示了这两个窗体和一个组合窗体的示例。在方法语法部分,注意到Where方法的参数使用了 lambda 表达式,如第十三章所述。在这一章的后面我会谈到它在 LINQ 的使用。

`static void Main( )
{
int[] numbers = { 2, 5, 28, 31, 17, 16, 42 };

var numsQuery = from n in numbers                    // Query syntax
where n < 20
select n;

var numsMethod = numbers.Where(x ⇒ x < 20);         // Method syntax

int numsCount = (from n in numbers                   // Combined
where n < 20
select n).Count();

foreach (var x in numsQuery)
Console.Write("{0}, ", x);
Console.WriteLine();

foreach (var x in numsMethod)
Console.Write("{0}, ", x);
Console.WriteLine();

Console.WriteLine(numsCount);
}`

该代码产生以下输出:


2, 5, 17, 16, 2, 5, 17, 16, 4


查询变量

LINQ 查询可以返回两种类型的结果之一:一个枚举,它列出满足查询参数的项目;或者单个值,称为标量,它是满足查询的结果的某种形式的汇总。

在下面的代码示例中,发生了以下情况:

  • The first statement creates an array of int and initializes it with three values.
  • The second statement specifies a LINQ query that lists the results of the query.
  • The third statement executes the query, and then calls the LINQ method (Count), which returns the number of items returned by the query. I will introduce operators that return scalars later in this chapter, such as Count.

`   int[] numbers = { 2, 5, 28 };

IEnumerable lowNums = from n in numbers       // Returns an enumerator
where n < 20
select n;

int numsCount            = (from n in numbers       // Returns an int
where n < 20
select n).Count();`

第二个和第三个语句等号左边的变量叫做查询变量。尽管在示例语句中明确给出了查询变量的类型(IEnumerable<T>int),但是您也可以使用var关键字来代替类型名,并让编译器推断查询变量的类型。

理解如何使用查询变量很重要。执行上述代码后,查询变量lowNums是否包含查询结果而非。相反,编译器已经创建了代码,如果稍后在代码中调用该代码,将运行该代码来执行查询。

然而,查询变量numsCount包含一个实际的整数值,它只能通过实际运行查询来获得。

查询执行时间的差异可以总结如下:

  • If the query expression returns an enumeration, the query will not be executed until the enumeration is processed.
  • If the enumeration is processed multiple times, the query is executed multiple times.
  • If the data changes between the time when the enumeration is generated and the time when the query is executed, the query will be run against the new data.
  • If the query expression returns a scalar, the query is executed immediately and the result is stored in the query variable.

查询表达式的结构

一个查询表达式由一个from子句后跟一个查询体组成,如图图 19-2 所示。关于查询表达式,需要了解的一些重要信息如下:

子句必须按所示顺序出现。* The required two parts are from clause and select...group clause.* Other terms are optional.* In the LINQ query expression, the select clause is at the end of the expression. This is different from SQL, where the SELECT statement is located at the beginning of the query. One of the reasons for using this location in C# is that it allows Visual Studio's intellisense to provide you with more options when you enter code.* There can be any number of from...let...where clauses, as shown in the figure.

Image

图 19-2 。查询语句的结构由一个 from 子句后跟一个查询体组成。

from 子句

from子句指定用作数据源的数据集合。它还引入了迭代变量。关于from条款的要点如下:

迭代变量

  • The syntax of from clause is as follows, where
    • *Type* is the type of element in the set. This is optional because the compiler can infer the type from the collection.
    • *Item* is the name of the iteration variable.
    • *Items* is the name of the set to be queried. Collections must be enumerable, as described in Chapter 18.

      Iteration variable declaration <ins>     ↓   </ins> from Type Item in *Items*

以下代码显示了用于查询四个int的数组的查询表达式。迭代变量item将表示数组中四个元素中的每一个,并将被其后的whereselect子句选择或拒绝。这段代码省略了迭代变量的可选类型(int)。

`   int[] arr1 = {10, 11, 12, 13};
Iteration variable

var query = from item in arr1
where item < 13      ←  Uses the iteration variable
select item;         ←  Uses the iteration variable

foreach( var item in query )
Console.Write("{0}, ", item );`

该代码产生以下输出:


10, 11, 12,


图 19-3 显示了from子句的语法。同样,类型说明符是可选的,因为它可以被编译器推断出来。可以有任意数量的可选join子句。

Image

图 19-3 。from 子句的语法

尽管 LINQ from条款和foreach声明之间有很大的相似性,但也有几个主要区别:

  • The foreach statement forcibly specifies that the items in the collection should be considered in order, from the first to the last. The from clause declares that every item in the collection must be considered, but does not assume the order.
  • The foreach statement executes its body where it is encountered in the code. On the other hand, the from clause does nothing. It creates a background code object that can be queried later. Only when the control flow of the program encounters a statement that accesses the query variable will the query be executed.

连接子句

LINQ 中的join子句很像 SQL 中的JOIN子句。如果您熟悉 SQL 中的连接,那么 LINQ 中的连接对您来说并不陌生,除了您现在可以在对象集合和数据库表上执行它们。如果您是 joins 的新手或者需要复习,那么下一节将帮助您理清思路。

关于join首先要知道的重要事情如下:

  • You can use a connection to merge data from two or more collections.
  • A join operation takes two sets and creates a new temporary object set, in which each object in the new set contains all fields of one object from the two initial sets.

下面显示了一个join的语法。它指定第二个集合将与前一个子句中的集合相联接。注意上下文关键字equals,它必须用于比较字段,而不是==操作符。

  Keyword         Keyword        Keyword     Keyword ↓                ↓             ↓          ↓ join <ins>*Identifier* in *Collection2*</ins> on <ins>*Field1* equals *Field2*</ins> ↑                       ↑ Specify additional collection        The fields to compare and ID to reference it.              for equality

图 19-4 说明了join子句的语法。

Image

图 19-4 。join 子句的语法

以下带注释的语句显示了一个join子句的示例:

Image

什么是联接?

LINQ 的 A join获取两个集合并创建一个新的集合,其中每个元素都有来自两个原始集合的元素的成员。

例如,下面的代码声明了两个类:StudentCourseStudent

  • The object of type Student contains the student's last name and student ID.
  • The object of type CourseStudent represents the students who have registered for the course, including the course name and student ID number.

`   public class Student
{
public int    StID;
public string LastName;
}

public class CourseStudent
{
public string CourseName;
public int    StID;
}`

图 19-5 显示了一个项目中的情况,其中有三个学生和三门课程,学生注册了各种课程。该程序有一个名为students的数组,包含Student个对象,还有一个名为studentsInCourses的数组,包含CourseStudent个对象,针对每门课程中注册的每个学生。

Image

图 19-5 。参加各种课程的学生

现在假设您想要获取某门课程中每个学生的姓氏。students数组有姓氏,但没有班级注册信息。studentsInCourses数组有课程注册信息,但没有学生姓名。但是我们可以使用学号(StID)将这些信息联系在一起,这对两个数组的对象都是通用的。您可以通过在StID字段上连接来实现这一点。

图 19-6 显示了连接是如何工作的。左栏显示的是students数组,右栏显示的是studentsInCourses数组。如果我们获取第一个学生记录,并将其 ID 与每个studentsInCourses对象中的学生 ID 进行比较,我们会发现其中两个匹配,如中间列顶部所示。如果我们对另外两个学生做同样的事情,我们会发现第二个学生选了一门课,第三个学生选了两门课。

中间一列中的五个灰色对象表示字段StID上两个数组的连接。每个对象包含三个字段:来自Students类的LastName字段,来自CourseStudent类的CourseName字段,以及两个类共有的StID字段。

Image

图 19-6 。两个对象数组及其在字段 StId 上的连接

下面的代码将整个例子放在一起。该查询查找所有学习历史课程的学生的姓氏。

`   class Program
{
public class Student {                         // Declare classes.
public int    StID;
public string LastName;
}

public class CourseStudent {
public string CourseName;
public int    StID;
}

static Student[] students = new Student[] {
new Student ,
new Student ,
new Student ,
};
// Initialize arrays.
static CourseStudent[] studentsInCourses = new CourseStudent[] {
new CourseStudent { CourseName = "Art",         StID = 1 },
new CourseStudent { CourseName = "Art",         StID = 2 },
new CourseStudent { CourseName = "History",     StID = 1 },
new CourseStudent { CourseName = "History",     StID = 3 },
new CourseStudent { CourseName = "Physics",     StID = 3 },
};

static void Main( )
{
// Find the last names of the students taking history.
var query = from s in students
join c in studentsInCourses on s.StID equals c.StID
where c.CourseName == "History"
select s.LastName;

// Display the names of the students taking history.
foreach (var q in query)
Console.WriteLine("Student taking History:  {0}", q);
}
}`

该代码产生以下输出:


Student taking History:  Carson Student taking History:  Fleming


此从。。。让。。。查询正文中的 where 部分

可选的from...let...where部分是查询体的第一部分。它可以包含组成它的三个子句中的任意一个——from子句、let子句和where子句。图 19-7 总结了三个子句的语法。

Image

图 19-7 。from 的语法。。。让。。。where 子句

from 子句

您看到查询表达式以必需的from子句开始,后面是查询体。主体本身可以从任意数量的附加from子句开始,其中每个后续的from子句指定一个附加的源数据集合,并引入一个新的迭代变量用于进一步的评估。所有from子句的语法和含义都是相同的。

下面的代码显示了这种用法的一个示例。

  • The first from clause is a required clause of the query expression.
  • The second from clause is the first clause of the query body.
  • The select clause creates objects of anonymous type.

`   static void Main()
{
var groupA = new[] { 3, 4, 5, 6 };
var groupB = new[] { 6, 7, 8, 9 };

var someInts = from a in groupA                ← Required first from clause
from b in groupB                ← First clause of query body
where a > 4 && b ⇐ 8
select new {a, b, sum = a + b}; ← Object of anonymous type

foreach (var a in someInts)
Console.WriteLine(a);
}`

该代码产生以下输出:


{ a = 5, b = 6, sum = 11 } { a = 5, b = 7, sum = 12 } { a = 5, b = 8, sum = 13 } { a = 6, b = 6, sum = 12 } { a = 6, b = 7, sum = 13 } { a = 6, b = 8, sum = 14 }


let 子句

let子句接受表达式的求值,并将其分配给一个标识符,以便在其他求值中使用。let子句的语法如下:

   let Identifier = Expression

例如,以下代码中的查询表达式将数组groupA的每个成员与数组groupB的每个元素配对。where子句从两个数组中删除两个和不等于 12 的每组整数。

`   static void Main()
{
var groupA = new[] { 3, 4, 5, 6 };
var groupB = new[] { 6, 7, 8, 9 };

var someInts = from a in groupA
from b in groupB
let sum = a + b        ← Store result in new variable.
where sum == 12
select new {a, b, sum};

foreach (var a in someInts)
Console.WriteLine(a);
}`

该代码产生以下输出:


{ a = 3, b = 9, sum = 12 } { a = 4, b = 8, sum = 12 } { a = 5, b = 7, sum = 12 } { a = 6, b = 6, sum = 12 }


where 子句

如果项目不符合指定的条件,则where子句会将它们排除在进一步考虑之外。where子句的语法如下:

   where BooleanExpression

关于where子句需要了解的重要事项如下:

  • A query expression can have any number of where clauses as long as they are in from...let...where paragraph.
  • A project must meet all where clauses to avoid further consideration and exclusion.

以下代码显示了一个包含两个where子句的查询表达式示例。where子句从两个数组中删除每组整数,其中两个数组的和不大于或等于 11,并且groupA中的元素不是值 4。所选的每组元素必须满足两个 where子句的条件。

`   static void Main()
{
var groupA = new[] { 3, 4, 5, 6 };
var groupB = new[] { 6, 7, 8, 9 };

var someInts = from int a in groupA
from int b in groupB
let sum = a + b
where sum >= 11          ← Condition 1
where a == 4             ← Condition 2
select new {a, b, sum};

foreach (var a in someInts)
Console.WriteLine(a);
}`

该代码产生以下输出:


{ a = 4, b = 7, sum = 11 } { a = 4, b = 8, sum = 12 } { a = 4, b = 9, sum = 13 }


order by 子句

orderby子句接受一个表达式,并根据该表达式按顺序返回结果项。

图 19-8 显示了orderby子句的语法。可选关键字ascendingdescending设置订单的方向。表达式通常是项的字段。该字段不一定是数值字段。它也可以是另一种可排序的类型,如字符串。

orderby

ascending

descending

  • There can be any number of orderby clauses and they must be separated by commas.

Image

图 19-8 。orderby 子句的语法

以下代码显示了一个按学生年龄排序的学生记录示例。请注意,学生信息数组存储在一个匿名类型数组中。

`   static void Main( ) {
var students = new []       // Array of objects of an anonymous type
{
new { LName="Jones",   FName="Mary",   Age=19, Major="History" },
new { LName="Smith",   FName="Bob",    Age=20, Major="CompSci" },
new { LName="Fleming", FName="Carol",  Age=21, Major="History" }
};

var query = from student in students
orderby student.Age      ← Order by Age.
select student;

foreach (var s in query) {
Console.WriteLine("{0}, {1}:  {2} - {3}",
s.LName, s.FName, s.Age, s.Major);
}
}`

该代码产生以下输出:


Jones, Mary:  19 - History Smith, Bob:  20 - CompSci Fleming, Carol:  21 - History


选择。。。集团条款

有两种类型的子句组成了select...group部分:select子句和group...by子句。虽然select...group部分前面的子句指定了数据源和要选择的对象,但是select...group部分执行以下操作:

  • The select clause specifies which parts of the selected object should be selected. It can specify any of the following:
    • Whole data item
    • A field in a data item.
    • A new object (or any other value) consisting of several fields in a data item.
  • The group...by clause is optional, and it specifies how to group the selected items. I will discuss the group...by clause later in this chapter.

图 19-9 显示了select...group子句的语法。

Image

***图 19-9。*选择的语法。。。集团条款

下面的代码展示了一个使用select子句选择整个数据项的例子。首先,程序创建一个匿名类型的对象数组。然后,查询表达式使用select语句选择数组中的每一项。

`   using System;
using System.Linq;
class Program {
static void Main() {
var students = new[]       // Array of objects of an anonymous type
{
new { LName="Jones",   FName="Mary",  Age=19, Major="History" },
new { LName="Smith",   FName="Bob",   Age=20, Major="CompSci" },
new { LName="Fleming", FName="Carol", Age=21, Major="History" }
};

var query = from s in students
select s;

foreach (var q in query)
Console.WriteLine("{0}, {1}: Age {2}, {3}",
q.LName, q.FName, q.Age, q.Major);
}
}`

该代码产生以下输出:


Jones, Mary: Age 19, History Smith, Bob: Age 20, CompSci Fleming, Carol: Age 21, History


您还可以使用select子句来选择对象的特定字段。例如,如果用下面两条语句替换上例中相应的两条语句,代码将只选择学生的姓氏。

`   var query = from s in students
select s.LName;

foreach (var q in query)
Console.WriteLine(q);`

通过这种替换,程序产生以下输出,仅打印姓氏:


Jones Smith Fleming


查询中的匿名类型

查询结果可以由源集合中的项、源集合中项的字段或匿名类型组成。

您可以在select子句中创建一个匿名类型,方法是用花括号将您想要包含在该类型中的以逗号分隔的字段列表括起来。例如,要使上一节中的代码只选择学生的姓名和专业,可以使用以下语法:

   select new { s.LastName, s.FirstName, s.Major }; ↑ Anonymous type

下面的代码在select子句中创建一个匿名类型,并在后面的WriteLine语句中使用它。

`   using System;
using System.Linq;

class Program
{
static void Main()
{
var students = new[]       // Array of objects of an anonymous type
{
new { LName="Jones",   FName="Mary",  Age=19, Major="History" },
new { LName="Smith",   FName="Bob",   Age=20, Major="CompSci" },
new { LName="Fleming", FName="Carol", Age=21, Major="History" }
};

var query = from s in students
select new { s.LName, s.FName, s.Major };

Create anonymous type.
foreach (var q in query)
Console.WriteLine("{0} {1} -- {2}",
q.FName, q.LName, q.Major );
}                                      •↑
}                             Access fields of anonymous type`

该代码产生以下输出:


Mary Jones -- History Bob Smith -- CompSci Carol Fleming -- History


群子句

group子句根据指定的标准对选定的对象进行分组。例如,对于前面示例中的学生数组,程序可以根据学生的专业对他们进行分组。

关于group条款需要了解的重要事项如下:

  • When items are included in the query results, they are grouped according to the values of specific fields. The attribute of grouping items is called key .
  • Queries with a group clause do not return an enumeration of items from the original source. Instead, it returns an enumerable value that enumerates the formed project groups.
  • The group itself is enumerable and can enumerate the actual items.

group子句的语法示例如下:

   group student by student.Major; ↑            ↑ Keyword       Keyword

例如,以下代码根据专业对学生进行分组:

`   static void Main( )
{
var students = new[]       // Array of objects of an anonymous type
{
new { LName="Jones",   FName="Mary",  Age=19, Major="History" },
new { LName="Smith",   FName="Bob",   Age=20, Major="CompSci" },
new { LName="Fleming", FName="Carol", Age=21, Major="History" }
};

var query = from student in students
group student by student.Major;

foreach (var s in query)            // Enumerate the groups.
{
Console.WriteLine("{0}", s.Key);

Grouping key
foreach (var t in s)             // Enumerate the items in the group.
Console.WriteLine("      {0}, {1}", t.LName, t.FName);
}
}`

这段代码产生以下输出:


History Jones, Mary Fleming, Carol CompSci Smith, Bob


图 19-10 显示了从查询表达式返回并存储在查询变量中的对象。

  • The object returned by the query is an enumerable object, which enumerates the groups obtained by the query.
  • Each group is distinguished by a field called Key.
  • Each group itself is enumerable and its items can be enumerated.

Image

***图 19-10。*group 子句返回对象集合的集合,而不是对象的集合。

查询延续:into 子句

查询 continuation 子句获取查询的一部分的结果,并为其指定一个名称,以便可以在查询的另一部分中使用。图 19-11 显示了查询延续的语法。

Image

***图 19-11。*查询延续子句的语法

例如,下面的查询连接了groupAgroupB,并将结果命名为groupAandB。然后从groupAandB执行简单的选择。

`   static void Main()
{
var groupA = new[] { 3, 4, 5, 6 };
var groupB = new[] { 4, 5, 6, 7 };

var someInts = from a in groupA
join b in groupB on a equals b
into groupAandB                ← Query continuation
from c in groupAandB
select c;

foreach (var a in someInts)
Console.Write("{0}  ", a);
}`

该代码产生以下输出:


4  5  6


标准查询运算符

标准查询操作符由一组称为应用编程接口(API)的方法组成 .NET 数组或集合。标准查询运算符的重要特征如下:

  • The collection object being queried is called a sequence, and the ienumerable < T > interface must be implemented, where t is the type.
  • Standard query operators use method syntax.
  • Some operators return IEnumerable objects (or other sequences), while others return scalars. Operators that return scalars immediately execute their queries and return a value instead of an enumerable object.
  • Many of these operators take predicates as parameters. Predicate is a method that takes an object as a parameter and returns true or false according to whether the object meets certain criteria.

例如,以下代码显示了运算符SumCount的用法,它们返回int s。请注意以下代码:

  • As a method , the operator acts directly on the object sequence , in this case, the array numbers.
  • The return type is not IEnumerable object, but int.

`   class Program
{
static int[] numbers = new int[] {2, 4, 6};

static void Main( )
{
int total   = numbers.Sum();
int howMany = numbers.Count();
↑               ↑       ↑
Scalar         Sequence   Operator
object
Console.WriteLine("Total: {0}, Count: {1}", total, howMany);
}
}`

该代码产生以下输出:


Total: 12, Count: 3


有 47 个标准查询运算符。它们对一个或多个序列进行操作。一个序列是任何实现IEnumerable<>接口的类。这包括诸如List<>Dictionary<>Stack<>Array这样的等级。标准查询操作符可以帮助您以非常强大的方式查询和操作这些类型的对象。

表 19-1 列出了这些运算符,并给出了足够的信息,让你知道每个运算符的用途和大致意思。然而,大多数都有几个重载,允许不同的选项和行为。你应该仔细阅读这个列表,熟悉这些可以节省你大量时间和精力的强大工具。当你需要使用它们的时候,你可以在网上查找完整的文档。

Image

Image

Image

标准查询操作符的签名

标准查询操作符是在类System.Linq.Enumerable中声明的方法。然而,这些方法不是普通的方法——它们是扩展泛型类IEnumerable<T>的扩展方法。

我在第七章和第十七章中介绍了扩展方法,但是这是一个让您了解如何扩展的好机会 .NET 使用它们。这将为您自己的代码提供一个很好的模型,并让您更好地理解标准查询操作符。

回顾一下,回想一下扩展方法是公共的静态方法,虽然在一个类中定义,但它们被设计成向另一个不同的类添加功能,这个类被列为第一个形参。这个形参前面必须有关键字this

例如,以下是三个标准查询操作符的签名:CountFirstWhere。乍一看,这些可能有点吓人。请注意以下关于签名的内容:

  • Because operators are generic methods, they have a generic parameter (T) associated with their names.
  • Because operators are extension methods of extension class ienumerable < T >, they meet the following syntax requirements:
    • They are declared as public and static.
    • They have a this extension indicator before the first parameter.
    • They take IEnumerable<T> as the first parameter type.

         Always                      Name and               First public, static                   generic param           parameter <ins>     ↓       </ins>                  <ins>  ↓ </ins>      <ins>             ↓             </ins> public static      int         Count<T>( this IEnumerable<T> source ); public static       T          First<T>( this IEnumerable<T> source ); public static IEnumerable<T>   Where<T>( this IEnumerable<T> source, ... ); ↑                    ↑ Return             Extension method type                 indicator

为了显示直接调用扩展方法和将其作为扩展调用之间的语法差异,下面的代码使用两种形式调用标准查询操作符CountFirst。两个操作符都只接受一个参数——对IEnumerable<T>对象的引用。

  • The Count operator returns a single value, that is, the count of all elements in the sequence.
  • The first operator returns the first element of the sequence.

在这段代码中,前两次使用了运算符,它们被直接调用,就像普通方法一样,将数组的名称作为第一个参数传递。但是,在下面的两行中,使用扩展语法调用它们,就好像它们是数组的方法成员一样。这是有效的,因为 .NET 类Array实现了IEnumerable<T>接口。

注意,在这种情况下,没有提供任何参数。相反,数组名已从参数列表移到方法名之前。在那里,它就像包含了方法的声明一样被使用。

方法语法调用和扩展语法调用在语义上是等价的——只是它们的语法不同。

`   using System.Linq;
...
static void Main( )
{
int[] intArray = new int[] { 3, 4, 5, 6, 7, 9 };
Array as parameter

var count1    = Enumerable.Count(intArray);   // Method syntax
var firstNum1 = Enumerable.First(intArray);   // Method syntax

var count2    = intArray.Count();             // Extension syntax
var firstNum2 = intArray.First();             // Extension syntax

Array as extended object
Console.WriteLine("Count: {0}, FirstNumber: {1}", count1, firstNum1);
Console.WriteLine("Count: {0}, FirstNumber: {1}", count2, firstNum2);
}`

该代码产生以下输出:


Count: 6, FirstNumber: 3 Count: 6, FirstNumber: 3


查询表达式和标准查询运算符

标准查询运算符集是一组用于执行查询的方法。正如在本章开始时提到的,每个查询表达式也可以使用带有标准查询操作符的方法语法来编写。编译器将每个查询表达式翻译成标准的查询操作符形式。

显然,由于所有的查询表达式都被翻译成标准的查询操作符,这些操作符可以执行查询表达式所做的一切。但是运算符也提供了查询表达式形式中没有的附加功能。例如,在前面的例子中使用的操作符SumCount只能用方法语法来表达。

但是,查询表达式和方法语法这两种形式可以结合使用。例如,下面的代码显示了一个也使用运算符Count的查询表达式。请注意,语句的查询表达式部分在括号内,后跟一个点和方法名。

`   static void Main()
{
var numbers = new int[] { 2, 6, 4, 8, 10 };

int howMany = (from n in numbers
where n < 7
select n).Count();
↑        ↑
Query expression   Operator

Console.WriteLine("Count: {0}", howMany);
}`

该代码产生以下输出:


Count: 3


代表作为参数

正如您在上一节中看到的,每个操作符的第一个参数是对一个IEnumerable<T>对象的引用。其后的参数可以是任何类型。许多操作符将通用委托作为参数。(通用代表在第十七章的中进行了解释。)关于作为参数的泛型委托,最重要的事情是:

  • Generic delegates are used to provide custom code to operators.

为了解释这一点,我将从一个例子开始,展示使用Count操作符的几种方法。Count操作符是重载的,有两种形式。正如您在前面的示例中看到的,第一个表单有一个返回集合中元素数量的参数。这里重复了它的签名:

   public static int Count<T>(this IEnumerable<T> source);

但是,假设您只想计算数组的奇数个元素。要做到这一点,您必须为Count方法提供代码来确定一个整数是否是奇数。

为此,您需要使用第二种形式的Count方法,如下所示。作为它的第二个参数,它接受一个泛型委托。在调用它时,您必须提供一个委托对象,该对象接受一个类型为T的输入参数并返回一个布尔值。委托代码的返回值必须指定该元素是否应包括在计数中。

   public static int Count<T>(this IEnumerable<T> source, <ins>Func<T, bool> predicate</ins> ); ↑ Generic delegate

例如,下面的代码使用第二种形式的Count操作符来指示它只包含那些奇数值。它通过提供一个 lambda 表达式来实现这一点,如果输入值是奇数,则返回true,否则返回false。(同样,lambda 表达式在第十三章的中讨论过。)在集合的每次迭代中,Count使用当前值作为输入来调用这个方法(由 lambda 表达式表示)。如果输入是奇数,该方法返回true,并且Count将该元素包含在总数中。

`   static void Main()
{
int[] intArray = new int[] { 3, 4, 5, 6, 7, 9 };

var countOdd = intArray.Count(n ⇒ n % 2 == 1);

Lambda expression identifying the odd values
Console.WriteLine("Count of odd numbers: {0}", countOdd);
}`

该代码产生以下输出:


Count of odd numbers: 4


LINQ 预定义的委托类型

像前面例子中的Count操作符一样,许多 LINQ 操作符要求您提供指导操作符如何执行操作的代码。您可以通过使用委托对象作为参数来实现这一点。

记住第十三章中的内容,你可以把委托对象看作是一个包含一个方法或一系列方法的对象,这些方法具有特定的签名和返回类型。当委托被调用时,它包含的方法按顺序被调用。

LINQ 定义了两类通用委托类型,用于标准查询操作符。这些是Func代表和Action代表。每个集合有 17 个成员。

  • The delegate objects you create as actual parameters must be these delegate types or these forms.
  • TR represents the return type, and it is always the last of in the type parameter list.

这里列出了前四个通用Func委托。第一种形式不带方法参数,返回返回类型的对象。第二个函数接受单个方法参数并返回值,依此类推。

   public delegate TR Func<out TR>                     ( ); public delegate TR Func<in T1, out TR >             ( T1 a1 ); public delegate TR Func<in T1, in T2, out TR >      ( T1 a1, T2 a2 ); public delegate TR Func<<ins>in T1, in T2, in T3, out TR</ins>>( <ins>T1 a1, T2 a2, T3 a3</ins> ); ↑                  ↑                         ↑ Return type         Type parameters              Method parameters

注意,返回类型参数有out关键字,使其成为协变的。因此,它可以接受声明的类型或从该类型派生的任何类型。输入参数有in关键字,使它们成为逆变的。因此,它们可以接受声明的类型或从该类型派生的任何类型。

记住这一点,如果您再次查看下面显示的Count的声明,您会看到第二个参数必须是一个委托对象,它将某个类型的单个值T作为方法参数,并返回一个类型为bool的值。正如本章前面提到的,这种形式的委托称为谓词。

   public static int Count<T>(this IEnumerable<T> source, Func<T, bool> predicate ); ↑   ↑ Parameter type  Return type

前四个Action代表如下。它们与Func委托相同,只是它们没有返回值,因此也没有返回值类型参数。它们的所有类型参数都是逆变的。

   public delegate void Action                     ( ); public delegate void Action<in T1>              ( T1 a1 ); public delegate void Action<in T1, in T2>       ( T1 a1, T2 a2 ); public delegate void Action<in T1, in T2, in T3>( T1 a1, T2 a2, T3 a3 );

使用委托参数的例子

现在你更好地理解了Count的签名和 LINQ 对通用委托参数的使用,你将更好地理解一个完整的例子。

下面的代码声明了方法IsOdd,该方法接受一个类型为int的参数,并返回一个指定输入参数是否为奇数的bool值。方法Main执行以下操作:

  • Declare an array of int as the data source.
  • Create a entrusted object named MyDel with type Func<int, bool>, and initialize it with method IsOdd. Note that you don't need to declare the Func delegate type, because, as you can see, it has been predefined by LINQ.
  • Call Count with the entrusted object.

`   class Program
{
static bool IsOdd(int x)    // Method to be used by the delegate object
{
return x % 2 == 1;       // Return true if x is odd.
}

static void Main()
{
int[] intArray = new int[] { 3, 4, 5, 6, 7, 9 };

Func<int, bool> myDel = new Func<int, bool>(IsOdd); // Delegate object
var countOdd = intArray.Count(myDel);               // Use delegate.

Console.WriteLine("Count of odd numbers: {0}", countOdd);
}
}`

该代码产生以下输出:


Count of odd numbers: 4


使用 Lambda 表达式参数的例子

前面的示例使用了一个单独的方法和一个委托将代码附加到运算符。这需要声明方法,声明委托对象,然后将委托对象传递给操作符。这种方法工作正常,并且如果满足以下任一条件,这就是正确的方法:

  • If the method must be called from somewhere in the program instead of from the place where the delegate object is initialized.
  • If the code in the method body is not just one or two long statements

但是,如果这两个条件都不成立,您可能希望使用一种更紧凑、更本地化的方法,通过 lambda 表达式向操作符提供代码。

我们可以通过首先完全删除IsOdd方法并将等效的 lambda 表达式直接放在 delegate 对象的声明中,来修改前面的示例以使用 lambda 表达式。新代码更短、更简洁,如下所示:

`   class Program
{
static void Main()
{
int[] intArray = new int[] { 3, 4, 5, 6, 7, 9 };
Lambda expression
       ↓      
var countOdd = intArray.Count( x ⇒ x % 2 == 1 );

Console.WriteLine("Count of odd numbers: {0}", countOdd);
}
}`

与前面的示例一样,此代码产生以下输出:


Count of odd numbers: 4


我们也可以使用匿名方法来代替 lambda 表达式,如下所示。不过这更冗长,因为 lambda 表达式在语义上是等价的,而且不太冗长,所以没有理由再使用匿名方法了。

`   class Program
{
static void Main( )
{
int[] intArray = new int[] { 3, 4, 5, 6, 7, 9 };
Anonymous method
      ↓      
Func<int, bool> myDel = delegate(int x)
{
return x % 2 == 1;
};
var countOdd = intArray.Count(myDel);

Console.WriteLine("Count of odd numbers: {0}", countOdd);
}
}`

LINQ 到 XML

可扩展标记语言(XML)是存储和交换数据的重要手段。LINQ 为这种语言增加了一些特性,使得处理 XML 比以前的方法(如 XPath 和 XSLT)容易得多。如果您熟悉这些方法,您可能会很高兴听到 LINQ 到 XML 以多种方式简化了 XML 的创建、查询和操作,包括:

  • You can use a statement to create an XML tree in a top-down way.
  • You can create and manipulate XML in memory without XML documents to contain trees.
  • You can create and operate string nodes without Text child nodes.
  • One of the great differences (improvement! ) is that you don't have to traverse the XML tree to search for it. Instead, you just need to query the tree and let it return your results.

虽然我不会对 XML 进行完整的论述,但在描述 LINQ 提供的一些 XML 操作特性之前,我会先对它进行简单的介绍。

标记语言

一个标记语言是放置在文档中的一组标签,用来给出关于文档中信息的信息并组织其内容。也就是说,标记标签不是文档的数据——它们包含关于数据的数据。关于数据的数据称为元数据。**

标记语言是一组定义好的标记,用来表达关于文档内容的特定类型的元数据。例如,HTML 是最广为人知的标记语言。其标签中的元数据包含关于网页应该如何在浏览器中呈现以及如何使用超文本链接在页面之间导航的信息。

虽然大多数标记语言都包含一组预定义的标记,但 XML 只包含少数几个已定义的标记,其余的由程序员定义,以表示特定文档类型所需的任何种类的元数据。只要数据的作者和读者就标签的含义达成一致,标签就可以包含设计者想要的任何有用的信息。

XML 基础知识

XML 文档中的数据包含在 XML 树中,XML 树主要由一组嵌套元素组成。

元素是 XML 树的基本组成部分。每个元素都有一个名称,并且可以包含数据。有些还可以包含其他嵌套元素。元素由开始和结束标记来划分。元素包含的任何数据都必须在开始和结束标记之间。

  • The opening tag begins with the opening angle bracket, followed by the element name, optionally followed by any attribute, followed by the closing angle bracket: <PhoneNumber>
  • The closing tag begins with an opening angle bracket, followed by a slash character, followed by the element name, followed by the closing angle bracket: </PhoneNumber>
  • Elements with no content can be represented by a single tag, which starts with an opening angle bracket, followed by the element name, followed by a slash, and finally ends with an ending angle bracket: <PhoneNumber />

下面的 XML 片段显示了一个名为EmployeeName的元素,后跟一个名为PhoneNumber的空元素。

           Opening tag                  Closing tag <ins>      ↓     </ins>              <ins>      ↓       </ins> <EmployeeName><ins>Sally Jones</ins></EmployeeName> ↑            ↑ Content <PhoneNumber />     ← Element with no content

关于 XML,需要知道的其他重要事情如下:

  • The XML document must have a root element that contains all other elements.

  • XML tags must be nested correctly.

  • XML attributes are name/value pairs that contain additional metadata of elements. The value part of the attribute must always be enclosed in quotation marks, which can be double quotation marks or single quotation marks.

  • Keep white space in XML document. This is different from HTML, where white space is merged into one space in the output.

下面的 XML 文档是一个包含两名雇员信息的 XML 示例。为了清楚地显示元素,这个 XML 树非常简单。关于 XML 树,需要注意的重要事项如下:

  • The tree contains a root node of type Employees, which contains two child nodes of type Employee.
  • Each Employee node contains a node containing the employee's name and phone number.

   <Employees> <Employee> <Name>Bob Smith</Name> <PhoneNumber>408-555-1000</PhoneNumber> <CellPhone /> </Employee> <Employee> <Name>Sally Jones</Name> <PhoneNumber>415-555-2000</PhoneNumber> <PhoneNumber>415-555-2001</PhoneNumber> </Employee> </Employees>

图 19-12 展示了样本 XML 树的层次结构。

Image

***图 19-12。*样本 XML 树的层次结构

XML 类

LINQ 到 XML 可以通过两种方式处理 XML。第一种方式是作为简化的 XML 操作 API。第二种方法是使用你在本章前面看到的 LINQ 查询工具。我将从介绍 LINQ 到 XML API 开始。

LINQ 到 XML API 由许多代表 XML 树组件的类组成。您将使用的三个最重要的类是XElementXAttributeXDocument。还有其他的类,但是这些是主要的。

在图 19-12 中,你看到了 XML 树是一组嵌套的元素。图 19-13 显示了用于构建 XML 树的类以及它们是如何嵌套的。

例如,该图显示了以下内容:

  • XDocument A node can have the following nodes as its direct child nodes:
    • It is at most one of the following node types: XDeclaration node, XDocumentType node and XElement node.
    • Any number of XProcessingInstruction nodes
  • If there is a top-level XElement node under XDocument, it is the root of other elements in the XML tree.
  • The root can contain any number of nested XElement, XComment or XProcessingInstruction nodes in turn, and can be nested to any level.

Image

***图 19-13。*XML 节点的包容结构

除了XAttribute类之外,大多数用于创建 XML 树的类都是从一个名为XNode的类中派生出来的,在文献中统称为 XNodes 。图 19-13 显示了白色云朵中的XNode类,而XAttribute类显示在灰色云朵中。

创建、保存、加载和显示 XML 文档

展示 XML API 的简单性和用法的最佳方式是展示简单的代码示例。例如,下面的代码显示了在使用 XML 时执行几项重要任务是多么简单。

它首先创建一个简单的 XML 树,由一个名为Employees的节点和两个包含两名雇员姓名的子节点组成。请注意以下关于代码的内容:

  • A tree is created with a statement that creates all nested elements in the tree. This is called functional structure .
  • Use the object to create an expression, and use the constructor of the node type to create each element in place.

创建树之后,代码使用XDocumentSave方法将它保存到一个名为EmployeesFile.xml的文件中。然后,它使用XDocument的静态Load方法从文件中读回 XML 树,并将树分配给一个新的XDocument对象。最后,它使用WriteLine显示新的XDocument对象持有的树的结构。

`   using System;
using System.Xml.Linq;                           // Required namespace

class Program {
static void Main( ) {
XDocument employees1 =
new XDocument(                       // Create the XML document.
new XElement("Employees",         // Create the root element.
new XElement("Name", "Bob Smith"),      // Create element.
new XElement("Name", "Sally Jones")     // Create element.
)
);

employees1.Save("EmployeesFile.xml");            // Save to a file.

// Load the saved document into a new variable.
XDocument employees2 = XDocument.Load("EmployeesFile.xml");

Static method
Console.WriteLine(employees2);                   // Display document.
}
}`

该代码产生以下输出:


<Employees> <Name>Bob Smith</Name> <Name>Sally Jones</Name> </Employees>


创建 XML 树

在前面的例子中,您看到了可以通过使用构造函数为XDocumentXElement在内存中创建 XML 文档。在两个构造函数的情况下

  • The first parameter is the name of the object.
  • The second and following parameters contain the nodes of the XML tree. The second parameter of the constructor is a params parameter, so there can be any number of parameters.

例如,下面的代码生成一个 XML 树,并使用Console.WriteLine方法显示它:

`   using System;
using System.Xml.Linq;                         // This namespace is required.

class Program
{
static void Main( ) {
XDocument employeeDoc =
new XDocument(                     // Create the document.
new XElement("Employees",       // Create the root element.
new XElement("Employee",     // First employee element
new XElement("Name", "Bob Smith"),
new XElement("PhoneNumber", "408-555-1000") ),

new XElement("Employee",     // Second employee element
new XElement("Name", "Sally Jones"),
new XElement("PhoneNumber", "415-555-2000"),
new XElement("PhoneNumber", "415-555-2001") )
)
);
Console.WriteLine(employeeDoc);       // Displays the document
}
}`

该代码产生以下输出:


<Employees> <Employee> <Name>Bob Smith</Name> <PhoneNumber>408-555-1000</PhoneNumber> </Employee> <Employee> <Name>Sally Jones</Name> <PhoneNumber>415-555-2000</PhoneNumber> <PhoneNumber>415-555-2001</PhoneNumber> </Employee> </Employees>


使用 XML 树中的值

当您遍历 XML 树并检索或修改值时,XML 的威力变得显而易见。表 19-2 显示了用于检索数据的主要方法。

Image

关于表 19-2 中的方法,需要了解的一些重要事项如下:

  • Nodes: The Nodes method returns an object of type IEnumerable<object>, because the returned nodes may be of different types, such as XElement, XComment and so on. You can use the type parameterization method OfType<*type*> to specify what type of node to return. For example, the following line of code only retrieves the XComment node: IEnumerable<XComment> comments = xd.Nodes().OfType<XComment>();
  • Elements: Since searching XElements is such a common requirement, there is a shortcut for expression Nodes().OfType<XElement>()-method Elements.
    • Use the Elements method without parameters to return all children XElement
    • Use the Elements method with a single name parameter to return only the child XElement s with that name. For example, the following line of code returns all child XElement nodes named phonenumber . IEnumerable<XElement> empPhones = emp.Elements("PhoneNumber");
  • Element: This method only takes the first child node XElement of the current node. Like the Elements method, it can be called with one parameter or without parameters. If there is no parameter, it will get the first child node XElement. Using a single name parameter, it gets the first child node XElement of the name.
  • Descendants and Ancestors: These methods work in a similar way to Elements and Parent methods, but they do not return direct child elements or parent elements, but include elements below or above the current node, regardless of the nesting level.

下面的代码说明了ElementElements方法:

`   using System;
using System.Collections.Generic;
using System.Xml.Linq;

class Program {
static void Main( ) {
XDocument employeeDoc =
new XDocument(
new XElement("Employees",
new XElement("Employee",
new XElement("Name", "Bob Smith"),
new XElement("PhoneNumber", "408-555-1000")),
new XElement("Employee",
new XElement("Name", "Sally Jones"),
new XElement("PhoneNumber", "415-555-2000"),
new XElement("PhoneNumber", "415-555-2001"))
)
);             Get first child XElement named "Employees"
         ↓        
XElement root = employeeDoc.Element("Employees");
IEnumerable employees = root.Elements();

foreach (XElement emp in employees)
{                        Get first child XElement named "Name"
       ↓      
XElement empNameNode = emp.Element("Name");
Console.WriteLine(empNameNode.Value);
Get all child elements named "PhoneNumber"
         ↓          
IEnumerable empPhones = emp.Elements("PhoneNumber");
foreach (XElement phone in empPhones)
Console.WriteLine("   {0}", phone.Value);
}
}
}`

该代码产生以下输出:


Bob Smith 408-555-1000 Sally Jones 415-555-2000 415-555-2001


添加节点和操作 XML

您可以使用Add方法向现有元素添加子元素。Add方法允许您在单个方法调用中添加任意多的元素,而不管您添加的节点类型。

例如,下面的代码创建一个简单的 XML 树并显示它。然后,它使用Add方法向根元素添加一个节点。接下来,它第二次使用Add方法添加三个元素——两个XElement和一个XComment。注意输出中的结果:

`   using System;
using System.Xml.Linq;

class Program
{
static void Main()
{
XDocument xd = new XDocument(               // Create XML tree.
new XElement("root",
new XElement("first")
)
);

Console.WriteLine("Original tree");
Console.WriteLine(xd); Console.WriteLine(); // Display the tree.

XElement rt = xd.Element("root");           // Get the first element.

rt.Add( new XElement("second"));            // Add a child element.

rt.Add( new XElement("third"),              // Add three more children.
new XComment("Important Comment"),
new XElement("fourth"));

Console.WriteLine("Modified tree");
Console.WriteLine(xd);                      // Display modified tree.
}
}`

这段代码产生以下输出:


`

               `

Add方法将新的子节点放在现有子节点之后,但是您也可以使用AddFirstAddBeforeSelfAddAfterSelf方法将节点放在子节点之前和之间。

表 19-3 列出了操作 XML 的一些最重要的方法。请注意,有些方法应用于父节点,有些方法应用于节点本身。

Image

处理 XML 属性

属性给出了关于一个XElement节点的附加信息。它们被放在 XML 元素的开始标记中。

当您在函数上构造 XML 树时,您可以通过在XElement构造函数的范围内包含XAttribute构造函数来添加属性。XAttribute构造函数有两种形式;一个接受名称和值,另一个接受对已经存在的XAttribute的引用。

下面的代码向root添加了两个属性。请注意,XAttribute构造函数的两个参数都是字符串;第一个指定属性的名称,第二个给出值。

`   XDocument xd = new XDocument(
Name    Value
new XElement("root",    ↓       ↓
new XAttribute("color", "red"),       // Attribute constructor
new XAttribute("size", "large"),      // Attribute constructor
new XElement("first"),
new XElement("second")
)
);

Console.WriteLine(xd);`

这段代码产生以下输出。请注意,属性放在元素的开始标记内。


<root color="red" size="large"> <first /> <second /> </root>


要从XElement节点检索属性,使用Attribute方法,提供属性的名称作为参数。下面的代码创建了一个 XML 树,它的节点有两个属性— colorsize。然后,它检索属性值并显示它们。

`   static void Main( )
{
XDocument xd = new XDocument(                      // Create XML tree.
new XElement("root",
new XAttribute("color", "red"),
new XAttribute("size", "large"),
new XElement("first")
)
);

Console.WriteLine(xd); Console.WriteLine();        // Display XML tree.

XElement rt = xd.Element("root");                  // Get the element.

XAttribute color = rt.Attribute("color");          // Get the attribute.
XAttribute size =  rt.Attribute("size");           // Get the attribute.

Console.WriteLine("color is {0}", color.Value);    // Display attr. value.
Console.WriteLine("size  is {0}", size.Value);     // Display attr. value.
}`

该代码产生以下输出:


`

color is red
size  is large`


要删除一个属性,您可以选择该属性并使用Remove方法,或者对其父属性使用SetAttributeValue方法并将属性值设置为null。下面的代码演示了这两种方法:

`   static void Main( ) {
XDocument xd = new XDocument(
new XElement("root",
new XAttribute("color", "red"),
new XAttribute("size", "large"),
new XElement("first")
)
);

XElement rt = xd.Element("root");          // Get the element.

rt.Attribute("color").Remove();            // Remove the color attribute.
rt.SetAttributeValue("size", null);        // Remove the size attribute.

Console.WriteLine(xd);
}`

该代码产生以下输出:


<root> <first /> </root>


要向 XML 树添加属性或更改属性的值,可以使用SetAttributeValue方法,如下面的代码所示:

`   static void Main( ) {
XDocument xd = new XDocument(
new XElement("root",
new XAttribute("color", "red"),
new XAttribute("size", "large"),
new XElement("first")));

XElement rt = xd.Element("root");            // Get the element.

rt.SetAttributeValue("size",  "medium");     // Change attribute value.
rt.SetAttributeValue("width", "narrow");     // Add an attribute.

Console.WriteLine(xd); Console.WriteLine();
}`

该代码产生以下输出:


<root color="red" size="medium" width="narrow"> <first /> </root>


其他类型的节点

在前面的例子中使用的另外三种类型的节点是XCommentXDeclarationXProcessingInstruction。下面几节将对它们进行描述。

xccomment

XML 中的注释由位于<!---->标记之间的文本组成。XML 解析器会忽略标记之间的文本。您可以使用XComment类在 XML 文档中插入文本,如下面的代码行所示:

   new XComment("This is a comment")

这段代码在 XML 文档中生成以下行:

   <!--This is a comment-->

扩展澄清

XML 文档以一行开始,这一行包括使用的 XML 版本、使用的字符编码类型以及文档是否依赖于外部引用。这是关于 XML 的信息,所以它实际上是关于元数据的元数据!这被称为 XML 声明,并使用XDeclaration类插入。下面显示了一个XDeclaration语句的示例:

   new XDeclaration("1.0", "utf-8", "yes")

这段代码在 XML 文档中生成以下行:

   <?xml version="1.0" encoding="utf-8" standalone="yes"?>

表达辛司酮的修饰

XML 处理指令用于提供关于如何使用或解释 XML 文档的附加数据。最常见的是,处理指令用于将样式表与 XML 文档相关联。

您可以使用XProcessingInstruction构造函数包含一个处理指令,该构造函数接受两个字符串参数——一个目标和一个数据字符串。如果处理指令采用多个数据参数,这些参数必须包含在XProcessingInstruction构造函数的第二个参数字符串中,如下面的构造函数代码所示。请注意,在本例中,第二个参数是一个逐字字符串,字符串中的文字双引号由两个连续的双引号表示。

   new XProcessingInstruction( "xml-stylesheet", @"href=""stories"", type=""text/css""")

这段代码在 XML 文档中生成以下行:

   <?xml-stylesheet href="stories.css" type="text/css"?>

下面的代码使用了所有三种结构:

   static void Main( ) { XDocument xd = new XDocument( new XDeclaration("1.0", "utf-8", "yes"), new XComment("This is a comment"), new XProcessingInstruction("xml-stylesheet", @"href=""stories.css"" type=""text/css"""), new XElement("root", new XElement("first"), new XElement("second") ) ); }

这段代码在输出文件中产生以下输出。然而,使用xdWriteLine不会显示声明语句,即使它包含在文档文件中。


`

      `

用 LINQ 对 XML 使用 LINQ 查询

您可以将 LINQ XML API 与 LINQ 查询表达式结合起来,产生简单而强大的 XML 树搜索。

下面的代码创建了一个简单的 XML 树,将其显示在屏幕上,然后保存到一个名为SimpleSample.xml的文件中。虽然这段代码中没有什么新内容,但是我们将在下面的例子中使用这个 XML 树。

`   static void Main( )
{
XDocument xd = new XDocument(
new XElement("MyElements",
new XElement("first",
new XAttribute("color", "red"),
new XAttribute("size",  "small")),
new XElement("second",
new XAttribute("color", "red"),
new XAttribute("size",  "medium")),
new XElement("third",
new XAttribute("color", "blue"),
new XAttribute("size",  "large"))));

Console.WriteLine(xd);                      // Display XML tree.
xd.Save("SimpleSample.xml");                // Save XML tree.
}`

该代码产生以下输出:


<MyElements> <first color="red" size="small" /> <second color="red" size="medium" /> <third color="blue" size="large" /> </MyElements>


下面的示例代码使用一个简单的 LINQ 查询从 XML 树中选择一个节点子集,然后以几种方式显示它们。该代码执行以下操作:

  • It selects only those elements whose names have five characters from the XML tree. Because the names of the elements are first , second and third , only the node names first and third meet the search criteria, so these nodes are selected.
  • Displays the name of the selected element.
  • Format and display the selected nodes, including node names and attribute values. Note that the attribute is retrieved using the Attribute method, and the attribute value is retrieved using the Value attribute.

`   static void Main( )
{
XDocument xd = XDocument.Load("SimpleSample.xml"); // Load the document.
XElement rt = xd.Element("MyElements");            // Get the root element.

var xyz = from e in rt.Elements()                  // Select elements whose
where e.Name.ToString().Length == 5      // names have 5 chars.
select e;

foreach (XElement x in xyz)                        // Display the
Console.WriteLine(x.Name.ToString());           // selected elements.

Console.WriteLine();
foreach (XElement x in xyz)
Console.WriteLine("Name: {0}, color: {1}, size: {2}",
x.Name,
x.Attribute("color").Value,
x.Attribute("size") .Value);
↑           ↑
}                          Get the attribute.       Get the attribute’s value.`

该代码产生以下输出:


`first
third

Name: first, color: red, size: small
Name: third, color: blue, size: large`


下面的代码使用一个简单的查询来检索 XML 树的所有顶级元素,并为每个元素创建一个匿名类型的对象。第一次使用WriteLine方法显示了匿名类型的默认格式。第二个WriteLine语句显式格式化匿名类型对象的成员。

`   using System;
using System.Linq;
using System.Xml.Linq;

static void Main( )
{
XDocument xd = XDocument.Load("SimpleSample.xml"); // Load the document.
XElement rt = xd.Element("MyElements");            // Get the root element.

var xyz = from e in rt.Elements()
select new { e.Name, color = e.Attribute("color") };

foreach (var x in xyz)       Create an anonymous type.
Console.WriteLine(x);                     // Default formatting

Console.WriteLine();
foreach (var x in xyz)
Console.WriteLine("{0,-6},   color: {1, -7}", x.Name, x.color.Value);
}`

这段代码产生以下输出。前三行显示了匿名类型的默认格式。最后三行显示了在第二个WriteLine方法的格式字符串中指定的显式格式。


`

first ,   color: red
second,   color: red
third ,   color: blue`


从这些例子中,您可以看到,您可以轻松地将 XML API 与 LINQ 查询工具结合起来,以产生强大的 XML 查询功能。

二十、异步编程简介

什么是异步?

当您启动一个程序时,系统会在内存中创建一个新的进程。进程是组成运行程序的一组资源。这些包括虚拟地址空间、文件句柄和程序运行所需的许多其他东西。

在进程内部,系统创建一个内核对象,称为线程,它代表实际执行的程序。(线程是“执行线程”的简称)一旦建立了进程,系统就开始在方法Main中的第一条语句处执行线程。

关于线程,需要了解的一些重要事项如下:

  • By default, a process contains only one thread, which runs from the beginning of the program to the end.
  • One thread can spawn other threads, so that at any time, a process may have multiple threads in different states and execute different parts of the program.
  • If there are multiple threads in a process, they all share the resources of the process. The unit that the system schedules to execute on the processor is a thread, not a process.

到目前为止,本书中展示的所有示例程序都只使用了一个线程,并且从程序中的第一条语句到最后一条语句顺序执行。然而,在许多情况下,这种简单的模型会产生不可接受的行为,无论是在性能还是最终用户体验方面。

例如,一个服务器程序可能不断地启动与其他服务器的连接并向它们请求数据,同时处理来自许多客户端程序的请求。这些通信任务通常需要相当长的时间,程序只是在等待网络或互联网上另一台计算机的响应。这大大降低了性能。与其浪费时间等待回复,不如同时处理其他任务,然后在回复到达时继续处理第一个任务,这样效率会更高。

另一个例子是交互式 GUI 程序。如果用户启动了一个需要很长时间的操作,程序在操作完成之前一直停在屏幕上是不可接受的。用户应该仍然能够在屏幕上移动窗口,甚至可以取消操作。

在这一章中,我们将会看到异步编程,这是一种编程类型,在这种编程中,部分程序代码不一定按照编写代码的严格顺序执行。有时这涉及到在另一个线程上运行一段代码。然而,在其他时候,不会创建新的线程,而是对代码的执行进行重新排序,以更好地利用单线程的能力。

我们将从 C# 5.0 中引入的一个新特性开始,它允许您构建异步方法。这就是所谓的async / await特性。之后,我们将了解属于的几个特性 .NET 框架,但没有内置到 C# 语言中,它允许额外形式的异步。这些主题包括BackgroundWorker类和对 .NET 任务并行库。这两个主题都通过创建新线程来实现异步。我们将通过研究产生异步的其他方式来结束这一章。

一个首发的例子

为了说明和比较,我们将从看一个不使用异步的例子开始,然后将它与一个使用异步的类似程序进行比较。

在下面显示的代码示例中,方法DoRun是类MyDownloadString的一个方法,它执行以下操作:

  • It creates and starts an object with class Stopwatch, which is located in the System.Diagnostics namespace. It uses this Stopwatch timer to time various tasks in the code.
  • Then it makes two calls to the method CountCharacters, which downloads the content of the website and returns the number of characters contained in the website. The website is specified as the URL string given as the second parameter. Then it makes four calls to the method CountToALargeNumber. This method is just a sham, which represents a task that takes a certain amount of time. It just cycles a given number of times.
  • Finally, it prints out the number of characters of two websites.

`using System;
using System.Net;
using System.Diagnostics;

class MyDownloadString
{
Stopwatch sw = new Stopwatch();

public void DoRun() {
const int LargeNumber = 6000000;
sw.Start();

int t1 = CountCharacters( 1, "http://www.microsoft.com" );
int t2 = CountCharacters( 2, "http://www.illustratedcsharp.com" );

CountToALargeNumber( 1, LargeNumber );  CountToALargeNumber( 2, LargeNumber );
CountToALargeNumber( 3, LargeNumber );  CountToALargeNumber( 4, LargeNumber );

Console.WriteLine( "Chars in http://www.microsoft.com        : {0}", t1 );
Console.WriteLine( "Chars in http://www.illustratedcsharp.com: {0}", t2 );
}

private int CountCharacters(int id, string uriString ) {
WebClient wc1 = new WebClient();
Console.WriteLine( "Starting call {0}    :     {1, 4:N0} ms",
id, sw.Elapsed.TotalMilliseconds );
string result = wc1.DownloadString( new Uri( uriString ) );
Console.WriteLine( "   Call {0} completed:     {1, 4:N0} ms",
id, sw.Elapsed.TotalMilliseconds );
return result.Length;
}       private void CountToALargeNumber( int id, int value ) {
for ( long i=0; i < value; i++ )
;
Console.WriteLine( "   End counting {0}  :     {1, 4:N0} ms",
id, sw.Elapsed.TotalMilliseconds );
}
}

class Program
{
static void Main() {
MyDownloadString ds = new MyDownloadString();
ds.DoRun();
}
}`

这段代码在一次运行中产生了以下输出。运行时,以毫秒(ms)为单位列出的计时数字会有所不同。


Starting call 1    :        1 ms Call 1 completed:      178 ms Starting call 2    :      178 ms Call 2 completed:      504 ms End counting 1  :      523 ms End counting 2  :      542 ms End counting 3  :      561 ms End counting 4  :      579 ms Chars in http://www.microsoft.com        : 1020 Chars in http://www.illustratedcsharp.com: 4699


图 20-1 总结了输出,显示了各种任务开始和结束的时间线。查看该图,您会注意到调用 1 和 2 占用了方法调用的大部分时间。但是对于每一个调用,所需的绝大部分时间都浪费在了等待网站的响应上。

Image

***图 20-1。*计划中各项任务所需时间的时间表

如果我们能够启动两个CountCharacter调用,而不是等待结果,而是继续执行对CountToALargeNumber的四个调用,然后在完成后获取两个CountCharacter方法调用的结果,我们就可以显著提高程序的性能。

C# 的新async / await特性让我们可以做到这一点。为使用该特性而重写的代码如下所示。我将很快介绍这个新特性的细节,但是在这个例子中需要注意的是:

  • When method DoRun calls CountCharactersAsync, CountCharactersAsync returns almost immediately before it actually downloads characters. It returns a placeholder object of type Task<int> to the calling method, indicating the work it plans to do, and will eventually "return" a int.
  • This allows the method DoRun to continue without waiting for the actual work to be completed. Its next statement is another call to CountCharactersAsync. It does the same thing and returns another Task<int> object.
  • DoRun can then continue and make four calls to CountToALargeNumber, while two calls to CountCharactersAsync continue to finish their work-mainly waiting.
  • The last two lines of method DoRun retrieve the result from Task returned by CountCharactersAsync call. If a result is not ready, execution will block and wait until it is ready.

`...
using System.Threading.Tasks;

class MyDownloadString
{
Stopwatch sw = new Stopwatch();

public void DoRun()  {
const int LargeNumber = 6000000;
sw.Start();
Objects that will hold the results
      ↓     
Task t1 = CountCharactersAsync( 1, "http://www.microsoft.com" );
Task t2 = CountCharactersAsync( 2, "http://www.illustratedcsharp.com" );
CountToALargeNumber( 1, LargeNumber );  CountToALargeNumber( 2, LargeNumber );
CountToALargeNumber( 3, LargeNumber );  CountToALargeNumber( 4, LargeNumber );
Get results.
    ↓    
Console.WriteLine( "Chars in http://www.microsoft.com        : {0}", t1.Result );
Console.WriteLine( "Chars in http://www.illustratedcsharp.com: {0}", t2.Result );
}
Contextual       Type that represents work being done,
keyword       which will eventually return an int
↓         ↓    
private async Task CountCharactersAsync( int id, string site ) {
WebClient wc = new WebClient();
Console.WriteLine( "Starting call {0}    :     {1, 4:N0} ms",
id, sw.Elapsed.TotalMilliseconds );
Contextual keyword

string result = await wc.DownloadStringTaskAsync( new Uri( site ) );          Console.WriteLine( "   Call {0} completed:     {1, 4:N0} ms",
id, sw.Elapsed.TotalMilliseconds );
return result.Length;
}

private void CountToALargeNumber( int id, int value )
{
for ( long i=0; i < value; i++ ) ;
Console.WriteLine( "   End counting {0}  :     {1, 4:N0} ms",
id, sw.Elapsed.TotalMilliseconds );
}
}
class Program
{
static void Main()
{
MyDownloadString ds = new MyDownloadString();
ds.DoRun();
}
}`

这段代码在我的机器上运行了一次,产生了以下结果。同样,您的计时结果,以及可能的行顺序,很可能与我的不同。


Starting call 1    :       12 ms Starting call 2    :       60 ms End counting 1  :       80 ms End counting 2  :       99 ms End counting 3  :      118 ms Call 1 completed:      124 ms End counting 4  :      138 ms Chars in http://www.microsoft.com        : 1020 Call 2 completed:      387 ms Chars in http://www.illustratedcsharp.com: 4699


图 20-2 总结了输出,显示了修改程序的时间线。新版本比以前的版本快 32%。在等待两个CountCharactersAsync方法调用中来自 web 站点的响应期间,它通过执行对CountToALargeNumber的四个调用来赢得这个时间。所有这些都是在主线程上完成的;我们没有创建任何额外的线程!

Image

***图 20-2。*程序的异步/等待版本的时间线

async/await 特性的结构

既然您已经看到了一个异步方法的例子,那么让我们来看看它的定义和细节。

当一个程序进行一个方法调用,并在继续之前等待该方法执行其所有处理时,我们说该方法调用是同步的。这是默认形式,也是你在本章之前的所有章节中看到的。

与此相反,异步方法在完成所有工作之前返回到调用方法。C# 的async / await特性允许你创建和使用异步方法。该特征由三部分组成,如图图 20-3 所示:

  • Calling the method means calling the async method, and then moving forward when the async method performs its task, whether on the same thread or on different threads.
  • async The method is to set the work to be completed asynchronously, and then return to the calling method in advance.
  • The await expression is used in the async method to specify tasks that need to be executed asynchronously. A async method can contain any number of await expressions, although the compiler will generate a warning message if there is no at least one expression.

我将在接下来的章节中详细介绍这三个组件,从async方法的语法和语义开始。

Image

***图 20-3。*异步/等待功能的整体结构

什么是异步方法?

如前一节所述,async方法是在完成其所有工作之前返回到调用方法的方法,然后在调用方法继续执行的同时完成其工作。

从语法上来说,async方法具有以下特征,如图 20-4 所示。

  • There is a async method modifier in the method header.
  • It contains one or more await expressions. These expressions represent tasks that can be completed asynchronously.
  • There must be one of the following three return types. In the second and third cases-namely Task and Task<T>-the returned objects represent a large piece of work to be completed in the future, and both the calling method and the async method can continue processing.
    • void
    • Task
    • Task<T>
  • A async method can have any number of parameters of any type. However, none of the parameters can be out or ref parameters.
  • Conventionally, the name of the async method should end with the suffix Async.
  • In addition to methods, lambda expressions and anonymous methods can also act as async objects.

Image

***图 20-4。*异步方法的结构

图 20-4 突出显示了一个async方法的组成部分,我们现在可以更详细地看一下。第一项是async关键字。

  • The method header of async method must have the keyword async, and it must be before the return type.
  • The modifier does nothing except indicate that this method contains one or more await expressions. That is, it does not generate any asynchrony itself.
  • The async keyword is a contextual keyword , which means that async can be used as an identifier in addition to being a method modifier (or lambda or anonymous method modifier).

返回类型必须是以下三种类型之一。注意,其中两个返回类型包含了Task类。当提到类时,我将使用大写形式(因为这是类的名称)和语法字样。我将用一般意义上的小写形式来表示一组需要完成的工作。

  • Task<T>: If the calling method wants to receive the value of type T returned by the call, the return type of method async must be Task<T>. Then, the calling method will get the value of type T by reading the Result attribute of Task, as shown in the following code of the calling method:    Task<int> value   = DoStuff.CalculateSumAsync( 5, 6 ); ... Console.WriteLine( "Value: {0}", value.Result );
  • Task: If the calling method does not need the return value from async method, but needs to be able to check the state of async method, then async method can return an object of type Task. In this case, if there are any return statements in the async method, they will definitely not return anything. The following code example comes from a calling method:    Task someTask = DoStuff.CalculateSumAsync(5, 6); ... someTask.Wait();
  • void: If the calling method just wants the async method to be executed, but it doesn't need any further interaction with it (this is sometimes called getting angry and forgetting ), then the async method can have a return type of void. In this case, as in the previous case, if there are any return statements in the async method, they will definitely not return anything.

注意,在上面的图 20-4 中,async方法的返回类型是Task<int>。然而,当您检查方法的主体时,您不会发现任何返回类型为Task<int>的对象的return语句。然而,在方法的末尾有一个返回类型为int的值的return语句。我们可以将这一观察结果归纳为以下几点,稍后我们将对此进行更详细的研究:

  • Any async method with a return type of Task<T> must return a value of type T or a type that can be implicitly converted to T.

图 20-5 、 20-6 和 20-7 显示了三种返回类型的调用方法和async方法之间交互所需的架构。

Image

***图 20-5。*使用异步方法返回任务< int >对象

Image

***图 20-6。*使用返回任务对象的异步方法

图 20-7 中的代码使用Thread.Sleep方法暂停主线程,这样它就不会在async方法完成之前退出。

Image

***图 20-7。*使用一劳永逸的异步方法

异步方法中的控制流

一个async方法的主体结构有三个不同的区域,如图图 20-8 所示。我将在下一节详细介绍await表达式,但是在这一节中,您可以大致了解它的位置和作用。这三个区域如下:

  • Before the first expression await : This includes all codes from the beginning of the method to the first expression await. This area should only contain a small amount of code that does not require much processing.
  • await Expression : This expression represents the task to be executed asynchronously.
    ** Continuation : This is the remaining code in the method, which follows the expression await. This is packaged with its execution environment, which includes information about which thread it is on, variable values in the current scope, and other information needed to resume execution after the await expression is completed.*

*Image

***图 20-8。*异步方法的代码区域

图 20-9 总结了通过async方法的控制流程。它从第一个await表达式之前的代码开始,正常(同步)执行,直到遇到第一个await。这个区域实际上在第一个await表达式结束,这里await的任务还没有完成(这应该是绝大部分时间)。如果await的任务已经完成,该方法继续同步执行。如果遇到另一个await,则重复该过程。

当到达await表达式时,async方法将控制权返回给调用方法。如果该方法的返回类型是类型TaskTask<T>,则该方法创建一个Task对象,该对象表示要异步完成的任务和延续,并将该Task返回给调用方法。

现在有两个控制流:一个在async方法中,一个在调用方法中。async方法中的代码执行以下操作:

  • Execute the realizable task of its await expression asynchronously.
  • When the await expression is completed, the continuation is executed. Continuation itself may have other await expressions, which are handled in the same way. That is, the await expression is executed asynchronously, and then its continuation is executed.
  • When the continuation meets the end of the return statement or method, the following will happen:
  • If the return type of the method is void, the control flow exits.
  • If the return type of the method is Task, continuation sets the status attribute on Task and exits. If the return type is a Task<T>, continuation additionally sets the Result property of the Task object.

同时,调用方法中的代码继续它的进程,从async方法接收回Task对象。当它需要实际的结果值时,它引用Task对象的Result属性。如果到那时,async方法已经设置了该属性,调用方法将检索该值并继续。否则,它会暂停并等待设置属性后再继续。

Image

***图 20-9。*通过异步方法的控制流

人们有时会困惑的一件事是当遇到async方法中的第一个await时返回的对象的类型。返回的类型是在async方法的头中作为返回类型列出的类型;与await表达式返回值的类型无关。

例如,在下面的代码中,await表达式返回一个string。但是在方法的执行过程中,当到达那个await表达式时,async方法返回给调用方法一个Task<int>的对象,因为那是方法的返回类型。

`   private async Task CountCharactersAsync( string site )
{
WebClient wc = new WebClient();

string result = await wc.DownloadStringTaskAsync( new Uri( site ) );

return result.Length;
}`

另一个潜在的令人困惑的事情是,当async方法作为方法中的return语句的结果“返回”时,或者到达方法的结尾时,它实际上并没有将返回给任何东西——它只是退出。

恭候表情

await表达式指定了一个异步完成的任务。await表达式的语法如下所示,它由await关键字和一个被称为任务的可获利对象组成。该任务可能是也可能不是类型为Task的对象。默认情况下,该任务在当前线程上异步运行。

   await *task*

一个可更新的对象是一个可更新类型的实例。一个可应用的类型有一个名为GetAwaiter的方法,该方法不带参数,返回一个名为应用的类型的对象,该对象有以下成员:

  • bool IsCompleted { get; }
  • void OnCompleted(Action);

它还具有以下特性之一:

  • void GetResult();
  • *T* GetResult(); (where T is any type)

然而,在现实中,你很少需要建立自己的一个。相反,你应该使用Task类,它是一个 awaitable,并且可能是大多数程序员在 awaitables 方面所需要的。

和。在. NET 4.5 中,微软在整个 BCL 中发布了大量新的和修改过的异步方法,这些方法返回类型为Task<T>的对象。您可以将这些插入到您的await表达式中,它们将在您当前的线程上异步工作。

在我们之前的许多例子中,我们一直在使用WebClient.DownloadStringTaskAsync方法。这是这些异步方法之一。以下代码是其用法的一个示例:

   Uri site      = new Uri("http://www.illustratedcsharp.com" ); WebClient wc  = new WebClient(); string result = await <ins>wc.DownloadStringTaskAsync( site )</ins>; ↑ Returns a Task<string>

尽管现在有许多返回类型为Task<T>的对象的 BCL 方法,但您很可能有自己的方法,希望用作await表达式的任务。最简单的方法是使用Task.Run方法从您的方法中创建一个Task。关于Task.Run方法的一个非常重要的事实是它在不同的线程上运行你的方法。

下面是Task.Run方法的一个签名,它将一个Func<TReturn>委托作为参数。你会记得在第十九章中,Func<TReturn>是一个预定义的委托,它不接受任何参数并返回一个类型为TReturn的值:

   Task Run( Func<TReturn> func )

因此,要将您的方法传递给Task.Run方法,您需要从中创建一个委托。下面的代码展示了实现这一点的三种方法。在代码中,方法Get10具有与Func<int>委托兼容的形式,因为它不接受参数并返回一个int

  • First, in the first two lines of method DoWorkAsync, a Func<int> delegate named ten is created by using Get10. Then use the delegate in the Task.Run method of the next line.
  • In the second example, a Func<int> delegate is created in the parameter list of the Task.Run method.
  • The last example does not use the Get10 method at all. It uses the return statement containing the Get10 method body, and uses it as the body of lambda expression compatible with Func<int> delegate. Lambda expression is implicitly converted to delegate.

`   class MyClass
{
public int Get10()                             // Func compatible
{
return 10;
}

public async Task DoWorkAsync()
{
Func ten = new Func(Get10);
int a = await Task.Run(ten);

int b = await Task.Run(new Func(Get10));

int c = await Task.Run(() ⇒ { return 10; });

Console.WriteLine("{0}  {1}  {2}", a, b, c);
}

class Program
{
static void Main()
{
Task t = (new MyClass()).DoWorkAsync();
t.Wait();
}
}`

这段代码产生以下输出:


10  10  10


在上面的示例代码中,我们使用了将Func<TResult>作为参数的Task.Run的签名。该方法共有八个重载,如表 20-1 所示。表 20-2 显示了可以使用的四种代表类型的签名。

Image

Image

下面的代码展示了四个await语句,它们使用Task.Run方法运行具有四种不同委托类型的方法:

`   static class MyClass
{
public static async Task DoWorkAsync()
{                                  Action
                    ↓                 
await Task.Run(() ⇒ Console.WriteLine(5.ToString()));
TResult Func()
    ↓    
Console.WriteLine((await Task.Run(() ⇒ 6)).ToString());
Task Func()
                         ↓                            
await Task.Run(() ⇒ Task.Run(() ⇒ Console.WriteLine(7.ToString())));
Task Func()
           ↓           
int value = await Task.Run(() ⇒ Task.Run(() ⇒ 8));
Console.WriteLine(value.ToString());
}
}

class Program
{
static void Main()
{
Task t = MyClass.DoWorkAsync();
t.Wait();
Console.WriteLine("Press Enter key to exit");
Console.Read();
}
}`

该代码产生以下输出:


5 6 7 8


在任何可以使用其他表达式的地方都可以使用await表达式(只要它在async方法内部)。在上面的代码中,四个await表达式用在了三个不同的位置。

  • The first and third examples use the expression await as a statement.
  • In the second example, await expression is used as the parameter of WriteLine method call.
  • The fourth example uses the expression await as the right side of the assignment statement.

然而,假设您有一个与四种委托形式都不匹配的方法。例如,假设您有一个名为GetSum的方法,它将两个int值作为输入,并返回这两个值的和。这与四个可接受委托中的任何一个都不兼容。为了解决这个问题,您可以以可接受的Func委托的形式创建一个 lambda 函数,它唯一的动作就是运行GetSum方法,如下面的代码行所示:

   int value = await Task.Run(() => GetSum(5, 6));

lambda 函数() => GetSum(5, 6)满足Func<TResult>委托,因为它是一个不带参数但返回单个值的方法。以下代码显示了一个完整的示例:

`static class MyClass
{
private static int GetSum(int i1, int i2)
{
return i1 + i2;
}

public static async Task DoWorkAsync()
{                                   TResult Func()
         ↓        
int value = await Task.Run( () ⇒ GetSum(5, 6) );
Console.WriteLine(value.ToString());
}
}

class Program
{
static void Main()
{
Task t = MyClass.DoWorkAsync();
t.Wait();
Console.WriteLine("Press Enter key to exit");
Console.Read();
}
}`

该代码产生以下输出:


11 Press Enter key to exit


取消异步操作

一些 .NET 异步方法允许您请求它们中止执行。您也可以将这个特性构建到您自己的async方法中。在System.Threading.Tasks名称空间中有两个类是为此目的而设计的:CancellationTokenCancellationTokenSource

  • CancellationToken The object contains information about whether the task should be cancelled.
  • There is a task of CancellationToken object that needs to check it regularly to see the status of the token. If the IsCancellationRequested property of the CancellationToken object is set to true, the task should stop operation and return.
  • A CancellationToken is irreversible and can only be used once. That is, once its IsCancellationRequested attribute is set to true, it cannot be changed.
  • A CancellationTokenSource object creates a CancellationToken object, which can then be assigned to different tasks. Any object holding CancellationTokenSource can call its Cancel method, which sets the IsCancellationRequested attribute of CancellationToken to true.

下面的代码展示了如何使用CancellationTokenSourceCancellationToken类来实现取消。请注意,该流程是合作的。也就是说,你在CancellationTokenSource上调用Cancel本身并不会取消操作。相反,它将CancellationTokenIsCancellationRequested属性的状态设置为true。由包含CancellationToken的代码来检查它,看它是否应该停止执行并返回。

下面的代码显示了取消类的用法。编写的代码没有取消async方法,但是在方法Main中间包含了两个调用取消操作的注释行。

`class Program
{
static void Main()
{
CancellationTokenSource cts   = new CancellationTokenSource();
CancellationToken       token = cts.Token;

MyClass mc  = new MyClass();
Task t      = mc.RunAsync( token );

//Thread.Sleep( 3000 );   // Wait 3 seconds.
//cts.Cancel();           //cancel the operation.

t.Wait();
Console.WriteLine( "Was Cancelled: {0}", token.IsCancellationRequested );
}
}    class MyClass
{
public async Task RunAsync( CancellationToken ct )
{
if ( ct.IsCancellationRequested )
return;
await Task.Run( () ⇒ CycleMethod( ct ), ct );
}

void CycleMethod( CancellationToken ct )
{
Console.WriteLine( "Starting CycleMethod" );
const int max = 5;
for ( int i=0; i < max; i++ )
{
if ( ct.IsCancellationRequested )     // Monitor the CancellationToken.
return;
Thread.Sleep( 1000 );
Console.WriteLine( "   {0} of {1} iterations completed", i+1, max );
}
}
}`

保留注释行的第一次运行不会取消任务,并产生以下输出:


Starting CycleMethod 1 of 5 iterations completed 2 of 5 iterations completed 3 of 5 iterations completed 4 of 5 iterations completed 5 of 5 iterations completed Was Cancelled: False


如果取消方法Main中的Thread.SleepCancel语句的注释,三秒钟后任务被取消,执行产生以下输出:


Starting CycleMethod 1 of 5 iterations completed 2 of 5 iterations completed 3 of 5 iterations completed Was Cancelled: True


异常处理和 await 表达式

您可以在try语句中使用await表达式,就像您使用任何其他表达式一样,并且trycatchfinally结构如您所料地工作。

下面的代码展示了一个带有抛出异常的任务的await表达式的例子。await表达式在一个try块中,它以正常方式处理异常。

`   class Program
{
static void Main(string[] args)
{
Task t = BadAsync();
t.Wait();
Console.WriteLine("Task Status   :  {0}", t.Status);
Console.WriteLine("Task IsFaulted:  {0}", t.IsFaulted);
}

static async Task BadAsync()
{
try
{
await Task.Run(() ⇒ { throw new Exception(); });
}
catch
{
Console.WriteLine("Exception in BadAsync");
}
}
}`

该代码产生以下输出:


Exception in BadAsync Task Status   :  RanToCompletion Task IsFaulted:  False


注意在输出中,即使Task抛出了一个Exception,在Main结束时,Task的状态是RanToCompletion。这可能有点令人惊讶,因为async方法抛出了一个异常。不过,这样做的原因是以下两个条件为真:(1)Task没有被取消,以及(2)没有未处理的异常。类似地,IsFaulted属性被设置为False,因为没有未处理的异常。

同步等待调用方法中的任务

调用方法可以对各种async方法进行任意次数的调用,并从它们那里接收回Task对象。然后你的代码可能会继续执行各种任务,但是在继续下一步之前,会等待一个特定的Task对象完成。为此,Task类提供了实例方法Wait,您可以在Task对象上调用该方法。

下面的代码显示了它的用法示例。代码中,调用方法DoRun调用async方法CountCharactersAsync,并接收回一个Task<int>。然后它调用Task实例上的Wait方法,等待直到Task结束。完成后,它会显示结果消息。

`   static class MyDownloadString
{
public static void DoRun()
{
Task t = CountCharactersAsync( "http://www.illustratedcsharp.com" );
Wait until the Task t completes.

t.Wait();
Console.WriteLine( "The task has finished, returning value {0}.", t.Result );
}

private static async Task CountCharactersAsync( string site )
{
string result = await new WebClient().DownloadStringTaskAsync( new Uri( site ) );
return result.Length;
}
}

class Program
{
static void Main()
{
MyDownloadString.DoRun();
}
}`

该代码产生以下输出:


The task has finished, returning value 4699.


Wait方法用于单个Task对象。但是你也可以侍候一组Task对象。给定一组Task,你可以等到所有的都完成,也可以等到其中一个完成。你使用的方法是下面两个Task类上的static方法:

  • WaitAll
  • WaitAny

这些是不返回值的同步方法。也就是说,在继续之前,它们会停止并等待,直到满足约束条件。

我们先来看一个简单的程序,它有一个名为DoRun的方法,这个方法调用了两次async方法,返回两个Task<int>对象。然后,该方法继续进行,检查并打印出任务是否完成。然后,它转到方法的末尾,在完成之前等待Console.Read调用。Console.Read方法等待从键盘接收到的字符。

正如所写的,这个程序没有使用等待方法,但是它在DoRun的中间包含了一个注释部分,其中包含了等待代码,我们将很快使用它来与这个版本的结果进行比较。

`   class MyDownloadString
{
Stopwatch sw = new Stopwatch();

public void DoRun()
{
sw.Start();

Task t1 = CountCharactersAsync( 1, "http://www.microsoft.com" );
Task t2 = CountCharactersAsync( 2, "http://www.illustratedcsharp.com" );

//Task[] tasks = new Task[] { t1, t2 };
//Task.WaitAll( tasks );
//Task.WaitAny( tasks );

Console.WriteLine( "Task 1:  {0}Finished", t1.IsCompleted ? "" : "Not " );
Console.WriteLine( "Task 2:  {0}Finished", t2.IsCompleted ? "" : "Not " );
Console.Read();
}

private async Task CountCharactersAsync( int id, string site )
{
WebClient wc = new WebClient();
string result = await wc.DownloadStringTaskAsync( new Uri( site ) );
Console.WriteLine( "   Call {0} completed:     {1, 4:N0} ms",
id, sw.Elapsed.TotalMilliseconds );
return result.Length;
}
}

class Program
{
static void Main()
{
MyDownloadString ds = new MyDownloadString();
ds.DoRun();
}
}`

这段代码产生以下输出。请注意,当使用IsCompleted方法检查时,两个Task都没有完成。


Task 1:  Not Finished Task 2:  Not Finished Call 1 completed:      166 ms Call 2 completed:      425 ms


如果我们在DoRun中间取消注释前两行代码,如下面三行代码所示,该方法将创建一个包含这两个任务的数组,并将该数组传递给WaitAll方法。然后,代码将停止并等待,直到两个任务都完成后再继续执行。

   Task<int>[] tasks = new Task<int>[] { t1, t2 }; Task.WaitAll( tasks ); //Task.WaitAny( tasks );

当我们使用这种配置运行代码时,结果如下:


   Call 1 completed:      137 ms Call 2 completed:      601 ms Task 1:  Finished Task 2:  Finished


如果我们再次修改该部分,注释掉WaitAll方法调用,并取消注释WaitAny方法调用,代码如下所示:

   Task<int>[] tasks = new Task<int>[] { t1, t2 }; //Task.WaitAll( tasks ); Task.WaitAny( tasks );

在这种情况下,WaitAny调用暂停,直到至少一个Task完成。当我们再次运行代码时,结果如下:


   Call 1 completed:      137 ms Task 1:  Finished Task 2:  Not Finished Call 2 completed:      413 ms


对于WaitAllWaitAny方法,各有四个重载,允许不同的方式继续执行,而不是完成任务。不同的重载允许你设置一个超时或者使用一个CancellationToken来强制进程继续。表 20-3 显示了这些方法的重载。

Image

异步等待异步方法中的任务

在上一节中,您学习了如何同步等待Task完成。然而,有时候,在你的async方法中,你会想要等待Task s 作为你的await表情。这允许您的async方法返回到调用方法,但允许async方法等待一组任务中的一个或全部任务完成。允许这样做的调用是Task.WhenAllTask.WhenAny方法。这些方法被称为组合子

下面的代码展示了一个使用Task.WhenAll方法的例子。这个方法异步等待,不需要在主线程上花费时间,直到所有相关的Task都完成。注意,await表达式的任务是Task.WhenAll调用。

`using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;

class MyDownloadString
{
public void DoRun()
{
Task t = CountCharactersAsync( "http://www.microsoft.com",
"http://www.illustratedcsharp.com");

Console.WriteLine( "DoRun:  Task {0}Finished", t.IsCompleted ? "" : "Not " );
Console.WriteLine( "DoRun:  Result = {0}", t.Result );
}

private async Task CountCharactersAsync(string site1, string site2 )
{
WebClient wc1 = new WebClient();
WebClient wc2 = new WebClient();
Task t1 = wc1.DownloadStringTaskAsync( new Uri( site1 ) );
Task t2 = wc2.DownloadStringTaskAsync( new Uri( site2 ) );

List<Task> tasks = new List<Task>();
tasks.Add( t1 );
tasks.Add( t2 );

await Task.WhenAll( tasks );

Console.WriteLine( "     CCA:  T1 {0}Finished", t1.IsCompleted ? "" : "Not " );
Console.WriteLine( "     CCA:  T2 {0}Finished", t2.IsCompleted ? "" : "Not " );

return t1.IsCompleted ? t1.Result.Length : t2.Result.Length;
}
}    class Program
{
static void Main()
{
MyDownloadString ds = new MyDownloadString();
ds.DoRun();
}
}`

该代码产生以下输出:


DoRun:  Task Not Finished CCA:  T1 Finished CCA:  T2 Finished DoRun:  Result = 1020


Task.WhenAny组合器异步等待,直到与之关联的Task之一完成。如果您将await表达式更改为使用Task.WhenAny方法而不是Task.WhenAll方法,并重新运行程序,它会产生以下输出:


DoRun:  Task Not Finished CCA:  T1 Finished CCA:  T2 Not Finished DoRun:  Result = 1020


任务。延迟方法

Task.Delay方法创建一个Task对象,该对象在线程上停止自己的处理,并在经过一段时间后完成。然而,与阻塞线程上的工作的Thread.Sleep不同,Task.Delay不阻塞线程,所以它可以继续处理其他工作。

下面的代码展示了一个使用Task.Delay方法的例子:

`   class Simple
{
Stopwatch sw = new Stopwatch();

public void DoRun()
{
Console.WriteLine( "Caller: Before call" );
ShowDelayAsync();
Console.WriteLine( "Caller: After call" );
}

private async void ShowDelayAsync (  )
{
sw.Start();
Console.WriteLine( "   Before Delay: {0}", sw.ElapsedMilliseconds );
await Task.Delay( 1000 );
Console.WriteLine( "   After Delay : {0}", sw.ElapsedMilliseconds );
}
}

class Program
{
static void Main()
{
Simple ds = new Simple ();
ds.DoRun();
Console.Read();
}
}`

该代码产生以下输出:


Caller: Before call Before Delay: 0 Caller: After call After Delay : 1007


Delay方法有四个重载,允许不同的指定时间段的方式,也允许一个CancellationToken对象。表 20-4 显示了该方法的四种重载。

Image

GUI 程序中的异步操作

尽管到目前为止,本章中的所有代码都是针对控制台应用的,但异步方法对 GUI 程序尤其有用。

其原因是 GUI 程序被设计成使得显示中的几乎每个变化,包括服务按钮点击、显示标签和移动窗口本身,都必须在主 GUI 线程上完成。这在 Windows 程序中实现的方式是通过使用消息,这些消息被放入由消息泵管理的消息队列

消息泵从队列中取出一条消息,并调用该消息的处理程序代码。当处理程序代码完成时,消息泵获取下一条消息并重复这个循环。

由于这种架构,处理程序代码必须简短,这样才不会阻碍进程和其他 GUI 操作的处理。如果特定消息的处理程序代码花费了很长时间,消息队列中就会出现消息积压,程序就会变得没有响应,因为在长时间运行的处理程序完成之前,这些消息都不能被处理。

图 20-10 显示了 WPF 程序窗口的两个版本。该窗口由一个状态标签及其下方的一个按钮组成。程序员的意图是程序用户单击按钮,按钮的处理程序代码将执行以下操作:

  • Disable the button, so that the user can no longer click when the handler is working.
  • Change the message to "Doing something" so that users can know that the program is working.
  • Let the program sleep for 4 seconds-simulate some work.
  • Change the message back to the original message and re-enable the button.

图右边的截屏显示了程序员所期望的按钮被按下后 4 秒钟的窗口。然而,事实证明,结果并非如此。当程序员点击按钮时,似乎什么都没有发生,当他在点击按钮几秒钟后试图在屏幕上移动窗口时,窗口被冻结在屏幕上,不会移动——直到 4 秒钟结束后,窗口突然倾斜到新的位置。

Image

***图 20-10。*一个简单的 WPF 程序,有一个按钮和一个状态字符串

Image WPF 是微软对 Windows Forms GUI 编程框架的替代。关于 WPF 编程的更多信息,请参见我的书插图 WPF(2009 年出版)。

要在 Visual Studio 2012 中重新创建名为 MessagePump 的 WPF 程序,请执行以下操作:

  1. Select the file Image New Image project menu item to open the new project window.
  2. In the left pane of the window, open the installed template section, if it is not already open.
  3. Under the C# category, click the Windows entry. This will populate the center pane with the installed Windows program template. Click on the WPF application.
  4. , and then enter MessagePump in the [name] text box at the bottom of the window. Below it, select a location, and then click the OK button.
  5. Modify the XAML tag in the file MainWindow.xaml to be the same as the tag below. This will create a window with status labels and buttons.    <Window x:Class="MessagePump.MainWindow" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Pump" Height="120" Width="200  "> <StackPanel> <Label Name="lblStatus" Margin="10,5,10,0" >Not Doing Anything</Label> <Button Name="btnDoStuff" Content="Do Stuff" HorizontalAlignment="Left" Margin="10,5" Padding="5,2" Click="btnDoStuff_Click"/> </StackPanel> </Window>
  6. Modify the code-behind file MainWindow.xaml.cs to match the following C# code.    using System.Threading; using System.Threading.Tasks; using System.Windows; namespace MessagePump { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void btnDoStuff_Click( object sender, RoutedEventArgs e ) { btnDoStuff.IsEnabled = false; lblStatus.Content    = "Doing Stuff"; Thread.Sleep( 4000 ); lblStatus.Content    = "Not Doing Anything"; btnDoStuff.IsEnabled = true; } } }

当你运行这个程序时,你会发现它的行为符合前面的描述,即按钮没有被禁用,状态标签没有改变,如果你试图移动窗口,它不会移动,直到 4 秒钟过去。

这种可能令人惊讶的行为的原因非常简单。图 20-11 说明了这种情况。当点击按钮时,按钮Click消息被放置在消息队列中。消息泵将消息从队列中移除,并启动按钮 click 的处理程序代码——这是方法btnDoStuff_ClickbtnDoStuff_Click处理程序将触发我们想要的行为的消息放到队列中,如图右侧所示。但是在处理程序本身退出之前,这些消息都不能被执行,直到它休眠 4 秒钟并退出。然后所有这些事情都发生了,但它们来得太快了。

Image

***图 20-11。*消息泵从消息队列的前端发送消息。当按钮消息处理程序执行时,其他动作的消息在队列中累积,直到它完成后才能执行。

但是,如果我们可以让处理程序将前两条消息放入队列,然后将自己从处理器中取出,只在 4 秒钟结束时将自己放回队列中,那么这些消息和任何其他消息都可以在等待期间得到处理,流程将按照我们希望的那样执行并保持响应。

我们可以使用async / await特性轻松实现这一点,如下面修改后的处理程序代码所示。当到达await语句时,处理程序返回到调用方法并离开处理器,允许处理其他消息——包括它刚刚放入的两条消息。在一个可实现的任务完成之后(在本例中是Task.Delay,继续(方法的剩余部分)被调度回线程上。

`   private async void btnDoStuff_Click( object sender, RoutedEventArgs e )
{
btnDoStuff.IsEnabled = false;
lblStatus.Content    = "Doing Stuff";

await Task.Delay( 4000 );

lblStatus.Content    = "Not Doing Anything";
btnDoStuff.IsEnabled = true;
}`

任务。产量

Task.Yield方法创建一个可立即返回的变量。等待一个Yield允许async方法返回到调用方法,同时继续执行async方法的代码。您可以认为这是离开消息队列的前面,到队列的后面,以便让其他任务获得处理器上的时间。

下面的示例代码展示了一个async方法,它通过执行一个循环每 1000 次产生一次控制权。每次执行Yield方法时,它都允许线程中的其他任务执行。

`   static class DoStuff
{
public static async Task FindSeriesSum( int i1 )
{
int sum = 0;
for ( int i=0; i < i1; i++ )
{
sum += i;
if ( i % 1000 == 0 )
await Task.Yield();
}

return sum;
}
}

class Program
{
static void Main()
{
Task value = DoStuff.FindSeriesSum( 1000000 );
CountBig( 100000 );  CountBig( 100000 );
CountBig( 100000 );  CountBig( 100000 );
Console.WriteLine( "Sum: {0}", value.Result );
}

private static void CountBig( int p )
{
for ( int i=0; i < p; i++ )
;
}
}`

该代码产生以下输出:


Sum: 1783293664


在 GUI 程序中,Yield方法非常有用,可以分解大量工作,让其他任务使用处理器。

使用异步 Lambda 表达式

到目前为止,本章中你只看到了async 方法。但是如果你还记得,我说过你也可以使用匿名方法和 ?? 表达式。这些构造对于工作量很少的事件处理程序特别有用。下面的代码片段显示了一个 lambda 表达式,它被注册为按钮单击事件的事件处理程序。

   startWorkButton.Click += async ( sender, e ) => { // Do the Click handler work. };

下面是一个简短的 WPF 程序,展示了它的使用。以下是代码隐藏:

`   using System.Threading.Tasks;
using System.Windows;

namespace AsyncLambda
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();

Async lambda expression
            ↓           
startWorkButton.Click += async ( sender, e ) ⇒
{
SetGuiValues( false, "Work Started" );
await DoSomeWork();
SetGuiValues( true, "Work Finished" );
};
}

private void SetGuiValues(bool buttonEnabled, string status)

private Task DoSomeWork()
{
return Task.Delay( 2500 );
}
}
}`

以下标记是程序的 XAML 文件:

`   <Window x:Class="AsyncLambda.MainWindow"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Async Lambda" Height="115" Width="150">






`

修改代码隐藏文件MainWindow.xaml.cs,使其与以下内容匹配:

using System.Threading; using System.Threading.Tasks; using System.Windows; namespace WpfAwait { `      public partial class MainWindow : Window
{
CancellationTokenSource _cancellationTokenSource;
CancellationToken       _cancellationToken;

public MainWindow()
{
InitializeComponent();
}

private async void btnProcess_Click( object sender, RoutedEventArgs e )
{
btnProcess.IsEnabled = false;

_cancellationTokenSource = new CancellationTokenSource();
_cancellationToken       = _cancellationTokenSource.Token;

int completedPercent = 0;
for ( int i = 0; i < 10; i++ )
{
if ( _cancellationToken.IsCancellationRequested )
break;
try {
await Task.Delay( 500, _cancellationToken );
completedPercent = ( i + 1 ) * 10;
}
catch ( TaskCanceledException ex ) {
completedPercent = i * 10;
}
progressBar.Value = completedPercent;
}

string message = _cancellationToken.IsCancellationRequested
? string.Format("Process was cancelled at {0}%.", completedPercent)
: "Process completed normally.";
MessageBox.Show( message, "Completion Status" );

progressBar.Value = 0;
btnProcess.IsEnabled = true;
btnCancel.IsEnabled  = true;
}

private void btnCancel_Click( object sender, RoutedEventArgs e ) {
if ( !btnProcess.IsEnabled )
{
btnCancel.IsEnabled = false;
_cancellationTokenSource.Cancel();
}
}
}
}`

背景工人类

在前面的小节中,您学习了如何使用async / await特性来异步处理任务。在本节中,您将学习另一种进行异步工作的方法——在本例中,在后台线程上。async / await功能最适合需要在后台完成的小型离散任务。

但是,有时您可能希望设置另一个线程,该线程在后台持续运行,执行工作,偶尔与主线程通信。这个BackgroundWorker类非常适合这个。图 20-14 显示了该类的主要成员。

  • The first two properties in the figure are used to set whether the background task can report the progress to the main thread and whether it supports cancellation from the main thread. You use the third attribute to determine whether the background task is running.
  • This class has three events to represent different program events and states. You need to write event handlers for these events, so that you can take whatever action suits your program.
    • The DoWork event is raised when the background thread starts.
    • ProgressChanged event is raised when the background task reports the progress. The RunWorkerCompleted event is triggered when the background worker exits.
  • All three methods are used to initiate actions or change states. Call the RunWorkerAsync method to retrieve the background thread that executes the DoWork event handler.
  • Call the CancelAsync method to set the CancellationPending attribute to true. DoWork The responsibility of the event handler is to check this attribute to determine whether it should stop processing.
  • ReportProgress method can be called by DoWork event handler (from background thread ) when it wants to report its progress to the main thread.

Image

***图 20-14。*background worker 类的主要成员

要使用一个BackgroundWorker类对象,你需要编写下面的事件处理程序。第一个是必需的,因为它包含了您希望在后台线程中执行的代码。另外两个是可选的,您可以包含或不包含它们,这取决于您的程序需要什么。

  • The handler attached to the DoWork event contains the code that you want to execute on a separate thread in the background.
    • In Figure 20-15 , the handler is named DoTheWork, and it is located in a gradient shadow box to show that it is executed in the background thread.
    • When the main thread calls the RunWorkerAsync method of the BackgroundWorker object, the DoWork event is raised.
  • The background thread communicates with the main thread by calling ReportProgress method. When this happens, the ProgressChanged event will be raised, and the main thread can use the handler attached to the ProgressChanged event to handle the event.
  • The handler attached to the RunWorkerCompleted event contains the code to be executed on the main thread after the background thread completes the execution of the DoWork event handler.

图 20-15 显示了一个程序的结构,事件处理程序附加在BackgroundWorker对象的事件上。

Image

***图 20-15。*您的代码必须为控制任务执行流程的事件提供事件处理程序。

这些事件处理程序的代表如下。每个都将一个object引用作为第一个参数,将一个EventArgs类的专门化子类作为第二个参数。

`   void DoWorkEventHandler             ( object sender, DoWorkEventArgs e )

void ProgressChangedEventHandler    ( object sender, ProgressChangedEventArgs e )

void RunWorkerCompletedEventHandler ( object sender, RunWorkerCompletedEventArgs e)`

图 20-16 展示了这些事件处理程序使用的EventArg类的结构。

Image

***图 20-16。*background worker 事件处理程序使用的 EventArg 类

当您编写了事件处理程序并将其附加到事件时,可以通过执行下列操作来使用类:

  • First, create an object of class BackgroundWorker and configure it.
    • If you want the worker thread to communicate the progress with the main thread, set the WorkerReportsProgress property to true.
    • If you want to be able to cancel the worker thread from the main thread, set the WorkerSupportsCancellation attribute to true.
  • Now that the object is configured, it can be started by calling the RunWorkerAsync method of the object. This will retrieve a background thread that caused the DoWork event and execute the handler for the event in the background.

现在主线程和后台线程都在运行。当后台线程运行时,您可以在主线程上继续处理。

在主线程中,如果您已经启用了WorkerSupportsCancellation属性,那么您可以调用对象的CancelAsync方法。正如你在本章开始看到的CancellationToken,这并没有取消背景线索。相反,它将对象的CancellationPending属性设置为true。运行在后台线程上的DoWork事件处理程序代码需要定期检查CancellationPending属性,看它是否应该退出。

同时,后台线程继续执行其计算任务,并执行以下操作:

  • If the WorkerReportsProgress attribute is true, and the background thread has progress to report to the main thread, it must call the ReportProgress method of the BackgroundWorker object. This raises the ProgressChanged event in the main thread, which runs the corresponding event handler.
  • If the WorkerSupportsCancellation attribute is enabled, then the DoWork event handler code should check the CancellationPending attribute periodically to determine whether it is canceled. If so, it should quit.
  • If the background thread completes its processing without being canceled, it can return a result to the main thread by setting the Result field in the DoWorkEventArgs parameter shown in Figure 20-16 earlier.

当后台线程退出时,会引发RunWorkerCompleted事件,并在主线程上执行其处理程序。RunWorkerCompletedEventArgs参数可以包含来自现在已完成的后台线程的信息,比如返回值和线程是否被取消。

WPF 程序中 BackgroundWorker 类的例子

由于BackgroundWorker类主要用于 GUI 编程,下面的程序展示了它在一个简单的 WPF 程序中的用法。

该程序产生图 20-17 中左侧所示的窗口。当您单击 Process 按钮时,它会启动后台线程,该线程每半秒钟向主线程报告一次,并以 10%的增量递增顶部的进度条。完成后,它会在图的右侧显示对话框。

Image

***图 20-17。*使用 BackgroundWorker 类的示例 WPF 程序

要重新创建这个 WPF 程序,使用 Visual Studio 创建一个名为SimpleWorker的新 WPF 应用。修改您的MainWindow.xaml文件以匹配下面的列表:

`   <Window x:Class="SimpleWorker.MainWindow"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="150  " Width="250">





`

修改MainWindow.xaml.cs文件以匹配以下列表:

`using System.Windows;
using System.ComponentModel;
using System.Threading;

namespace SimpleWorker
{
public partial class MainWindow : Window
{
BackgroundWorker bgWorker = new BackgroundWorker();

public MainWindow()
{
InitializeComponent();

// Set BackgroundWorker properties
bgWorker.WorkerReportsProgress      = true;
bgWorker.WorkerSupportsCancellation = true;

// Connect handlers to BackgroundWorker object.
bgWorker.DoWork             += DoWork_Handler;
bgWorker.ProgressChanged    += ProgressChanged_Handler;
bgWorker.RunWorkerCompleted += RunWorkerCompleted_Handler;
}

private void btnProcess_Click( object sender, RoutedEventArgs e )
{
if ( !bgWorker.IsBusy )
bgWorker.RunWorkerAsync();
}

private void ProgressChanged_Handler( object sender,
ProgressChangedEventArgs args )
         private void DoWork_Handler( object sender, DoWorkEventArgs args )
{
BackgroundWorker worker = sender as BackgroundWorker;

for ( int i = 1; i ⇐ 10; i++ )
{
if ( worker.CancellationPending )
{
args.Cancel = true;
break;
}
else
{
worker.ReportProgress( i * 10 );
Thread.Sleep( 500 );
}
}
}

private void RunWorkerCompleted_Handler( object sender,
RunWorkerCompletedEventArgs args )
{
progressBar.Value = 0;

if ( args.Cancelled )
MessageBox.Show( "Process was cancelled.", "Process Cancelled" );
else
MessageBox.Show( "Process completed normally.", "Process Completed" );
}

private void btnCancel_Click( object sender, RoutedEventArgs e )
{
bgWorker.CancelAsync();
}
}
}`

并联回路

在这一节中,我们将简略地看一下任务并行库。任务并行库是 BCL 中的类库,它极大地简化了并行编程。详细介绍这个库远远超出了我在这一章中所能做的。因此,不幸的是,我不得不满足于通过介绍两个非常简单的构造来吊起你的胃口,这样你就可以快速轻松地学习和使用了。这些是Parallel.For回路和Parallel.ForEach回路。这些构造位于System.Threading.Tasks名称空间中。

到目前为止,我相信你已经非常熟悉 C# 的标准forforeach循环了。这些都是常见且非常强大的构造。很多时候,当使用这些构造时,每次迭代都依赖于前一次迭代中的计算或操作。但情况并非总是如此。如果迭代是相互独立的,并且您在多处理器机器上运行,如果您可以将不同的迭代放在不同的处理器上并并行处理它们,这可能是一个巨大的优势。这正是Parallel.ForParallel.ForEach构造所做的。

这些构造是带有输入参数的方法的形式。Parallel.For方法有 12 个重载,但最简单的重载具有如下代码行所示的签名:

   void Parallel.For( int *fromInclusive*, int *toExclusive*, Action *body*);

这些参数如下:

  • *fromInclusive* The parameter is the first integer in the iteration sequence.
  • toExclusive The parameter is an integer larger than the last index in the iteration sequence. That is to say, this is the same as using the expression index < ToExclusive for comparison.
  • body is a commission that accepts a single input parameter. The code of body is executed once every iteration.

下面的代码是一个使用Parallel.For构造的例子。它从 0 迭代到 14(记住,作为实际参数列出的 15 比顶部迭代索引多 1),并打印出迭代索引和索引的平方。这个应用符合每个迭代独立于任何其他迭代的要求。还要注意,您必须使用System.Threading.Tasks名称空间。

`   using System;
using System.Threading.Tasks;         // Must use this namespace

namespace ExampleParallelFor
{
class Program
{
static void Main( )
{
Parallel.For( 0, 15, i ⇒         
Console.WriteLine( "The square of {0} is {1}", i, i * i ));
}
}
}`

在我装有双核处理器的电脑上运行一次这段代码,产生了以下输出。请注意,不保证迭代的任何特定顺序。


The square of 0 is 0 The square of 7 is 49 The square of 8 is 64 The square of 9 is 81 The square of 10 is 100 The square of 11 is 121 The square of 12 is 144 The square of 13 is 169 The square of 3 is 9 The square of 4 is 16 The square of 5 is 25 The square of 6 is 36 The square of 14 is 196 The square of 1 is 1 The square of 2 is 4


另一个例子是下面的代码。这个程序用迭代索引的平方并行填充一个整数数组。

`   class Program
{
static void Main()
{
const int maxValues = 50;
int[] squares = new int[maxValues];

Parallel.For( 0, maxValues, i ⇒ squares[i] = i * i );
}
}`

在这个例子中,尽管迭代可能以任何顺序并行执行,但最终结果是一个包含前 50 个方块的数组——按顺序!

另一个并行循环结构是Parallel.ForEach方法。这个方法有十多个多载,但最简单的如下:

TSource

  • source is a collection of TSource objects.
  • body is a lambda expression applied to each element in the set.

   static ParallelLoopResult ForEach<TSource>( IEnumerable<TSource> *source*, Action<TSource> *body*)

下面的代码是使用Parallel.ForEach方法的一个例子。在这种情况下,TSource就是string,而 source 就是一个string[]

`   using System;
using System.Threading.Tasks;

namespace ParallelForeach1
{
class Program
{
static void Main()
{
string[] squares = new string[]
{ "We", "hold", "these", "truths", "to", "be", "self-evident",
"that", "all", "men", "are", "created", "equal"};

Parallel.ForEach( squares,
i ⇒ Console.WriteLine( string.Format("{0} has {1} letters", i, i.Length) ));
}
}
}`

这段代码在我的双核处理器电脑上运行了一次,产生了以下输出,但顺序可能每次都会改变:


"We" has 2 letters "equal" has 5 letters "truths" has 6 letters "to" has 2 letters "be" has 2 letters "that" has 4 letters "hold" has 4 letters "these" has 5 letters "all" has 3 letters "men" has 3 letters "are" has 3 letters "created" has 7 letters "self-evident" has 12 letters


其他异步编程模式

对于您将要生成的大多数异步代码,您可能会使用async/await特性、本章第一部分中介绍的BackgroundWorker类或者任务并行库。然而,仍然有可能需要使用旧的模式来产生异步代码。为了完整起见,我将涵盖这些模式,从这一节开始到本章的结尾。学习完这些旧模式后,你会更加感激有了async / await功能,生活变得简单多了。

第十三章讲述了委托的主题,你看到了当一个委托对象被调用时,它调用包含在其调用列表中的方法。这是同步完成的,就像程序调用了这些方法一样。

如果一个委托对象在其调用列表中只有一个方法(我称之为引用的方法),它可以异步执行该方法。delegate 类有两个方法,叫做BeginInvokeEndInvoke,用来做这件事。您可以按以下方式使用这些方法:

  • When you call the BeginInvoke method of the delegate, it starts to execute the method it refers to on a separate thread, and then immediately returns to the initial thread. When the referenced methods are executed in parallel, the initial thread continues to run.
  • When your program wants to retrieve the result of the completed asynchronous method, it either checks the T2 attribute of T1 returned by T0 or calls the T3 method of the delegate to wait for the delegate to complete.

图 20-18 显示了使用该过程的三种标准模式。在所有这三种模式中,初始线程启动一个异步方法调用,然后进行一些额外的处理。然而,这些模式的不同之处在于初始线程接收衍生线程已经完成的信息的方式。

  • In wait-until-done mode, after generating asynchronous methods and doing some extra processing, the initial thread pauses and waits for the generated thread to finish before continuing.
  • In the polling mode, the initial thread periodically checks whether the generated thread has completed, and if not, it continues additional processing.
  • In the callback mode, the initial thread continues to execute without waiting or checking whether the derived thread has finished. On the contrary, when the referenced method in the derived thread completes, it calls a callback method, which processes the result of the asynchronous method before calling EndInvoke.

Image

***图 20-18。*异步方法调用的标准模式

开始 Invoke 和结束 Invoke

在我们看这些异步编程模式的例子之前,让我们仔细看看BeginInvokeEndInvoke方法。关于BeginInvoke需要知道的一些重要事情如下:

  • When BeginInvoke is called, the actual parameters in the parameter table consist of the following:
    • Referenced method
    • The other two parameters are called callback parameter and state parameter.
  • BeginInvoke Retrieve a thread from the thread pool and start the referenced method running on the new thread.
  • BeginInvoke Returns a reference to the object that implements the IAsyncResult interface to the calling thread. This interface reference contains information about the current state of asynchronous methods on thread pool threads. The initial thread then continues to execute.

下面的代码展示了一个调用委托的BeginInvoke方法的例子。第一行声明了名为MyDel的委托类型。下一行声明了一个名为Sum的方法,它匹配委托。

  • The following line declares a delegate object del with the delegate type MyDel, and initializes its call list with the Sum method.
  • Finally, the last line of code calls the BeginInvoke method of the entrusted object and provides it with two entrusted parameters 3 and 5 and two BeginInvoke parameters callback and state, which are set to null in this example. When executed, the BeginInvoke method performs two actions:
    • It gets a thread from the thread pool, starts the method Sum running on the new thread, and provides it with 3 and 5 as its actual parameters.
    • It collects the state information of the new thread, makes it available by referencing the interface of type IAsyncResult, and then returns it to the calling thread. In this example, the calling thread stores it in a variable named iar.

   delegate long MyDel( int first, int second );          // Delegate declaration ... static long Sum(int x, int y){ ... }                   // Method matching delegate ... MyDel del        = new MyDel(Sum);                     // Create delegate object <ins>IAsyncResult iar</ins> = <ins>del.BeginInvoke</ins>( <ins>3, 5,</ins> <ins>null, null</ins> ); ↑                 ↑           ↑        ↑ Information about       Invoke delegate      Delegate    Extra new thread          asynchronously      params    params

使用EndInvoke方法检索异步方法调用返回的值,并释放线程使用的资源。EndInvoke具有以下特点:

  • It takes the reference to IAsyncResult returned by BeginInvoke method as a parameter and finds the thread it refers to.
  • If the thread pool thread has exited, EndInvoke will perform the following operations:
    • It cleans up the unfinished part of the exited thread and releases its resources.
    • Find the value returned by the referenced method and return it as the return value.
  • If the thread pool thread is still running when EndInvoke is called, the calling thread will stop and wait for it to finish before clearing the return value. Because EndInvoke is cleaned up after the generated thread, you must make sure to call a EndInvoke for each BeginInvoke.
  • If an asynchronous method triggers an exception, it will be thrown when EndInvoke is called.

下面一行代码展示了一个调用EndInvoke从异步方法中检索值的例子。您必须始终包含对IAsyncResult对象的引用作为参数。

             Delegate object ↓ <ins>long result</ins> = del.EndInvoke( iar ); ↑                        ↑ Return value from             IAsyncResult asynchronous method             object

EndInvoke提供异步方法调用的所有输出,包括refout参数。如果一个委托被引用的方法有refout参数,它们必须在引用IAsyncResult对象之前包含在EndInvoke的参数列表中,如下图所示:

  <ins>long result</ins> = del.EndInvoke(<ins>out someInt</ins>, iar); ↑                           ↑        ↑ Return value from                    Out     IAsyncResult asynchronous method                parameter     object

等待完成的模式

现在您已经理解了BeginInvokeEndInvoke委托方法,我们可以看看异步编程模式了。我们要看的第一个是等待完成模式。在这种模式中,初始线程启动一个异步方法调用,进行一些额外的处理,然后停止并等待,直到产生的线程完成。总结如下:

   IAsyncResult iar = del.BeginInvoke( 3, 5, null, null ); // Do additional work in the calling thread, while the method // is being executed asynchronously in the spawned thread. ... long result = del.EndInvoke( iar );

下面的代码展示了这种模式的完整示例。这段代码使用Thread类的Sleep方法将自己挂起 100 毫秒(1/10 秒)。Thread类位于System.Threading名称空间中。

`   using System;
using System.Threading;                         // For Thread.Sleep()

delegate long MyDel( int first, int second );   // Declare delegate type.

class Program {
static long Sum(int x, int y)                // Declare method for async.
{
Console.WriteLine("                       Inside Sum");
Thread.Sleep(100);

return x + y;
}
static void Main( ) {
MyDel del = new MyDel(Sum);

Console.WriteLine( "Before BeginInvoke" );
IAsyncResult iar = del.BeginInvoke(3, 5, null, null); // Start async
Console.WriteLine( "After  BeginInvoke" );

Console.WriteLine( "Doing stuff" );

long result = del.EndInvoke( iar );    // Wait for end and get result
Console.WriteLine( "After  EndInvoke: {0}", result );
}
}`

该代码产生以下输出:


Before BeginInvoke After  BeginInvoke Doing stuff Inside Sum After  EndInvoke: 8


async result 类

既然您已经看到了最简单形式的BeginInvokeEndInvoke的运行,现在是时候仔细看看IAsyncResult,它是使用这些方法不可或缺的一部分。

BeginInvoke返回对IAsyncResult接口的引用,该接口位于AsyncResult类型的类对象内部。AsyncResult类表示异步方法的状态。图 20-19 展示了这个类的一些重要部分。关于该类,需要了解的重要事项如下:

  • When you call the BeginInvoke method of a delegate object, the system creates an object of class AsyncResult. However, it does not return references to class objects. Instead, it returns a reference to the interface contained in the object IAsyncResult.
  • The AsyncResult object contains an attribute named AsyncDelegate, which returns a reference to the delegate called to start the asynchronous method. However, this property is part of the class object, not part of the interface.
  • The IsCompleted property returns a Boolean value indicating whether the asynchronous method has been completed.
  • The AsyncState property returns a reference to the object listed as the state parameter in the BeginInvoke method call. It returns a reference of type object. I will explain this in the callback mode section.

Image

***图 20-19。*一个 AsyncResult 类对象

轮询模式

在轮询模式中,初始线程发起一个异步方法调用,进行一些额外的处理,然后使用IAsyncResult对象的IsCompleted方法来定期检查所产生的线程是否已经完成。如果异步方法已经完成,初始线程调用EndInvoke并继续。否则,它会做一些额外的处理,并在稍后再次检查。本例中的“处理”只包括从 0 到 10,000,000 的计数。

`   delegate long MyDel(int first, int second);

class Program
{
static long Sum(int x, int y)
{
Console.WriteLine("                  Inside Sum");
Thread.Sleep(100);

return x + y;
}

static void Main()
{
MyDel del = new MyDel(Sum); Spawn asynchronous method.

IAsyncResult iar = del.BeginInvoke(3, 5, null, null); // Start async.
Console.WriteLine("After BeginInvoke");
Check whether the asynchronous method is done.
    ↓          
while ( !iar.IsCompleted )
{
Console.WriteLine("Not Done");

// Continue processing, even though in this case it's just busywork.
for (long i = 0; i < 10000000; i++)
;                                       // Empty statement
}
Console.WriteLine("Done");
Call EndInvoke to get result and clean up.
       ↓         
long result = del.EndInvoke(iar);
Console.WriteLine("Result: {0}", result);
}
}`

这段代码产生以下输出:


After BeginInvoke Not Done Inside Sum Not Done Not Done Done Result: 8


回调模式

在前面的两种模式中,等待完成和轮询,初始线程只有在知道派生线程已经完成后才继续它的控制流。然后,它检索结果并继续。

回调模式的不同之处在于,一旦初始线程产生了异步方法,它就继续前进,不再与它同步。当异步方法调用完成时,系统调用用户提供的方法来处理其结果,并调用委托的EndInvoke方法。这个用户定义的方法被称为回调方法,或者简称为回调

BeginInvoke参数列表末尾的两个额外参数与回调方法一起使用,如下所示:

  • The first of the two parameters callback is the name of the callback method.
  • The second parameter, namely the state parameter, can be null or a reference to the object to be passed to the callback method. You will be able to access the object by using the AsyncState property of the method and the IAsyncResult parameter of the method. The type of this parameter is object.
回调方法

回调方法的签名和返回类型必须是由AsyncCallback委托类型描述的形式。该表单要求该方法接受类型为IAsyncResult的单个参数,并具有一个void返回类型,如下所示:

   void AsyncCallback( IAsyncResult iar )

有几种方法可以将回调方法提供给BeginInvoke方法。由于BeginInvoke中的callback参数是一个类型为AsyncCallback的委托,您可以将其作为委托提供,如下面的第一个代码语句所示。或者,您可以只提供回调方法的名称,让编译器为您创建委托。这两种形式在语义上是等价的。

                            Create a delegate with the callback method. IAsyncResult iar1 =         <ins>         ↓                   </ins> del.BeginInvoke(3, 5, new AsyncCallback(CallWhenDone), null); Just use the callback method’s name. <ins>      ↓        </ins> IAsyncResult iar2 = del.BeginInvoke(3, 5, CallWhenDone, null);

第二个附加的BeginInvoke参数(参数列表中的最后一个)用于向回调方法发送一个对象。它可以是任何类型的对象,因为参数的类型是object。在回调方法中,你必须将它转换成正确的类型。

在回调方法中调用 EndInvoke

在回调方法中,您的代码应该调用委托的EndInvoke方法,并负责处理异步方法执行的输出结果。不过,要调用委托的EndInvoke方法,您需要一个对委托对象的引用,它在初始线程中——而不是在衍生线程中。

如果您没有将BeginInvokestate参数用于任何其他用途,您可以使用它将委托引用发送给回调方法,如下所示:

                  Delegate object                Send delegate object as state param. ↓                                  ↓   IAsyncResult iar = del.BeginInvoke(3, 5, CallWhenDone, del);

否则,您可以从作为参数发送到方法中的IAsyncResult对象中提取委托的引用。这在以下代码中显示,并在图 20-20 中说明。

  • The single parameter of the callback method is a reference to the IAsyncResult interface of the asynchronous method just completed. Remember, the IAsyncResult interface object is inside the AsyncResult class object.
  • Although the IAsyncResult interface has no reference to the entrusted object, the AsyncResult class object encapsulating it has a reference to the entrusted object. Therefore, the first line in the example method body obtains the reference to the class object by converting the interface reference to the class type. The variable T2 now has a reference to the class object.
  • With the reference to the class object, you can now use the AsyncDelegate attribute of the class object and convert it into the appropriate delegate type. This provides you with a delegate reference, and then you can use it to call EndInvoke.

`   using System.Runtime.Remoting.Messaging;     // Contains AsyncResult class

void CallWhenDone( IAsyncResult iar )
{
AsyncResult ar = (AsyncResult) iar;       // Get class object reference.
MyDel del = (MyDel) ar.AsyncDelegate;     // Get reference to delegate.

long Sum = del.EndInvoke( iar );          // Call EndInvoke.
...
}` Image

***图 20-20。*在回调方法中提取委托的引用

下面的代码将所有这些放在一起,是一个使用回调模式的例子:

`   using System;
using System.Runtime.Remoting.Messaging;      // To access the AsyncResult type
using System.Threading;

delegate long MyDel(int first, int second);

class Program
{
static long Sum(int x, int y)
{
Console.WriteLine("                         Inside Sum");
Thread.Sleep(100);
return x + y;
}
static void CallWhenDone(IAsyncResult iar)
{
Console.WriteLine("                         Inside CallWhenDone.");
AsyncResult ar = (AsyncResult) iar;         
MyDel del = (MyDel)ar.AsyncDelegate;

long result = del.EndInvoke(iar);
Console.WriteLine
("                         The result is: {0}.", result);
}
static void Main()
{
MyDel del = new MyDel(Sum);

Console.WriteLine("Before BeginInvoke");
IAsyncResult iar =
del.BeginInvoke(3, 5, new AsyncCallback(CallWhenDone), null);

Console.WriteLine("Doing more work in Main.");
Thread.Sleep(500);
Console.WriteLine("Done with Main. Exiting.");
}
}`

该代码产生以下输出:


Before BeginInvoke Doing more work in Main. Inside Sum Inside CallWhenDone. The result is: 8. Done with Main. Exiting.


计时器

计时器提供了另一种定期循环运行异步方法的方式。虽然在 .NET BCL,我将描述在System.Threading命名空间中的那个。

关于这个定时器类,需要知道的重要事情如下:

  • The timer uses a callback method, which is called every time the timer expires. The callback method must take the form of TimerCallback delegation, which is as follows. It accepts a single parameter of type object and has a return type of void.    void TimerCallback( object state )
  • When the timer expires, the system sets the callback method on a thread in the thread pool, provides the state object as its parameter, and starts running.
  • You can set several timer characteristics, including:
    • dueTime is the amount of time before calling the callback method for the first time. If dueTime is set to the special value Timeout.Infinite, the timer will not start. If set to 0, the callback will be called immediately.
    • period is the time between each successive call of the callback method. If its value is set to Timeout.Infinite, the callback will not be called after the first time.
    • state is either null or a reference to the object passed to the callback method every time it is executed.

Timer类的构造函数将回调方法的名称、dueTimeperiodstate作为参数。Timer有好几个构造器;可能是最常用的一种形式如下:

   Timer( TimerCallback callback, object state, uint dueTime, uint period )

以下代码语句显示了一个创建Timer对象的示例:

Image

一旦一个Timer对象被创建,你可以使用Change方法改变它的dueTimeperiod

下面的代码展示了一个使用定时器的例子。Main方法创建计时器,这样它将在 2 秒钟后第一次调用回调,之后每秒调用一次。回调方法只是打印出一条消息,包括它被调用的次数。

`   using System;
using System.Threading;

namespace Timers
{
class Program
{
int TimesCalled = 0;

void Display (object state)
{
Console.WriteLine("{0} {1}",(string)state, ++TimesCalled);
}

static void Main( )
{
Program p = new Program();               First callback at
2 seconds
Timer myTimer = new Timer                                                     ↓  
(p.Display, "Processing timer event", 2000, 1000);
Console.WriteLine("Timer started.");                                                      ↑
Repeat every
Console.ReadLine();                               second.
}
}
}`

大约 5 秒钟后,此代码在终止之前会产生以下输出:


Timer started. Processing timer event 1 Processing timer event 2 Processing timer event 3 Processing timer event 4


还提供了其他几个定时器类 .NET BCL,各有各的用处。其他计时器类如下:

  • System.Windows.Forms.Timer: This class is used in Windows Forms applications, and WM_TIMER messages are regularly put into the message queue of the program. When the program gets the message from the queue, it synchronously processes the handler on the main user interface thread. This is extremely important in Windows Forms applications.
  • System.Timers.Timer: This class is more extensive and contains many members who operate timers through properties and methods. It also has a member event named Elapsed, which is raised when each cycle expires. The timer can be run on the user interface thread or worker thread.*

二十一、命名空间和程序集

  • 引用其他组件
  • 名称空间
  • 使用指令
  • 装配的结构
  • 一个程序集的身份
  • 强命名程序集
  • 程序集的私有部署
  • 共享程序集和 GAC
  • 配置文件
  • 延迟签约

引用其他组件

在第一章中,我们从高层次上看了编译过程。您看到编译器获取源代码文件并生成一个名为汇编的输出文件。这一章更详细地介绍了程序集以及它们是如何产生和部署的。它还介绍了命名空间如何帮助组织类型。

到目前为止,你看到的所有程序,大部分都声明并使用了它们自己的类。但是,在许多项目中,您会希望使用其他程序集中的类或类型。这些其他程序集可能来自 BCL 或第三方供应商,也可能是您自己创建的。这些被称为类库,它们的汇编文件名一般以.dll扩展名结尾,而不是.exe扩展名。

例如,假设您想要创建一个类库,其中包含可由其他程序集使用的类和类型。下面的例子展示了一个简单库的源代码,它包含在一个名为SuperLib.cs的文件中。该库包含一个名为SquareWidget的公共类。图 21-1 说明了 DLL 的制作。

   public class SquareWidget { public double SideLength = 0; public double Area { get { return SideLength * SideLength; } } } Image

图 21-1 。SuperLib 源代码和结果汇编

若要使用 Visual Studio 创建类库,请从已安装的 Windows 模板中选择类库模板。具体来说,在 Visual Studio 中,请执行下列操作:

  1. 选择文件Image新建Image项目,新建项目窗口将会打开。
  2. 在左窗格中,在已安装的Image模板面板上,找到 Visual C# 节点并选择它。
  3. 在中间窗格中,选择类库模板。

假设你正在编写一个名为MyWidgets的程序,你想使用SquareWidget类。该程序的代码在一个名为MyWidgets.cs的文件中,如下所示。代码简单地创建了一个类型为SquareWidget的对象,并使用了该对象的成员。

`   using System;

class WidgetsProgram
{
static void Main( )
{
SquareWidget sq = new SquareWidget();   // From class library

Not declared in this assembly
sq.SideLength = 5.0;                    // Set the side length.
Console.WriteLine(sq.Area);             // Print out the area.
}     ↑
}    Not declared in this assembly`

注意,代码没有声明类SquareWidget。相反,您使用在SuperLib中定义的类。然而,当你编译MyWidgets程序时,编译器必须意识到你的代码使用了汇编SuperLib,所以它可以获得关于类SquareWidget的信息。为此,您需要通过给出程序集的名称和位置,给编译器一个对程序集的引用*。*

在 Visual Studio 中,可以通过以下方式添加对项目的引用:

  • Select Solution Explorer and find the References folder under the project name. References The folder contains a list of assemblies used by the project.
  • Right-click the References folder and select Add Reference. There are five tabs to choose from, allowing you to find class libraries in different ways.
  • For our program, select the Browse tab, browse to the DLL file containing the SquareWidget class definition, and select it.
  • Click the OK button, and the reference will be added to the project.

添加引用后,可以编译MyWidgets。图 21-2 展示了完整的编译过程。

Image

图 21-2 。引用另一个组件

mscorlib 库

到目前为止,我在书中几乎所有的例子中都用到一个类库。它包含了Console类。在名为mscorlib.dll的文件中的名为mscorlib的程序集中定义了Console类。然而,你不会在References文件夹中找到这个程序集。Assembly mscorlib包含了 C# 类型的定义和 most 的基本类型。网络语言。在编译 C# 程序时必须总是引用它,所以 Visual Studio 不会费心在References文件夹中显示它。

当你考虑到mscorlib时,MyWidgets的编译过程看起来更像图 21-3 中的所示。在这之后,我将假设使用mscorlib程序集,而不再表示它。

Image

图 21-3 。引用类库

现在假设你的程序已经很好地使用了SquareWidget类,但是你想扩展它的功能来使用一个名为CircleWidget的类,这个类是在一个名为UltraLib的不同程序集中定义的。MyWidgets源代码现在看起来如下。它创建一个如SuperLib中定义的SquareWidget对象和一个如UltraLib中定义的CircleWidget对象。

`   class WidgetsProgram
{
static void Main( )
{
SquareWidget sq = new SquareWidget();      // From SuperLib
...

CircleWidget circle = new CircleWidget();   // From UltraLib
...
}
}`

类库UltraLib的源代码如下例所示。注意,除了类CircleWidget,像库SuperLib,它还声明了一个名为SquareWidget的类。您可以将UltraLib编译成一个 DLL,并将其添加到项目MyWidgets的引用列表中。

`   public class SquareWidget

public class CircleWidget
{
public double Radius = 0;
public double Area
{
get
}
}`

由于两个库都包含一个名为SquareWidget的类,当你试图编译程序MyWidgets时,编译器会产生一个错误消息,因为它不知道使用类SquareWidget的哪个版本。图 21-4 说明了这种冲突,也称为名称冲突

Image

图 21-4 。由于程序集 SuperLib 和 UltraLib 都包含一个名为 SquareWidget 的类的声明,编译器不知道要实例化哪一个。

命名空间

MyWidgets的例子中,由于您有源代码,您可以通过在SuperLib源代码或UltraLib源代码中修改SquareWidget类的名称来解决名称冲突。但是,如果这些库是由不同的公司开发的,而您没有源代码,该怎么办呢?假设SuperLib由一家名为 MyCorp 的公司生产,而UltraLib由 ABCCorp 公司生产。在这种情况下,如果你使用任何有冲突的类或类型,你就不能一起使用它们。

可以想象,如果您的开发机器包含由几十个甚至几百个不同公司生产的程序集,那么在类名中可能会有一定数量的重复。如果仅仅因为两个程序集碰巧有相同的类型名,就不能在同一个程序中使用它们,那就太遗憾了。

然而,假设 MyCorp 有一个策略,在所有类的前面加上一个字符串,该字符串由公司名、产品名和描述性类名组成。进一步假设 ABC 公司有同样的政策。在这种情况下,我们例子中的三个类名将被命名为MyCorpSuperLibSquareWidgetABCCorpUltraLibSquareWidgetABCCorpUltraLibCircleWidget,如图 21-5 中的所示。这些是完全有效的类名,一家公司的库中的类与另一家公司的冲突的可能性很小。

Image

***图 21-5。*有了类名前面的消歧字符串,库之间就没有冲突了。

但是,我们的示例程序需要使用这些长名称,看起来如下所示:

`   class WidgetsProgram
{
static void Main( )
{
MyCorpSuperLibSquareWidget sq
= new MyCorpSuperLibSquareWidget();      // From SuperLib
...

ABCCorpUltraLibCircleWidget circle
= new ABCCorpUltraLibCircleWidget();     // From UltraLib
...
}
}`

虽然这解决了冲突问题,但这些新的、无歧义的名称更难阅读,使用起来也很笨拙,即使使用智能感知也是如此。

然而,假设除了标识符中通常允许的字符之外,您还可以在字符串中使用句点字符—尽管仍然不能出现在类名的开头或结尾。在这种情况下,我们可以让名字更容易理解,比如MyCorp.SuperLib.SquareWidgetABCCorp.UltraLib.SquareWidgetABCCorp.UltraLib.CircleWidget。现在,代码将如下所示:

`   class WidgetsProgram
{
static void Main( )
{
MyCorp.SuperLib.SquareWidget sq
= new MyCorp.SuperLib.SquareWidget();      // From SuperLib
...

ABCCorp.UltraLib.CircleWidget circle
= new ABCCorp.UltraLib.CircleWidget();     // From UltraLib
...
}
}`

这让我们想到了名称空间和名称空间名称的概念。

  • You can think of the namespace name as a string of characters (which can contain periods in the string), attached to the front of the class or type name, separated by periods.
  • The complete string containing the namespace name, separated period and class name is called the fully qualified name of the class.
  • Namespace is a collection of classes and types that share the name of this namespace.

图 21-6 说明了这些定义。

Image

***图 21-6。*命名空间是共享相同命名空间名称的类型定义的集合。

您可以使用名称空间将一组类型组合在一起并给它们命名。通常,您希望命名空间名称描述命名空间包含的类型,并与其他命名空间名称相区别。

通过在包含类型声明的源文件中声明命名空间来创建命名空间。下面显示了声明命名空间的语法。然后在命名空间声明的花括号中声明所有的类和其他类型。这些就是名称空间的成员

      Keyword       Namespace name ↓              ↓ namespace *NamespaceName* { *TypeDeclarations* }

下面的代码展示了 MyCorp 的程序员如何创建MyCorp.SuperLib名称空间并在其中声明SquareWidget类:

      Company  name   Period ↓   ↓ namespace MyCorp.SuperLib { public class SquareWidget { public double SideLength = 0; public double Area { get { return SideLength * SideLength; } } } }

现在,当 MyCorp 公司向您发送更新的程序集时,您可以通过修改您的MyWidgets程序来使用它,如下所示:

`   class WidgetsProgram
{
static void Main( )
{        Fully qualified name                         Fully qualified name
             ↓                                     ↓              
MyCorp.SuperLib.SquareWidget sq = new MyCorp.SuperLib.SquareWidget();
↑               ↑
Namespace name     Class name

CircleWidget circle = new CircleWidget();
...`

现在您已经在代码中明确指定了SquareWidgetSuperLib版本,编译器将不再有区分类的问题。键入完全限定名有点长,但至少现在可以使用这两个库了。在本章的稍后部分,我将介绍using别名指令,它解决了必须重复输入完全限定名的不便。

如果UltraLib程序集也被生产它的公司(ABCCorp)更新了名称空间,那么编译过程将如图 21-7 所示。

Image

图 21-7 。带名称空间的类库

命名空间名称

如您所见,命名空间的名称可以包含创建该程序集的公司的名称。除了标识公司之外,这个名称还用于帮助程序员快速了解名称空间中定义的类型。

关于命名空间名称的一些要点如下:

  • The namespace name can be any valid identifier, as described in chapter 2 of . Like any identifier, this string is case sensitive.
  • In addition, namespace names can contain any number of period characters. You can use it to organize types into a hierarchy.

例如,表 21-1 给出了 .NET BCL。

Image

命名空间命名准则建议如下:

  • Namespace name starting with company name.
  • Add the technical name after the company name.
  • Do not use the same name namespace as the class or type.

例如,Acme Widget 公司的软件开发部门在以下三个名称空间中开发软件,如以下代码所示:

  • AcmeWidgets.SuperWidget
  • AcmeWidgets.Media
  • AcmeWidgets.Games

   namespace AcmeWidgets.SuperWidget { class SPDBase ... ... }

关于名称空间的更多信息

关于名称空间,您还应该知道其他一些重要的事情:

  • Each type name in the namespace must be different from all other type names.
  • Types in a namespace are called members of that namespace.
  • A source file can contain any number of namespace declarations, whether sequential or nested.

图 21-8 在左边显示了一个源文件,它顺序声明了两个名称空间,每个名称空间中有几个类型。请注意,尽管名称空间包含几个共同的类名,但它们是通过名称空间名称来区分的,如图右侧的程序集所示。

Image

图 21-8 。源文件中的多个名称空间

那个 .NET Framework BCL 提供了数千个已定义的类和类型供您在构建程序时选择。为了帮助组织这些大量的可用功能,具有相关功能的类型在同一个命名空间中声明。BCL 使用 100 多个命名空间来组织其类型。

跨文件传播的名称空间

命名空间未关闭。这意味着您可以通过再次声明向它添加更多的类型声明,无论是稍后在源文件中还是在另一个源文件中。

例如,图 21-9 显示了三个类的声明,它们都在同一个名称空间中,但是在不同的源文件中声明。源文件可以编译成一个单独的程序集,如图图 21-9 所示,也可以编译成单独的程序集,如图图 21-10 所示。

Image

图 21-9 。命名空间可以分布在源文件中,并编译为单个程序集。

Image

图 21-10 。命名空间可以分布在源文件中,并被编译成单独的程序集。

嵌套命名空间

名称空间可以嵌套,产生一个嵌套名称空间。嵌套命名空间允许您创建类型的概念性层次结构。

有两种方法可以声明嵌套命名空间:

  • Text nesting : You can create a nested namespace by placing the declaration of the nested namespace in the declaration body of the closed namespace. This is illustrated on the left side of Figure 21-11. In this example, the namespace OtherNs is nested in the namespace MyNamespace.
  • Separate declaration : You can also create a separate declaration for the nested namespace, but you must use its fully qualified name in the declaration. This is illustrated on the right side of Figure 21-11. Note that the fully qualified name MyNamespace.OtherNS is used in the declaration of nested namespace OtherNs.

Image

图 21-11 。声明嵌套命名空间的两种形式是等效的。

图 21-11 中所示的两种形式的嵌套命名空间声明产生相同的程序集,如图 21-12 中所示。该图显示了在文件SomeLib.cs中声明的两个类,以及它们的完全限定名。

Image

图 21-12 。嵌套的名称空间结构

尽管嵌套名称空间在封闭名称空间内部,但是它的成员是封闭名称空间的而不是成员。一个常见的误解是,由于嵌套命名空间位于封闭命名空间内部,因此嵌套命名空间的成员必须是封闭命名空间的子集。这不是真的;名称空间是独立的。

使用指令

完全限定名可能很长,在整个代码中使用它们会变得很麻烦。然而,有两个编译器指令可以让你避免使用完全限定名——using命名空间指令using 别名指令

关于using指令的两个要点如下:

  • They must be placed at the top of the source file, before any type declaration .
  • They apply to all namespaces in the current source file.

使用命名空间指令

您在前面几节的MyWidgets示例中看到,您可以通过使用完全限定名来指定一个类。通过将using名称空间指令放在源文件的顶部,可以避免使用长名称。

using命名空间指令指示编译器您将使用某些特定命名空间中的类型。然后,您可以继续使用简单的类名,而不必完全限定它们。

当编译器遇到不在当前名称空间中的名称时,它会检查在using名称空间指令中给出的名称空间列表,并将未知名称附加到列表中的第一个名称空间。如果生成的完全限定名与此程序集中的某个类或引用的程序集中的某个类匹配,编译器将使用该类。如果不匹配,它将尝试列表中的下一个名称空间。

using名称空间指令由关键字using组成,后跟一个名称空间标识符。

   Keyword ↓ using System; ↑ Name of namespace

我在本文中一直使用的一个方法是WriteLine方法,它是在System名称空间中的类Console的成员。我没有在整个代码中使用它的完全限定名,而是通过在代码顶部使用using名称空间指令来稍微简化我们的工作。

例如,下面的代码在第一行使用了using命名空间指令来声明代码使用了来自System命名空间的类或其他类型。

   using System;                                // using namespace directive ... System.Console.WriteLine("This is text 1");  // Use fully qualified name. Console.WriteLine("This is text 2");         // Use directive.

使用别名指令

using别名指令允许您为以下任一项分配别名:

  • A namespace.
  • A namespace.

例如,下面的代码显示了两个using别名指令的使用。第一个指令指示编译器标识符Syst是名称空间System的别名。第二个指令说标识符SC是类System.Console的别名。

  Keyword  Alias     Namespace ↓     ↓      ↓ using Syst = System; using SC   = <ins>System.Console</ins>; ↑    ↑            ↑ Keyword  Alias         Class

下面的代码使用这些别名。Main中的三行代码都调用了System.Console.WriteLine方法。

  • The first statement in Main uses the alias -System of namespace.
  • The second statement uses the fully qualified name of the method.
  • The third statement uses an alias -Console of the class.

`   using Syst = System;                         // using alias directive
using SC   = System.Console;                 // using alias directive

namespace MyNamespace
{
class SomeClass
{
static void Main()
{ Alias for namespace
 ↓ 
Syst.Console.WriteLine  ("Using the namespace alias.");
System.Console.WriteLine("Using fully qualified name.");
SC.WriteLine            ("Using the type alias");

} Alias for class
}
}`

一个装配的结构

正如你在第一章中看到的,一个汇编不包含本机代码,而是包含通用中间语言(CIL)代码。它还包含实时(JIT)编译器在运行时将 CIL 转换为本机代码所需的一切,包括对它引用的其他程序集的引用。组件的文件扩展名通常是.exe.dll

大多数程序集都由一个文件组成。图 21-13 显示了组件的四个主要部分。

  • The assembly list contains the following contents:
    • The identity of the assembly
    • List of files that make up an assembly
    • Location map in assembly
    • Information about other referenced assemblies
  • The type metadata section contains information about all types defined in the assembly. This information contains all information about each type.
  • CIL section contains all intermediate codes for assembly.
  • Resources is optional, but it can contain graphics or language resources.

Image

图 21-13 。单列程序集的结构

汇编代码文件被称为模块。尽管大多数程序集包含一个模块,但有些程序集包含更多模块。对于有多个模块的装配,一个文件是主模块,其他是次模块

  • The main module contains a list of assemblies and references to auxiliary modules.
  • The file name of the secondary module ends with the extension .netmodule.
  • The multi-file component is considered as a unit. They are deployed together and versioned together.

图 21-14 显示了带有次级模块的多文件组件。

Image

***图 21-14。*多文件汇编

一个程序集的身份

在 .NET Framework 中,程序集的文件名不像在其他操作系统和环境中那样重要。更重要的是程序集的标识

程序集的标识由四个部分组成,这四个部分应该一起唯一地标识它。这四个组成部分如下:

  • Simple name : This is just a file name without a file extension. Each assembly has a simple name. It is also called assembly name or friendly name .
  • Version number: A string of four integers separated by periods, in the format of MajorVersion.MinorVersion.Build.Revision—for example, 2.0.35.9.
  • Culture: A string of two to five characters representing a language, or a language and a country or region. For example, the cultural name of English used in America is en-US. The German used in Germany is de-DE.
  • Public key : This 128-byte string should be unique to the company that produces this component.

公钥是公钥/私钥对的一部分,公钥/私钥对是一组两个非常大的特别选择的数字,可用于创建安全的数字签名。公钥,顾名思义,是可以公开的。私钥必须由所有者保管。公钥是程序集标识的一部分。我们将在本章的后面讨论私钥的使用。

程序集名称的组成部分嵌入在程序集的清单中。图 21-15 说明了这部分的清单。

Image

图 21-15 。清单中一个组装标识的组件

图 21-16 显示了。有关程序集标识的. NET 文档和文献。

Image

图 21-16 。程序集标识的术语

强命名程序集

一个被强命名为的程序集是一个附加了唯一数字签名的程序集。强名称程序集比没有强名称的程序集安全得多,原因如下:

  • A strong name uniquely identifies an assembly. No one else can create an assembly with the same strong name, so users can be sure that the assembly comes from the declared source.
  • If the security component of CLR does not catch the modification, the content of the assembly with strong name cannot be modified.

一个弱命名的程序集是一个没有强命名的程序集。由于弱命名程序集没有数字签名,因此它本身就不安全。因为链的强度取决于其最弱的链接,所以默认情况下,具有强名称的程序集只能访问其他具有强名称的程序集。(还有一种方法允许“部分信任的呼叫者”,但我不会讨论这个话题。)

程序员不产生强名称。编译器通过获取有关程序集的信息并对其进行哈希处理以创建附加到程序集的唯一数字签名来生成它。它在哈希过程中使用的信息如下:

  • The byte sequence that makes up the assembly.
  • Simple name
  • version number
  • Cultural information
  • Public/private key pair

Image 注意围绕强名称的命名有一些多样性。我所说的“强命名”通常被称为“强命名”我所说的“弱命名”有时被称为“非强名称”或“具有简单名称的程序集”

创建强名称程序集

若要使用 Visual Studio 对程序集进行强名称命名,您必须拥有公钥/私钥对文件的副本。如果没有密钥文件,可以让 Visual Studio 为您生成一个。然后,您可以执行以下操作:

  1. 打开项目的属性。
  2. 选择签名选项卡。
  3. 选中“对程序集签名”复选框,并输入密钥文件的位置或创建一个新的密钥文件。

编译代码时,编译器会生成一个具有强名称的程序集。图 21-17 显示了编译器的输入和输出。

Image

图 21-17 。创建强名称程序集

Image 注意要创建具有强名称的程序集,您还可以使用一个名为强名称工具(sn.exe)的工具,它会在您安装 Visual Studio 时自动安装。这是一个命令行工具,允许您对程序集进行签名,还提供了许多其他选项来管理您的密钥和签名。如果您需要的选项比 Visual Studio IDE 提供的更多,这将非常有用。您可以在网上找到使用强名称工具的详细信息。

一个程序集的私有部署

在目标机器上部署程序可以像在机器上创建一个目录并将应用复制到其中一样简单。如果应用不需要其他程序集(如 dll ),或者如果所需的 dll 在同一个目录中,程序应该可以正常工作。这样部署的程序叫做私有程序集,这种部署方法叫做 xcopy 部署。

私有程序集几乎可以放在任何目录中,只要它们所依赖的所有文件都在同一个目录或子目录中,它们就是独立的。事实上,您可以在文件系统的不同部分有几个目录,每个目录都有一组相同的程序集,它们都可以在各自的位置正常工作。

关于私有程序集部署,需要了解的一些重要事项如下:

  • The directory where private assemblies are placed is called application directory .
  • Private assemblies can be strongly named or weakly named.
  • There is no need to register components in the registry.
  • To uninstall a private assembly, simply delete it from the file system.

共享程序集和 GAC

私有程序集非常有用,但有时您会希望将 DLL 放在中心位置,以便系统上的其他程序集可以共享一个副本。 .NET 有这样一个资源库,叫做全局程序集缓存 (GAC)。放入 GAC 的程序集被称为共享程序集

关于 GAC 的一些重要事实如下:

  • Only strongly named assemblies can be added to GAC.
  • Although earlier GAC only accepted files with extension .dll, now you can also add assemblies with extension .exe.
  • GAC is located in the subdirectory of the Windows system directory. Before. NET 4.0 It is located at \Windows\Assembly. Start with ... NET 4.0 It is located at \Windows\ Microsoft.NET\assembly.

将组件安装到 GAC 中

当您尝试将程序集安装到 GAC 中时,CLR 的安全组件必须首先验证程序集上的数字签名是否有效。如果没有数字签名或数字签名无效,系统不会将其安装到 GAC 中。

然而,这是一次性的检查。当一个程序集在 GAC 中之后,当它被一个正在运行的程序引用时,不需要进一步的检查。

gacutil.exe命令行实用程序允许您在 GAC 中添加和删除程序集,并列出它包含的程序集。三个最有用的标志如下:

  • /i: Insert an assembly into GAC
  • /u:从(同 granular-activatedcarbon)颗粒状活性炭
  • Uninstall an assembly in /l:

中列出程序集

在 GAC 中并行执行

将程序集部署到 GAC 后,它可以被系统中的其他程序集使用。但是,请记住,程序集的标识由完全限定名的所有四个部分组成。因此,如果一个库的版本号改变了,或者它有不同的公钥,这些差异就指定了不同的程序集。

结果是 GAC 中可能有许多不同的程序集具有相同的文件名。虽然它们有相同的文件名,但是它们是不同的程序集,并且在 GAC 中完美地共存。这使得不同的应用可以很容易地同时使用同一 DLL 的不同版本,因为它们是具有不同标识的不同程序集。这叫做并行执行

图 21-18 展示了 GAC 中四个不同的 dll,它们都有相同的文件名——??。从图中可以看出,前三个来自同一家公司,因为它们拥有相同的公钥,而第四个来自不同的来源,因为它拥有不同的公钥。这些版本的区别如下:

  • English version 1.0.0.0, from Company A.
  • English version 2.0.0, from Company A.
  • German version 1.0.0.0, from Company A.
  • English version 1.0.0, from Company B.

Image

***图 21-18。*GAC 中四个不同的并行 DLLs】

配置文件

配置文件包含有关应用的信息,供 CLR 在运行时使用。它们可以指示 CLR 使用不同版本的 DLL,或者在搜索程序引用的 DLL 时查看其他目录。

配置文件由 XML 代码组成,不包含 C# 代码。编写 XML 代码的细节超出了本文的范围,但是您应该理解配置文件的用途以及如何使用它们。使用它们的一种方式是更新应用程序集以使用 DLL 的新版本。

例如,假设您有一个引用 GAC 中的 DLL 的应用。应用清单中引用的标识必须与 GAC 中程序集的标识完全匹配。如果发布了 DLL 的新版本,它可以被添加到 GAC 中,在那里它可以与旧版本愉快地共存。

但是,应用仍然在其清单中嵌入了旧版本 DLL 的标识。除非您重新编译应用并使其引用 DLL 的新版本,否则它将继续使用旧版本。很好,如果这是你想要的。

但是,如果您不想重新编译应用,而是希望它使用新的 DLL,那么您可以创建一个配置文件,告诉 CLR 使用新版本而不是旧版本。配置文件放在应用目录中。

图 21-19 说明了运行时过程中的对象。左边的MyProgram.exe应用调用了MyLibrary.dll的版本 1.0.0.0,如虚线箭头所示。但是应用有一个配置文件,它指示 CLR 加载 2.0.0.0 版。请注意,配置文件的名称由可执行文件的全名(包括扩展名)和附加扩展名.config组成。

Image

图 21-19 。使用配置文件绑定到新版本

延迟签约

重要的是,公司要小心保护它们的官方公/私密钥对中的私密钥。否则,如果不可信的人获得它,他们可以发布伪装成公司代码的代码。为了避免这种情况,公司显然不能允许自由访问包含它们的公钥/私钥对的文件。在大公司中,程序集的最终强名称通常在开发过程的最后由有权访问密钥对的特殊小组来执行。

但是,由于几个原因,这可能会在开发和测试过程中引起问题。首先,因为公钥是程序集标识的四个组成部分之一,所以在提供公钥之前不能设置标识。此外,弱命名程序集不能部署到 GAC。开发人员和测试人员都需要能够以发布时部署的方式编译和测试代码,包括它在 GAC 中的身份和位置。

考虑到这一点,有一种分配强名称的修改形式,称为延迟签名,或部分签名,它克服了这些问题,但不释放对私钥的访问。

在延迟签名中,编译器只使用公钥/私钥对中的公钥。然后可以将公钥放在清单中,以完成程序集的标识。延迟签名还使用 0 块来为数字签名保留空间。

若要创建延迟签名程序集,必须做两件事。首先,创建一个只有公钥而没有公钥/私钥对的密钥文件副本。接下来,向源代码的汇编范围添加一个名为DelaySignAttribute的附加属性,并将其值设置为true

图 21-20 显示了产生延迟签名组件的输入和输出。请注意图中的以下内容:

  • In the input, DelaySignAttribute is located in the source file, and the key file only contains the public key.
  • In the output, there is space reserved for digital signatures at the bottom of the assembly.

Image

***图 21-20。*创建延迟签名的程序集

如果您试图将延迟签名的程序集部署到 GAC,CLR 不会允许,因为它没有强名称。要在特定的计算机上部署它,您必须首先发出一个命令行命令,在该计算机上禁用 GAC 的签名验证,只针对该程序集,并允许它安装在 GAC 中。为此,从 Visual Studio 命令提示符发出以下命令。

   sn –vr MyAssembly.dll

现在,您已经了解了弱命名程序集、延迟签名程序集和强名称程序集。图 21-21 总结了它们结构上的差异。

Image

图 21-21 。不同组装签名阶段的结构

二十二、异常

  • 什么是异常?
  • try 语句
  • 异常类
  • catch 子句
  • 使用特定 catch 子句的示例
  • 捕获条款部分
  • 最后一块
  • 寻找异常的处理程序
  • 进一步搜索
  • 抛出异常
  • 抛出无异常对象

有哪些异常?

异常是程序中违反系统或应用约束的运行时错误,或者是正常运行期间不希望发生的情况。例如,当程序试图将一个数除以零或试图写入只读文件时。当这些发生时,系统捕捉到错误,引发异常。

如果程序没有提供处理异常的代码,系统将暂停程序。例如,下面的代码在试图除以零时会引发异常:

   static void Main() { int x = 10, y = 0; x /= y;              // Attempt to divide by zero--raises an exception }

运行此代码时,系统显示以下错误消息:


Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero. at Exceptions_1.Program.Main() in C:\Progs\Exceptions\Program.cs:line 12


try 语句

try语句允许您指定代码块来防止异常,并在异常发生时提供代码来处理它们。try语句由三部分组成,如图图 22-1 所示。

  • try Block contains the code protected for exceptions.
  • The catch clause part contains one or more catch clauses. These are the code blocks that handle exceptions. They are also called exception handlers.
  • finally Block contains the code to be executed under all circumstances, regardless of whether there is an exception.

Image

图 22-1 。try 语句的结构

处理异常

前面的例子表明,试图除以零会导致异常。您可以通过将代码放在一个try块中并提供一个简单的catch子句来修改程序以处理该异常。当异常出现时,它在catch块中被捕获和处理。

`   static void Main()
{
int x = 10;

try
{
int y = 0;
x /= y;                    // Raises an exception
}
catch
{
...                     // Code to handle the exception

Console.WriteLine("Handling all exceptions - Keep on Running");
}
}`

该代码产生以下消息。请注意,除了输出消息之外,没有任何迹象表明发生了异常。


Handling all exceptions - Keep on Running


异常类

程序中可能出现许多不同类型的异常。BCL 定义了许多异常类,每个类代表一种特定的类型。发生这种情况时,CLR 会执行以下操作:

  • It creates an exception object for the type.
  • It looks for a suitable catch clause to deal with it.

所有的异常类最终都是从System.Exception类派生出来的。图 22-2 显示了异常继承层次的一部分。

Image

图 22-2 。异常层次结构

异常对象包含只读属性,其中包含导致异常的信息。表 22-1 显示了其中的一些特性。

Image

catch 子句

catch子句处理异常。有三种形式,允许不同级别的处理。图 22-3 显示了这些表格。

Image

图 22-3 。catch 子句的三种形式

general catch子句可以接受任何异常,但不能确定导致异常的异常类型。这仅允许对任何可能发生的异常进行一般的处理和清理。

特定的 catch子句形式将异常类的名称作为参数。它匹配指定类的异常或从它派生的异常类。

带有宾语形式的特定 catch 子句提供了关于异常的最多信息。它匹配指定类的异常,或从它派生的异常类。它通过将 CLR 创建的异常对象分配给异常变量*,为您提供了对该异常对象的引用。您可以在catch子句的块中访问异常变量的属性,以获得关于所引发的异常的特定信息。*

例如,下面的代码处理类型IndexOutOfRangeException的异常。当一个异常发生时,对实际异常对象的引用被传递到带有参数名e的代码中。三个WriteLine语句都从异常对象中读取一个字符串字段。

                  Exception type    Exception variable ↓             ↓ catch ( IndexOutOfRangeException  e ) {                                 Accessing the exception variables <ins>    ↓   </ins> Console.WriteLine( "Message: {0}", e.Message ); Console.WriteLine( "Source:  {0}", e.Source ); Console.WriteLine( "Stack:   {0}", e.StackTrace );

使用特定 catch 子句的例子

回到我们被零除的例子,下面的代码修改了前面的catch子句,专门处理DivideByZeroException类的异常。在前一个例子中,catch子句将处理在try块中引发的任何异常,而当前的例子将只处理那些DivideByZeroException类的异常。

   int x = 10; try { int y = 0; x /= y;                    // Raises an exception }            Exception type ↓                     catch ( DivideByZeroException ) { ... Console.WriteLine("Handling an exception."); }

您可以进一步修改catch子句来使用一个异常变量。这允许您访问catch块中的异常对象。

   int x = 10; try { int y = 0; x /= y;                    // Raises an exception }          Exception type     Exception variable ↓             ↓ catch ( DivideByZeroException  e ) {                             Accessing the exception variables <ins>   ↓   </ins> Console.WriteLine("Message: {0}", e.Message ); Console.WriteLine("Source:  {0}", e.Source ); Console.WriteLine("Stack:   {0}", e.StackTrace ); }

在我的计算机上,该代码产生以下输出。在您的计算机上,第三行和第四行中的文件路径会有所不同,并且会与您的项目和解决方案目录的位置相匹配。


Message: Attempted to divide by zero. Source:  Exceptions 1 Stack:      at Exceptions_1.Program.Main() in C:\Progs\Exceptions 1\ Exceptions 1\Program.cs:line 14


军规条款部分

一个catch子句的目的是允许你以一种优雅的方式处理一个异常。如果您的catch子句采用带参数的形式,那么系统已经将异常变量设置为对异常对象的引用,您可以通过检查来确定异常的原因。如果异常是前一个异常的结果,您可以从变量的InnerException属性中获取对前一个异常的对象的引用。

catch子句部分可以包含多个catch子句。图 22-4 显示了catch条款部分的概要。

Image

图 22-4 。try 语句的 catch 子句部分的结构

当出现异常时,系统按顺序搜索catch子句列表,执行第一个与异常对象类型匹配的catch子句。正因为如此,在安排catch条款的顺序时有两条重要的规则。它们是:

  • Specific catch clauses must be sorted from the most specific exception type, followed by the most general exception type. For example, if you declare an exception class derived from NullReferenceException, then the catch clause of your derived exception type should be listed before the catch clause of NullReferenceException.
  • If there is a general catch clause, it must be the last, after all, the specific catch clause. The general catch clause is not encouraged, because when your code should handle the error in a specific way, it will allow the program to continue to execute, thus hiding the error. It also leaves the program in an unknown state. Therefore, if possible, you should use one of the specific catch clauses.

终于封锁了

如果一个程序的控制流进入一个有finally块的try语句,那么finally块总是执行。图 22-5 显示了控制流程。

  • If there is no exception in try block, then at the end of try block, control skips any catch clause and goes to finally block.
  • If there is an exception in the try block, execute the corresponding catch clause in the catch clause section, and then execute the finally block.

Image

图 22-5 。最终块的执行

在返回到调用代码之前,finally块总是会被执行,即使try块有一个return语句或者在catch块中抛出一个异常。例如,在下面的代码中,在特定条件下执行的try块中间有一个return语句。这不允许它绕过finally语句。

   try { if (inVal < 10) { Console.Write("First Branch  - "); return; } else Console.Write("Second Branch - "); } finally { Console.WriteLine("In finally statement"); }

当变量inVal的值为5时,该代码产生以下输出:


First Branch  - In finally statement


寻找异常的处理程序

当程序引发异常时,系统会检查程序是否为它提供了处理程序。图 22-6 显示了控制流程。

  • If an exception occurs in the try block, the system will check whether there are any catch clauses that can handle the exception.
  • If a suitable catch clause is found, the following happens:
    • The catch clause is executed.
    • If there is a finally block, execute.
    • Continue execution after try statement (that is, after finally block or after the last catch clause if there is no finally block).

Image

图 22-6 。当前 try 语句中的处理程序出现异常

进一步搜索

如果异常是在不受try语句保护的代码段中引发的,或者如果try语句没有匹配的异常处理程序,系统将不得不进一步寻找匹配的处理程序。它将通过在调用栈中依次搜索,来查看是否有一个带有匹配处理程序的封闭的try块。

图 22-7 说明了搜索过程。图的左边是代码的调用结构,右边是调用栈。图中显示Method2是从Method1try块内部调用的。如果在Method2中的try块内发生异常,系统执行以下操作:

  • First, check whether Method2 has an exception handler that can handle exceptions.
    • If yes, Method2 processing, the program continues to execute.
    • If not, the system continues to call the stack down to Method1 to search for a suitable handler.
  • If Method1 has an appropriate catch clause, the system will do the following:
    • It will return to the top of the call stack-that is, Method2.
    • It executes finally block of Method2 and pops Method2 off the stack.
    • Execute the catch clause of Method1 and its finally block.
  • If Method1 does not have a suitable catch clause, the system continues to search the call stack downwards.

Image

图 22-7 。向下搜索调用栈

通用算法

图 22-8 显示了处理异常的一般算法。

Image

图 22-8 。处理异常的一般算法

向下搜索调用堆栈的示例

在下面的代码中,Main开始执行并调用方法A,方法A调用方法B。代码后和图 22-9 中的给出了过程的描述和图表。

`   class Program
{
static void Main()
{
MyClass MCls = new MyClass();
try
{ MCls.A(); }
catch (DivideByZeroException e)
{ Console.WriteLine("catch clause in Main()"); }
finally
{ Console.WriteLine("finally clause in Main()"); }
Console.WriteLine("After try statement in Main.");
Console.WriteLine("            -- Keep running.");
}
}

class MyClass
{
public void A()
{
try
{ B(); }
catch (System.NullReferenceException)
{ Console.WriteLine("catch clause in A()"); }
finally
{ Console.WriteLine("finally clause in A()"); }
}

void B()
{
int x = 10, y = 0;
try
{ x /= y; }
catch (System.IndexOutOfRangeException)
{ Console.WriteLine("catch clause in B()"); }
finally
{ Console.WriteLine("finally clause in B()"); }
}
}`

这段代码产生以下输出:


finally clause in B() finally clause in A() catch clause in Main() finally clause in Main() After try statement in Main. -- Keep running.


  1. Main Call A, T1 calls B, and DivideByZeroException exception is encountered.
  2. Check whether the catch part of the system B has a matching catch clause. Although it has one for IndexOutOfRangeException, it has no one for DivideByZeroException.
  3. Then, the system moves down the call stack, checks the catch part of A, and finds that A also has no matching catch clause.
  4. The system continues to call down the stack, checks the catch clause of Main, and finds that Main does have a DivideByZeroException catch clause.
  5. Although the matching catch clause is now located, it has not been executed. Instead, the system returns to the top of the stack, executes the T2 clause of T1, and pops T3 from the call stack.
  6. Then the system moves to A, executes its finally clause, and pops up A from the call stack.
  7. Finally, execute the matching catch clause of Main, and then execute its finally clause. Then continue to execute after the try statement of Main ends.

Image

图 22-9 。在堆栈中搜索异常处理程序

抛出异常

你可以通过使用throw语句让你的代码显式引发一个异常。throw语句的语法如下:

   throw ExceptionObject;

例如,下面的代码定义了一个名为PrintArg的方法,它接受一个string参数并将其打印出来。在try块中,它首先检查以确保参数不是null。如果是,它创建一个ArgumentNullException实例并抛出它。异常实例在catch语句中被捕获,并输出错误消息。Main调用该方法两次:一次使用null参数,另一次使用有效参数。

   class MyClass { public static void PrintArg(string arg) { try { if (arg == null)                              Supply name of null argument. {                                                         ↓ ArgumentNullException myEx = new ArgumentNullException("arg"); throw myEx; } Console.WriteLine(arg); } catch (ArgumentNullException e) { Console.WriteLine("Message:  {0}", e.Message); } } } class Program { static void Main() { string s = null; MyClass.PrintArg(s); MyClass.PrintArg("Hi there!"); } }

该代码产生以下输出:


Message:  Value cannot be null. Parameter name: arg Hi there!


投掷无异常物体

catch块中,也可以在没有异常对象的情况下使用throw语句。

  • The form throws the current exception again, and the system continues to look for additional handlers for it.
  • This form can only be used in catch statements.

例如,以下代码从第一个catch子句中重新抛出异常:

`   class MyClass
{
public static void PrintArg(string arg)
{
try
{
try
{
if (arg == null)                           Supply name of null argument.
{                                                          ↓
ArgumentNullException myEx = new ArgumentNullException("arg");
throw myEx;
}
Console.WriteLine(arg);
}
catch (ArgumentNullException e)
{
Console.WriteLine("Inner Catch:  {0}", e.Message);
throw;
}    ↑
}    Rethrow the exception, with no additional parameters.
catch
{
Console.WriteLine("Outer Catch:  Handling an Exception.");
}
}
}

class Program {
static void Main() {
string s = null;
MyClass.PrintArg(s);
}
}`

这段代码产生以下输出:


Inner Catch:  Value cannot be null. Parameter name: arg Outer Catch:  Handling an Exception.


二十三、预处理器指令

  • 什么是预处理器指令?
  • 一般规则
  • define 和#undef 指令

  • 条件编译
  • 条件编译构造
  • 诊断指令
  • 行号指令
  • 地区指令
  • pragma 警告指令

什么是预处理器指令?

源代码指定了程序的定义。预处理器指令指示编译器如何处理源代码。例如,在某些情况下,您可能希望编译器忽略部分代码,而在其他情况下,您可能希望编译该代码。预处理器指令为您提供了这些选项和其他几个选项。

在 C 和 C++中,有一个实际的预处理器阶段,在这个阶段,预处理器检查源代码,并准备一个输出文本流,供随后的编译阶段处理。在 C# 中,没有真正的预处理器。“预处理器”指令由编译器处理。然而,这个术语仍然存在。

一般规则

预处理器指令的一些最重要的语法规则如下:

  • The preprocessor instruction must be on a separate line from the C# code.
  • Unlike C# statements, preprocessor instructions do not end with semicolons.
  • Each line containing preprocessing instructions must start with the # character.
    • # There can be a space before the character.
    • There can be a space between the # character and the instruction.
  • End of line comments are allowed.
  • Delimited comments are allowed in the preprocessor command line by instead of .

以下代码说明了这些规则:

`                   No semicolon

#define PremiumVersion             // OK

Space before

#define BudgetVersion          // OK
#  define MediumVersion          // OK

Space between                                  Delimited comments are not allowed.
             ↓           
#define PremiumVersion                      /* all bells & whistles */
End-of-line comments are fine.
           ↓           
#define BudgetVersion                       // Stripped-down version`

表 23-1 列出了预处理器指令。

Image

# define 和#undef 指令

编译符号是只有两种可能状态的标识符。要么是定义的,要么是未定义的。编译符号具有以下特征:**

*> * Can be any identifier other than true or false. This includes the C# keyword and identifier declared in the C# code-both can be used.

  • It has no value. Unlike C and C++, it does not represent strings.

如表 23-1 所示:

#define

#undef

   #define PremiumVersion #define EconomyVersion ... #undef PremiumVersion

在列出任何 C# 代码之前,#define#undef指令只能用在源文件的顶部。C# 代码启动后,#define#undef指令就不能再使用了。

`   using System;                        // First line of C# code
#define PremiumVersion               // Error

namespace Eagle
{
#define PremiumVersion            // Error
...`

编译符号的范围仅限于单个源文件。重新定义一个已经定义的符号是完全可以的——当然,只要是在任何 C# 代码之前。

`   #define AValue
#define BValue

#define AValue                       // Redefinition is fine.`

有条件编译

条件编译允许您根据是否定义了特定的编译符号,将源代码的一部分标记为编译或跳过。

有四个指令用于指定条件编译:

  • #if
  • #else
  • #elif
  • #endif

一个条件是一个返回truefalse的简单表达式。

  • Conditions can be composed of a single compiled symbol or expressions of symbols and operators, as shown in Table 23-2 of . Subexpressions can be grouped by brackets.
  • The words true and false can also be used in conditional expressions.

Image

以下是条件编译条件的示例:

`        Expression
↓           
#if !DemoVersion
...
#endif             Expression

#if (LeftHanded && OemVersion) || FullVersion
...
#endif

#if true   // The following code segment will always be compiled.
...
#endif`

条件编译构造

#if#endif指令是条件编译结构的匹配分界。只要有一个#if指令,就必须有一个与之匹配的#endif

图 23-1 说明了#if#if...#else构造。

  • If the conditional calculation result in the #if construction is true, then compile the following code segment. Otherwise, it will be skipped.
  • In the #if...#else construction, if the condition evaluates to true, then code segment 1 is compiled. Otherwise, compile code segment 2 .

Image

图 23-1 。#if 和#else 结构

例如,下面的代码演示了一个简单的#if...#else构造。如果定义了符号RightHanded,则编译#if#else之间的代码。否则,编译#else#endif之间的代码。

   ... #if RightHanded // Code implementing right-handed functionality ... #else // Code implementing left-handed functionality ... #endif

图 23-2 说明了#if...#elif#if...#elif...#else构造。

#if...#elif构造中的

    • If COND1 evaluates to true, Codesection1 is compiled, and compilation continues after #endif.
    • Otherwise, if cond2 evaluates to true, codesection2 is compiled, and compilation continues after #endif.
    • This continues until one condition evaluates to true or all conditions return to false. If this is the case, no code part in the construction will be compiled, and compilation will continue after #endif.
  • The #if...#elif...#else construction works in the same way, except that if there is no condition of true, the code segment after #else will be compiled, and the compilation will continue after #endif.

Image

***图 23-2。*如果...#elif 构造(左)和#if...#elif...#else 构造(右)

下面的代码演示了#if...#elif...#else构造。根据定义的编译符号,包含程序版本描述的字符串被设置为不同的值。

`   #define DemoVersionWithoutTimeLimit
...
const int intExpireLength = 30;
string strVersionDesc     = null;
int    intExpireCount     = 0;

#if   DemoVersionWithTimeLimit
intExpireCount = intExpireLength;
strVersionDesc = "This version of Supergame Plus will expire in 30 days";

#elif DemoVersionWithoutTimeLimit
strVersionDesc = "Demo Version of Supergame Plus";

#elif OEMVersion
strVersionDesc = "Supergame Plus, distributed under license";

#else
strVersionDesc = "The original Supergame Plus!!";

#endif

Console.WriteLine( strVersionDesc );
...`

诊断指令

诊断指令产生用户定义的编译时警告和错误信息。

以下是诊断指令的语法。消息是字符串,但是请注意,与普通的 C# 字符串不同,它们不必用引号括起来。

`   #warning Message

#error Message`

当编译器到达一个诊断指令时,它写出相关的消息。编译器会列出诊断指令消息以及编译器生成的任何警告和错误消息。

例如,下面的代码显示了一个#error指令和一个#warning指令。

  • The #error instruction is in the #if structure, so it will only be generated when the conditions of the #if instruction are met.
  • The #warning instruction is to remind the programmer to come back and clean up a piece of code.

`#define RightHanded
#define LeftHanded

#if RightHanded && LeftHanded
#error Can't build for both RightHanded and LeftHanded
#endif

#warning Remember to come back and clean up this code!`

行号指令

行号指令可以做几件事,包括:

  • Change the apparent line number of warning and error messages reported by the compiler.
  • Change the apparent file name of the source file being compiled.
  • Hide a series of lines from the interactive debugger

#line指令的语法如下:

`   #line integer           // Sets line number of next line to value of integer
#line "filename"        // Sets the apparent file name
#line default           // Restores real line number and file name

#line hidden            // Hides the following code from stepping debugger
#line                   // Stops hiding from debugger`

带有整数参数的#line指令使编译器认为该值是下一行代码的行号。基于该行号,继续对后续行进行编号。

  • To change the apparent file name, use the file name in double quotation marks as the parameter. Double quotes are required.
  • To return the real line number and the real file name, use default as the parameter.
  • To hide a piece of code in the single-step debugging function of the interactive debugger, use hidden as the parameter. To stop hiding, use the command without parameters. So far, this feature is mainly used in ASP.NET and WPF to hide the code generated by the compiler.

以下代码显示了行号指令的示例:

`   #line 226
x = y + z; // Now considered by the compiler to be line 226
...

#line 330 "SourceFile.cs" // Changes the reported line number and file name
var1 = var2 + var3;
...

#line default              // Restores true line numbers and file name`

地区指令

region 指令允许您标记并有选择地命名一段代码。一个区域由一个#region指令和它下面的一个#endregion指令组成。区域的特征如下:

  • A #region instruction is placed on the previous line of the code segment you want to mark, and a # endregion instruction is placed after the last line of code in the area.
  • The #region instruction can follow an optional text string in the line after it. This string is used as the name of the region.
  • Other regions can be nested inside the region.
    • Zones can be nested at any level.
    • A #endregion instruction always matches the first mismatched #region instruction above it.

虽然编译器会忽略区域指令,但是源代码工具可以使用它们。例如,Visual Studio 允许您轻松隐藏或显示区域。

例如,下面的代码有一个名为Constructors的区域,它包含了类MyClass的两个构造函数。在 Visual Studio 中,当您不想在代码中看到该区域时,可以将其折叠成一行,然后当您需要处理它或添加另一个构造函数时,再将其展开。

`   #region Constructors
MyClass()

MyClass(string s)

#endregion`

区域可以嵌套,如图图 23-3 所示。

Image

图 23-3 。嵌套区域

# pragma 警告指令

#pragma warning指令允许您关闭和重新打开警告信息。

  • To turn off warning messages, use the disable form and separate the list of warning numbers you want to turn off with commas.
  • To reopen the warning message, use the restore table to list the warning numbers you want to reopen.

例如,下面的代码关闭两条警告消息:618 和 414。在代码的下面,它打开 618 的消息,但关闭 414 的消息。

                    Warning messages to turn off <ins>   ↓   </ins> #pragma warning disable 618, 414 ...      Messages for the listed warnings are off in this section of code. #pragma warning restore 618

如果使用不带警告编号列表的任一形式,该命令将应用于所有警告。例如,下面的代码关闭并恢复所有警告消息。

`   #pragma warning disable
...      All warning messages are turned off in this section of code.

#pragma warning restore
...      All warning messages are turned back on in this section of code.`*

二十四、反射和属性

  • 元数据和反射
  • 类型类
  • 获取类型对象
  • 什么是属性?
  • 应用属性
  • 预定义、保留的属性
  • 关于应用属性的更多信息
  • 自定义属性
  • 访问属性

元数据和反射

大多数程序都是用来处理数据的。它们读取、写入、操作和显示数据。(图形是数据的一种形式。)然而,对于某些类型的程序来说,它们操纵的数据不是数字、文本或图形,而是关于程序和程序类型的信息。

关于程序及其类的数据称为元数据,存储在程序的程序集中。* A program can view other assemblies or its own metadata at runtime. When a running program looks at its own metadata or the metadata of other programs, this is called reflection.

对象浏览器是显示元数据的程序的一个例子。它可以读取程序集并显示它们包含的类型,以及所有的特征和成员。

本章将介绍你的程序如何使用Type类反映数据,以及如何使用属性向你的类型添加元数据。

Image 注意要使用反射,必须使用System.Reflection名称空间。

铅字类

在本文中,我描述了如何声明和使用 C# 中可用的类型。这些包括预定义的类型(intlongstring等)、来自 BCL 的类型(ConsoleIEnumerable等),以及用户定义的类型(MyClassMyDel等)。每种类型都有自己的成员和特征。

BCL 声明了一个名为Type的抽象类,它被设计用来包含一个类型的特征。使用这个类的对象允许你得到关于你的程序正在使用的类型的信息。

由于Type是一个抽象类,它不能有实际的实例。相反,在运行时,CLR 创建从包含类型信息的Type ( RuntimeType)派生的类的实例。当您访问这些实例之一时,CLR 返回一个引用,不是派生类型的引用,而是基类Type的引用。不过,为了简单起见,在本章的其余部分,我将把引用所指向的对象称为类型为Type的对象,尽管从技术上讲,它是 BCL 内部的一个派生类型的对象。

关于Type需要了解的重要事项如下:

  • For each type used in the program, the CLR will create a Type object containing information about that type.
  • Each type used in the program is associated with a separate Type object.
  • No matter how many types of instances are created, only one Type object is associated with all instances.

图 24-1 显示了一个带有两个MyClass对象和一个OtherClass对象的运行程序。注意,虽然有两个MyClass实例,但是只有一个Type对象表示它。

Image

图 24-1 。CLR 为程序中使用的每个类型实例化类型的对象。

你几乎可以从类型的Type对象中获得任何你需要知道的信息。表 24-1 列出了这个类中一些更有用的成员。

Image

获取类型对象

你可以通过使用GetType方法或者使用typeof操作符得到一个Type对象。类型object包含一个名为GetType的方法,该方法返回对实例的Type对象的引用。因为每个类型最终都是从object派生的,所以您可以对任何类型的对象调用GetType方法来获取其Type对象,如下所示:

Type t = myInstance.GetType();

下面的代码显示了基类和从它派生的类的声明。方法Main为每个类创建一个实例,并将引用放在一个名为bca的数组中,以便于处理。在外层的foreach循环中,代码获取Type对象并打印出类名。然后它获取该类的字段并将它们打印出来。图 24-2 说明了内存中的对象。

`   using System;
using System.Reflection;                           // Must use this namespace
class BaseClass

class DerivedClass : BaseClass

class Program
{
static void Main( )
{
var bc = new BaseClass();
var dc = new DerivedClass();

BaseClass[] bca = new BaseClass[] { bc, dc };

foreach (var v in bca)
{
Type t = v.GetType();                     // Get the type.

Console.WriteLine("Object type : {0}", t.Name);

FieldInfo[] fi = t.GetFields();           // Get the field information.
foreach (var f in fi)
Console.WriteLine("      Field : {0}", f.Name);
Console.WriteLine();
}
}
}`

该代码产生以下输出:


`Object type : BaseClass
Field : BaseField

Object type : DerivedClass
Field : DerivedField
Field : BaseField`


Image

图 24-2 。基类和派生类对象以及它们的类型对象

你也可以使用typeof操作符来获得一个Type对象。只需提供类型名作为操作数,它就会返回一个对Type对象的引用,如下所示:

   Type t = typeof( DerivedClass ); ↑          ↑ Operator   Type you want the Type object for

下面的代码显示了一个使用typeof操作符的简单例子:

`   using System;
using System.Reflection;                           // Must use this namespace

namespace SimpleReflection
{
class BaseClass
{
public int MyFieldBase;
}

class DerivedClass : BaseClass
{
public int MyFieldDerived;
}

class Program
{
static void Main( )
{
Type tbc = typeof(DerivedClass);                    // Get the type.
Console.WriteLine("Result is {0}.", tbc.Name);

Console.WriteLine("It has the following fields:");  // Use the type.
FieldInfo[] fi = tbc.GetFields();
foreach (var f in fi)
Console.WriteLine("   {0}", f.Name);
}
}
}`

该代码产生以下输出:


Result is DerivedClass. It has the following fields: MyFieldDerived MyFieldBase


什么是属性?

一个属性是一种语言结构,允许你添加元数据到程序的集合中。它是一种特殊类型的类,用于存储关于程序结构的信息。

  • The program structure to which you apply attributes is called its target .
  • Programs designed to retrieve and use metadata, such as object browsers, are called consumers of attributes.
  • There are predefined attributes. You can also declare custom attributes.

图 24-3 给出了使用属性所涉及的组件的概述,并说明了关于它们的以下几点:

  • You apply the attribute to the program construction in the source code.
  • The compiler gets the source code, generates metadata from the attributes, and puts the metadata into the assembly.
  • The consumer program can access the metadata of attributes and the metadata of other components of the program. Note that the compiler generates and uses attributes.

Image

图 24-3 。创建和使用属性所涉及的组件

按照惯例,属性名使用 Pascal 大小写,并以后缀Attribute结尾。但是,在将属性应用于目标时,可以省略后缀。例如,对于属性SerializableAttributeMyAttributeAttribute,在将它们应用到一个构造时,您可以使用简称SerializableMyAttribute

应用属性

我将从展示如何使用已经定义的属性开始,而不是从描述如何创建属性开始。这样,你就可以知道它们是如何有用的。

属性的目的是告诉编译器发出一组关于程序构造的元数据,并将其放入程序集中。您可以通过将属性应用于构造来实现这一点。

  • Apply an attribute by placing a attribute segment before construction.
  • The attribute part consists of square brackets and contains an attribute name and sometimes a parameter list.

例如,下面的代码显示了两个类的标题。前几行代码显示了应用于类MyClass的名为Serializable的属性。注意Serializable没有参数列表。第二个类声明有一个名为MyAttribute的属性,它有一个带两个string参数的参数列表。

`   [ Serializable ]                                  // Attribute
public class MyClass
{ ...

[ MyAttribute("Simple class", "Version 3.57") ]   // Attribute with parameters
public class MyOtherClass
{ ...`

关于属性,需要了解的一些重要信息如下:

  • Most attributes only apply to constructs that follow the attribute segment.
  • A construction with an attribute is called modifying , or modifying , which has this attribute. These two terms are very common.

预定义的、保留的属性

在这一节中,我们将看看. NET 预定义和保留的几个属性。

过时的属性

在一个程序的生命周期中,它可能会经历许多不同的版本,可能会持续几年。在其生命周期的后期,您通常会编写一个新方法来取代执行类似功能的旧方法。出于多种原因,您可能希望保留所有调用旧的、现已过时的方法的旧代码,而让新代码调用新方法。

当这种情况发生时,您会希望您的团队成员,或者以后处理代码的程序员,使用新的方法而不是旧的方法。为了帮助警告他们不要使用旧方法,您可以使用Obsolete属性将旧方法标记为过时,并在编译代码时显示一条有用的警告消息。下面的代码显示了它的用法示例:

`   class Program     Apply attribute.
{                    ↓

[Obsolete("Use method SuperPrintOut")]   // Apply attribute to method.
static void PrintOut(string str)
{
Console.WriteLine(str);
}
static void Main(string[] args)
{
PrintOut("Start of Main");           // Invoke obsolete method.
}
}`

注意方法Main调用PrintOut,即使它被标记为过时。尽管如此,代码编译和运行良好,并产生以下输出:


Start of Main


不过,在编译期间,编译器会产生下列 CS0618 警告讯息,通知您正在使用过时的建构:


'AttrObs.Program.PrintOut(string)' is obsolete: 'Use method SuperPrintOut'


属性的另一个重载接受类型为bool的第二个参数。此参数指定是否将用法标记为错误,而不仅仅是警告。以下代码指定应将它标记为错误:

                                      Flag as an error. ↓ [ Obsolete("Use method SuperPrintOut", true) ]  // Apply attribute to method. static void PrintOut(string str) { ...

条件属性

Conditional属性允许您指示编译器包含或排除特定方法的所有调用。要使用Conditional属性,将它应用到方法声明中,并将编译符号作为参数。

如果定义了编译符号,编译器将包含所有方法调用的代码,就像对任何普通方法一样。* If the compilation symbol is defined by instead of , the compiler will omit all the method calls of in the whole code.

定义方法本身的 CIL 代码总是包含在程序集中。只是插入或省略了调用。

例如,在下面的代码中,Conditional属性被应用于名为TraceMessage的方法的声明。该属性只有一个参数,在本例中是字符串DoTrace

  • When compiling code, the compiler will check whether the compilation symbol named DoTrace is defined.
  • If DoTrace is defined, the compiler places all calls to method TraceMessage in the code as usual.
  • If DoTrace compilation symbol is not defined, it will not output any code for TraceMessage call.

              Compilation symbol <ins>    ↓    </ins> [Conditional( "DoTrace" )] static void TraceMessage(string str) { Console.WriteLine(str); }

条件属性的例子

下面的代码展示了使用Conditional属性的完整示例。

  • Method Main contains two calls to method TraceMessage.
  • The declaration of method TraceMessage is decorated with the attribute Conditional, whose parameter is the compilation symbol DoTrace. Therefore, if DoTrace is defined, the compiler will contain all the codes that call TraceMessage.
  • Since the first line of code defines a compilation symbol named DoTrace, the compiler will contain the code that calls TraceMessage twice.

`   #define DoTrace
using System;
using System.Diagnostics;

namespace AttributesConditional
{
class Program
{
[Conditional( "DoTrace" )]
static void TraceMessage(string str)
{ Console.WriteLine(str); }

static void Main( )
{
TraceMessage("Start of Main");
Console.WriteLine("Doing work in Main.");
TraceMessage("End of Main");
}
}
}`

该代码产生以下输出:


Start of Main Doing work in Main. End of Main


如果您注释掉第一行,使得DoTrace没有被定义,编译器将不会为对TraceMessage的两次调用插入代码。这一次,当您运行该程序时,它会产生以下输出:


Doing work in Main.


来电者信息属性

调用者信息属性允许您访问有关文件路径、行号和调用成员名称的源代码信息。

  • The three attribute names are CallerFilePath, CallerLineNumber and CallerMemberName respectively.
  • These properties can only be used with optional parameters on methods.

下面的代码声明了一个名为MyTrace的方法,该方法在其三个可选参数上使用了三个调用者信息属性。如果使用这些参数的显式值调用该方法,将使用实际参数的值。然而,在下面显示的来自Main的调用中,没有提供显式的值,所以系统提供源代码的文件路径、调用方法的行的行号以及调用方法的成员的名称。

`   using System;
using System.Runtime.CompilerServices;

public static class Program
{
public static void MyTrace( string message,
[CallerFilePath]   string fileName = "",
[CallerLineNumber] int lineNumber = 0,
[CallerMemberName] string callingMember = "" )
{
Console.WriteLine( "File:        {0}", fileName );
Console.WriteLine( "Line:        {0}", lineNumber );
Console.WriteLine( "Called From: {0}", callingMember );
Console.WriteLine( "Message:     {0}", message );
}

public static void Main()
{
MyTrace( "Simple message" );
}
}`

该代码产生以下输出:


File:        c:\TestCallerInfo\TestCallerInfo\Program.cs Line:        19 Called From: Main Message:     Simple message


DebuggerStepThrough 属性

很多时候,当你调试代码并一行一行地单步执行时,有些方法是你不想让调试器进入的;您只想让它执行方法,并单步执行方法调用后的行。DebuggerStepThrough属性指示调试器在不进入目标代码的情况下执行它。

在我自己的代码中,这是我经常发现的最有用的属性。有些方法很小,但显然是正确的,以至于在调试时不得不重复地一步一步来,这很烦人。但是,请小心使用该属性,因为您不想排除可能包含 bug 的代码。

关于DebuggerStepThrough需要了解的重要事项如下:

  • This attribute is in the System.Diagnostics namespace.
  • You can use this property on classes, structures, constructors, methods or accessors.

下面的代码展示了在访问器和方法上使用的属性。如果您在调试器中单步调试这段代码,您会发现调试器没有进入IncrementFields方法或X属性的set访问器。

`   using System;
using System.Diagnostics;         // Required for this DebuggerStepThrough

class Program
{
int _x = 1;
int X
{
get { return _x; }
[DebuggerStepThrough]       // Don’t step through the set accessor.
set
{
_x = _x * 2;
_x += value;
}
}

public int Y { get; set; }

public static void Main()  {
Program p = new Program();

p.IncrementFields();
p.X = 5;
Console.WriteLine( "X = {0}, Y = {1}", p.X, p.Y );
}
[DebuggerStepThrough]          // Don’t step through this method.
void IncrementFields()
{
X++;
Y++;
}
}`

其他预定义属性

那个 .NET Framework 预定义了许多由编译器和 CLR 理解和解释的属性。表 24-2 列出了其中的一些。该表使用短名称,不带“属性”后缀。比如CLSCompliant的全称是CLSCompliantAttribute

Image

关于应用属性的更多信息

到目前为止显示的简单属性使用了应用于方法的单个属性。本节描述其他类型的属性用法。

多重属性

您可以将多个属性应用于单个构造。

  • Multiple attributes can be listed in one of the following formats:
    • Individual attribute parts of, one by one. Usually these are stacked on top of each other in separate rows.
    • Individual attribute segments, separating attributes with commas.
  • Attributes can be listed in any order.

例如,下面的代码显示了应用多个属性的两种方式。代码的各个部分是等效的。

`   [ Serializable ]                                                 // Stacked
[ MyAttribute("Simple class", "Version 3.57") ]

[ MyAttribute("Simple class", "Version 3.57"), Serializable ]    // Comma separated
↑                             ↑
Attribute                          Attribute`

其他类型的目标

除了类之外,您还可以将属性应用于其他程序结构,例如字段和属性。下面的声明显示了一个字段的一个属性和一个方法的多个属性:

`   [MyAttribute("Holds a value", "Version 3.2")]                // On a field
public int MyField;

[Obsolete]                                                   // On a method
[MyAttribute("Prints out a message.", "Version 3.6")]
public void PrintOut()
{
...`

您还可以显式标记属性,以应用于特定的目标构造。若要使用显式目标说明符,请将目标类型放在属性部分的开头,后跟一个冒号。例如,下面的代码用一个属性来修饰方法,并且将一个属性应用于返回值

  Explicit target specifier ↓ [method: MyAttribute("Prints out a message.", "Version 3.6")] [return: MyAttribute("This value represents ...", "Version 2.3")] public long ReturnSetting() { ...

C# 语言定义了十个标准属性目标,在表 24-3 中列出。大多数目标名称都是不言自明的,但是type涵盖了类、结构、委托、枚举和接口。typevar目标名指定了使用泛型的构造的类型参数。

Image

全局属性

您还可以通过使用assemblymodule目标名称,使用显式目标说明符在程序集和模块级别设置属性。(组件和模块在第二十一章中进行了解释。)关于程序集级属性的一些要点如下:

  • Assembly-level attributes must be placed in outside any namespace scope, usually in AssemblyInfo.cs file.
  • AssembyInfo.cs Files usually contain metadata about companies, products and copyright information.

以下是来自一个AssemblyInfo.cs文件的行:

   [assembly: AssemblyTitle("SuperWidget")] [assembly: AssemblyDescription("Implements the SuperWidget product.")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("McArthur Widgets, Inc.")] [assembly: AssemblyProduct("Super Widget Deluxe")] [assembly: AssemblyCopyright("Copyright © McArthur Widgets 2012")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")]

自定义属性

您可能已经注意到,应用属性的语法与您到目前为止看到的语法非常不同。由此,您可能会认为属性是一种完全不同类型的构造。他们不是——他们只是一种特殊的阶级。

关于属性类的一些要点如下:

  • The user-defined attribute class is called custom attribute .
  • All the attribute classes of are derived from the System.Attribute class.

声明自定义属性

声明一个属性类在很大程度上与声明任何其他类是一样的。但是,有几件事需要注意:

  • To declare a custom attribute, do the following:
    • Declare a class derived from System.Attribute.
    • Give it a name ending with the suffix Attribute.
  • For security reasons, it is generally recommended that you declare the attribute class as sealed.

例如,下面的代码显示了属性MyAttributeAttribute声明的开始:

                           Attribute name <ins>        ↓          </ins> public sealed class MyAttribute<ins>Attribute</ins> : <ins>System.Attribute</ins> {                                 ↑              ↑ ...                           Suffix          Base class

由于属性包含有关目标的信息,因此属性类的公共成员通常只包含以下内容:

  • field
  • attribute
  • Constructor

使用属性构造函数

像其他类一样,属性也有构造函数。每个属性必须至少有一个公共构造函数。

  • Like other classes, if you do not declare a constructor, the compiler will generate an implicit, public and parameterless constructor for you.
  • Property constructors, like other constructors, can be overloaded.
  • When declaring a constructor, you must use the full class name, including the suffix. When applies attribute, you can only use abbreviation.

例如,使用下面的构造函数,如果方法名不包含后缀,编译器将产生一条错误消息:

                        Suffix <ins>    ↓   </ins> public MyAttributeAttribute(string desc, string ver) { Description   = desc; VersionNumber = ver; }

指定构造函数

当您将属性应用于目标时,您正在指定应该使用哪个构造函数来创建属性的实例。属性应用中列出的参数是构造函数的实际参数。

例如,在下面的代码中,MyAttribute被应用于一个字段和一个方法。对于该字段,声明指定了一个带有单个string参数的构造函数。对于该方法,它指定了一个带有两个string参数的构造函数。

`   [MyAttribute("Holds a value")]                 // Constructor with one string
public int MyField;

[MyAttribute("Version 1.3", "Sal Martin")]     // Constructor with two strings
public void MyMethod()
{ ...`

关于属性构造函数的其他要点如下:

  • When applying the attribute, the actual parameter of the constructor must be a constant expression, and its value can be determined at compile time.
  • If you apply the property constructor without parameters, you can omit the brackets. For example, both classes in the following code use the parameterless constructor of the property MyAttr. The meaning of these two forms is the same.

`   [MyAttr]
class SomeClass ...

[MyAttr()]
class OtherClass ...`

使用构造函数

请注意,您从未显式调用构造函数。相反,只有当属性消费者访问属性时,才会创建属性的实例,并调用其构造函数*。这与其他类实例非常不同,其他类实例是在使用对象创建表达式的位置创建的。应用属性是一个声明性语句,它并不决定何时应该构造属性类的对象。*

图 24-4 比较了普通类的构造函数的使用和带属性的构造函数的使用。

命令式语句实际上是说,“在这里创建一个新的类对象。”* Declarative statement says, "This attribute is associated with this target, and if you need to construct an attribute, use this constructor."

Image

图 24-4 。比较构造函数的使用

构造函数中的位置和命名参数

像常规类的方法和构造函数一样,属性构造函数也可以使用位置参数和命名参数。

下面的代码使用一个位置参数和两个命名参数演示了属性的应用:

Image

下面的代码展示了属性类的声明,以及它在类MyClass上的应用。注意,构造函数声明只列出了一个形参。然而,通过使用命名参数,您可以给构造函数三个实际参数。这两个命名参数设置字段VerReviewer的值。

`   public sealed class MyAttributeAttribute : System.Attribute
{
public string Description;
public string Ver;
public string Reviewer;

public MyAttributeAttribute(string desc)  // Single formal parameter

}                               Three actual parameters
                             ↓                             
[MyAttribute("An excellent class", Reviewer="Amy McArthur", Ver="7.15.33")]
class MyClass
`

Image 注意和方法一样,如果构造函数需要任何位置参数,它们必须放在任何命名参数之前。

限制一个属性的使用

您已经看到了可以将属性应用于类。但是属性本身是类,并且有一个重要的预定义属性可以应用于您的定制属性:AttributeUsage属性。您可以使用它将属性的使用限制到一组特定的目标类型。

例如,如果您希望您的自定义属性MyAttribute只应用于方法,您可以使用下面形式的AttributeUsage:

                        Only to methods <ins>         ↓           </ins> [ AttributeUsage( AttributeTarget.Method ) ] public sealed class MyAttributeAttribute : System.Attribute { ...

AttributeUsage有三个重要的公共属性,在表 24-4 中列出。该表显示了属性的名称及其含义。对于后两个属性,它还显示了它们的默认值。

Image

【AttributeUsage 的构造函数

AttributeUsage的构造函数接受一个单一的位置参数,该参数指定属性允许哪些目标类型。它使用这个参数来设置它的ValidOn属性。可接受的目标类型是AttributeTarget枚举的成员。表 24-5 显示了AttributeTarget枚举成员的完整集合。

您可以使用按位 OR 运算符组合使用类型。例如,下面代码中声明的属性只能应用于方法和构造函数。

                                              Targets <ins>                          ↓                          </ins> [ AttributeUsage( AttributeTarget.Method | AttributeTarget.Constructor ) ] public sealed class MyAttributeAttribute : System.Attribute { ...

Image

当您将AttributeUsage应用到一个属性声明时,构造函数将至少有一个必需的参数,它包含要存储在ValidOn中的目标类型。您还可以通过使用命名参数来设置InheritedAllowMultiple属性。如果你不设置它们,它们将有它们的默认值,如表 24-4 所示。

例如,下一个代码块指定了关于MyAttribute的以下内容:

  • MyAttribute must only be applied to classes.
  • MyAttribute is not inherited by a class derived from the class to which it is applied.
  • There cannot be multiple instances of MyAttribute applied to the same target.

   [ AttributeUsage( AttributeTarget.Class,          // Required, positional Inherited = false,              // Optional, named AllowMultiple = false ) ]       // Optional, named public sealed class MyAttributeAttribute : System.Attribute { ...

建议自定义属性的做法

在编写自定义属性时,强烈建议采用以下做法:

  • The attribute class should represent a certain state of the target construct.
  • If the property requires certain fields, it contains a constructor with position parameters to collect data and initialize optional fields with named parameters as needed.
  • Do not implement public methods or other function members other than properties.
  • To increase security, declare the attribute class as sealed.
  • Use the AttributeUsage attribute in the attribute declaration to explicitly specify the attribute target set.

以下代码阐释了这些准则:

`   [AttributeUsage( AttributeTargets.Class )]
public sealed class  ReviewCommentAttribute : System.Attribute
{
public string Description   { get; set; }
public string VersionNumber { get; set; }
public string ReviewerID    { get; set; }

public ReviewCommentAttribute(string desc, string ver)

}`

访问属性

在本章的开始,您看到了您可以使用类型的Type对象来访问关于类型的信息。您可以用同样的方式访问自定义属性。这里有两种Type方法特别有用:IsDefinedGetCustomAttributes

使用 IsDefined 方法

您可以使用Type对象的IsDefined方法来确定特定的属性是否应用于特定的类。

例如,下面的代码声明了一个名为MyClass的属性化类,并通过访问在程序本身中声明和应用的属性来充当自己的属性消费者。在代码的顶部是属性ReviewComment和应用它的类MyClass的声明。该代码执行以下操作:

  • First, Main Create an object of a class. Then, it retrieves the reference to the Type object by using the GetType method inherited from its base class object.
  • By referencing the Type object, you can call the IsDefined method to see if the attribute ReviewComment is applied to this class.
    • The first parameter accepts a Type object of the attribute you are checking.
    • The type of the second parameter is bool, which specifies whether to search the inheritance tree of MyClass to find the attribute.

`   [AttributeUsage(AttributeTargets.Class)]
public sealed class ReviewCommentAttribute : System.Attribute

[ReviewComment("Check it out", "2.4")]
class MyClass

class Program
{
static void Main()
{
MyClass mc = new MyClass(); // Create an instance of the class.
Type t = mc.GetType();      // Get the Type object from the instance.
bool isDefined =            // Check the Type for the attribute.
t.IsDefined(typeof(ReviewCommentAttribute), false);

if( isDefined )
Console.WriteLine("ReviewComment is applied to type {0}", t.Name);
}
}`

该代码产生以下输出:


ReviewComment is applied to type MyClass


使用 GetCustomAttributes 方法

GetCustomAttributes方法返回一个应用于构造的属性数组。

  • The returned actual object is an array of object, which must be converted to the correct attribute type.
  • Boolean parameter specifies whether to search the inheritance tree to find the attribute. object[] AttArr = t.GetCustomAttributes(false);
  • When the GetCustomAttributes method is called, an instance of each attribute associated with the target is created.

下面的代码使用与前面的示例相同的属性和类声明。但是在这种情况下,它不仅仅决定一个属性是否被应用到类中。相反,它检索应用于该类的属性数组,并循环遍历它们,打印出它们的成员值。

`   using System;

[AttributeUsage( AttributeTargets.Class )]
public sealed class MyAttributeAttribute : System.Attribute
{
public string Description   { get; set; }
public string VersionNumber { get; set; }
public string ReviewerID    { get; set; }

public MyAttributeAttribute( string desc, string ver )

}

[MyAttribute( "Check it out", "2.4" )]
class MyClass

class Program
{
static void Main()
{
Type t = typeof( MyClass );
object[] AttArr = t.GetCustomAttributes( false );

foreach ( Attribute a in AttArr )
{
MyAttributeAttribute attr = a as MyAttributeAttribute;
if ( null != attr )
{
Console.WriteLine( "Description    : {0}", attr.Description );
Console.WriteLine( "Version Number : {0}", attr.VersionNumber );
Console.WriteLine( "Reviewer ID    : {0}", attr.ReviewerID );
}
}
}
}`

该代码产生以下输出:


Description    : Check it out Version Number : 2.4 Reviewer ID    :


二十五、其他主题

  • 概述
  • 琴弦
  • StringBuilder 类
  • 将字符串解析为数据值
  • 关于可空类型的更多信息
  • 方法主
  • 文档注释
  • 嵌套类型
  • 析构函数和处置模式
  • 与 COM 互操作

概述

在这一章中,我将讨论一些在使用 C# 时很重要但不适合其他章节的主题。这些包括字符串处理、可空类型、Main方法、文档注释和嵌套类型。

字符串

0 和 1 对于内部计算来说很好,但是对于人类可读的输入和输出,我们需要字符串。BCL 提供了许多使字符串处理变得容易的类。

C# 预定义类型string表示 .NET 类System.String。关于字符串,需要了解的最重要的事情如下:

  • String is an array of Unicode characters.
  • Strings are immutable-they cannot be changed.

string类型有许多有用的字符串操作成员,包括那些允许您确定它们的长度、改变它们的大小写、连接字符串以及执行许多其他有用任务的成员。表 25-1 显示了一些最有用的成员。

Image

表 25-1 中许多方法的名字听起来好像它们在改变字符串对象。实际上,他们不是改变字符串,而是返回新的副本。对于一个string,任何“改变”都会分配一个新的不可变字符串。

例如,下面的代码声明并初始化一个名为sstring。第一个WriteLine语句调用s上的ToUpper方法,返回一个全大写的字符串副本。最后一行打印出s的值,显示它没有改变。

`   string s = "Hi there.";

Console.WriteLine("{0}", s.ToUpper());         // Print uppercase copy
Console.WriteLine("{0}", s);                   // String is unchanged`

该代码产生以下输出:


HI THERE. Hi there.


在我自己的编码中,我发现表中列出的非常有用的方法之一是Split方法。它将一个string分割成一组子字符串,并以数组的形式返回它们。您向该方法传递一个分隔符数组,这些分隔符用于确定在何处拆分字符串,并且您可以指定它应该如何处理输出数组中的空元素。最初的string当然保持不变。

下面的代码展示了一个使用Split方法的例子。在本例中,分隔符集由空格字符和四个标点符号组成。

   class Program { static void Main() { string s1 = "hi there! this, is: a string."; char[] delimiters = { ' ', '!', ',', ':', '.' }; string[] words = s1.Split( delimiters, StringSplitOptions.RemoveEmptyEntries ); Console.WriteLine( "Word Count: {0}\n\rThe Words...", words.Length ); foreach ( string s in words ) Console.WriteLine( "   {0}", s ); } }

该代码产生以下输出:


Word Count: 6 The Words... hi there this is a string


StringBuilder 类

StringBuilder类帮助你动态有效地产生字符串,同时减少复制的数量。

  • The StringBuilder class is a member of BCL, in the namespace System.Text.
  • The StringBuilder object is a variable array of Unicode characters.

例如,下面的代码声明并初始化一个StringBuilder对象,并打印其结果字符串值。第四行通过替换内部字符数组的一部分来改变实际的对象。现在,当您通过隐式调用ToString打印其字符串值时,您可以看到,与string类型的对象不同,StringBuilder对象实际上发生了变化。

`   using System;
using System.Text;

class Program
{
static void Main()
{
StringBuilder sb = new StringBuilder( "Hi there." );
Console.WriteLine( "{0}", sb.ToString() );              // Print string.

sb.Replace( "Hi", "Hello" );                            // Replace a substring.
Console.WriteLine( "{0}", sb.ToString() );              // Print changed string.
}
}`

该代码产生以下输出:


Hi there. Hello there.


当基于给定的字符串创建一个StringBuilder对象时,该类分配一个比实际的当前字符串长度更长的缓冲区。只要对字符串所做的更改适合缓冲区,就不会分配新的内存。如果对字符串的更改需要比缓冲区中可用空间更多的空间,则会分配一个新的更大的缓冲区,并将字符复制到该缓冲区中。和原来的缓冲区一样,这个新的缓冲区也有额外的空间。

要获得与StringBuilder内容对应的string,只需调用它的ToString方法。

解析字符串为数据值

字符串是 Unicode 字符的数组。例如,字符串"25.873"有六个字符长,而不是是一个数字。虽然它看起来像一个数字,但你不能对它执行算术运算。“相加”两个字符串产生它们的连接。

  • Parsing allows you to get a string whose represents a value of and convert it into an actual typed value.
  • All predefined simple types have a static method named Parse, which takes a string representing a value and converts it into the actual value of the type.
  • If the string cannot be parsed, the system throws an exception.

以下语句显示了使用Parse方法的语法示例。注意,Parsestatic,所以您需要通过使用目标类型的名称来调用它。

   double d1 = double.Parse("<ins>25.873</ins>"); ↑             ↑ Target type         String to be converted

以下代码显示了将两个字符串解析为类型为double的值,然后将它们相加的示例:

`   static void Main()
{
string s1 = "25.873";
string s2 = "36.240";

double d1 = double.Parse(s1);
double d2 = double.Parse(s2);

double total = d1 + d2;
Console.WriteLine("Total:  {0}", total);
}`

该代码产生以下输出:


Total:  62.113


Image 注意关于Parse的一个常见误解是,由于它对字符串进行操作,所以它被认为是string类的成员。不是的。Parse根本不是单一的方法,而是由目标类型实现的许多方法。

Parse方法的缺点是,如果它们不能成功地将字符串解析为目标类型,就会抛出异常。异常是开销很大的操作,如果可能的话,您应该尝试以编程方式避免它们。TryParse方法允许您这样做。关于TryParse需要知道的重要事情如下:

  • Every built-in type with a Parse method also has a TryParse method.
  • The TryParse method takes two parameters and returns a bool.
    • The first parameter is the string you want to parse.
    • The second is the out parameter that refers to the target type variable.
    • If TryParse is successful, the parsed value is assigned to out parameter, and true is returned. Otherwise, return to false.

一般来说,你应该使用TryParse而不是Parse来避免可能抛出的异常。下面的代码展示了使用int.TryParse方法的两个例子:

`   class Program
{
static void Main( )
{
string parseResultSummary;
string stringFirst = "28";
int intFirst;                 Input string       Output variable
↓              ↓       
bool success = int.TryParse( stringFirst, out intFirst );

parseResultSummary = success
? "was successfully parsed"
: "was not successfully parsed";
Console.WriteLine( "String {0} {1}", stringFirst, parseResultSummary );

string stringSecond = "vt750";
int intSecond;           Input string        Output variable
↓               ↓        
success = int.TryParse( stringSecond, out intSecond );

parseResultSummary = success
? "was successfully parsed"
: "was not successfully parsed";
Console.WriteLine( "String {0} {1}", stringSecond, parseResultSummary );
}
}`

该代码产生以下输出:


String 28 was successfully parsed String vt750 was not successfully parsed


关于可空类型的更多信息

在第三章中,你可以快速了解可空类型。正如您所记得的,可空类型允许您创建一个可以标记为有效或无效的值类型变量,实际上允许您将值类型变量设置为“null”我想在《??》第三章中介绍可空类型和其他内置类型,但是既然你对 C# 有了更多的了解,现在是时候介绍它们更复杂的方面了。

回顾一下,可空类型总是基于另一个已经声明的类型,称为底层类型。

  • Nullable types can be created from any value type, including predefined simple types.
  • Nullable types cannot be created from reference types or other nullable types. You didn't explicitly declare a nullable type in your code. Instead, you declare a nullable variable. The compiler implicitly creates nullable types for you.

要创建一个可空类型的变量,只需在变量声明中在基础类型名称的末尾添加一个问号。不幸的是,这种语法让你看起来对你的代码有很多疑问。(开个玩笑——不过有点丑。)

例如,下面的代码声明了一个可空的int类型的变量。注意后缀是附加在类型名称上的——而不是变量名。

      Suffix ↓ <ins>int?</ins> myNInt = 28; ↑    The name of the nullable type includes the suffix.

有了这个声明语句,编译器负责生成可空类型和该类型的变量。图 25-1 显示了这种可空类型的结构。它包含以下内容:

  • Bottom type
  • Several important read-only properties of an instance of:
    • The attribute HasValue belongs to the type bool and indicates whether the value is valid or not.
    • The attribute Value is the same as the underlying type. If the variable is valid, it returns the value of the variable.

Image

***图 25-1。*一个可空类型在一个结构中包含一个底层类型的对象,有两个只读属性。

使用可空类型和使用任何其他类型的变量几乎是一样的。读取可空类型的变量会返回其值。但是,您必须确保变量不是null。试图读取null变量的值会产生异常。

  • Like any variable, to retrieve its value, you only need to use its name.
  • To check whether the nullable type has a value, you can compare it with null or check its HasValue attribute.

   int? myInt1 = 15; Compare to null. ↓ if ( myInt1 != null ) Console.WriteLine("{0}", myInt1); ↑ Use variable name.


15


您可以轻松地在可空类型和其对应的不可空类型之间进行转换。关于可空类型转换,您需要知道的重要事情如下:

在不可空类型和它的可空版本之间有一个隐含的 t 转换。也就是说,不需要任何造型。* There is a explicit transformation between the nullable type and its non-nullable version.

例如,下面几行显示了两个方向的转换。在第一行中,类型为int的文字被隐式转换为类型为int?的值,并用于初始化可空类型的变量。在第二行中,变量被显式转换为不可为空的版本。

   int? myInt1 = 15;                       // Implicitly convert int to int? int  regInt = (int) myInt1;             // Explicitly convert int? to int

赋给可空类型

可以为可空类型的变量分配三种值:

  • A value of the underlying type.
  • A value of the same nullable type.
  • A value null

以下代码显示了三种类型赋值的示例:

`   int? myI1, myI2, myI3;

myI1 = 28;                                   // Value of underlying type
myI2 = myI1;                                 // Value of nullable type
myI3 = null;                                 // null

Console.WriteLine("myI1: {0}, myI2: {1}", myI1, myI2);`

该代码产生以下输出:


myI1: 28, myI2: 28


零合并算子

标准的算术和比较运算符也处理可空类型。还有一个特殊的操作符叫做零合并操作符,它返回一个非空值给一个表达式,以防一个可空的类型变量是null

null 合并运算符由两个连续的问号组成,有两个操作数。

  • The first operand is a variable of nullable type.
  • The second is the non-nullable value of the underlying type.
  • If the calculation result of the first operand (nullable operand) is null at runtime, the nonnullable operand is returned as the result of the expression.

`                               Null coalescing operator
int? myI4 = null;                  ↓
Console.WriteLine("myI4: {0}", myI4 ?? -1);

myI4 = 10;
Console.WriteLine("myI4: {0}", myI4 ?? -1);`

该代码产生以下输出:


myI4: -1 myI4: 10


当比较同一可空类型的两个值并且都是null时,相等比较运算符(==!=)认为它们相等。例如,在下面的代码中,两个可空的int被设置为null。相等比较运算符声明它们相等。

`   int? i1 = null, i2 = null;                       // Both are null.

if (i1 == i2)                                    // Operator returns true.
Console.WriteLine("Equal");`

该代码产生以下输出:


Equal


使用可空的用户自定义类型

到目前为止,您已经看到了预定义的简单类型的可空形式。您还可以创建用户定义值类型的可空形式。这些带来了使用简单类型时不会出现的额外问题。

主要问题是对封装的基础类型成员的访问。可空类型不直接公开基础类型的任何成员。例如,看看下面的代码及其在图 25-2 中的表示。代码声明了一个名为MyStructstruct(这是一个值类型),带有两个public字段。

  • Because the fields of struct are public, they can be easily accessed in any instance of this structure, as shown in the left figure.
  • However, the nullable version of struct only exposes the underlying type through Value attribute, while does not directly disclose any of its members . Although members are common to struct, they are not common to nullable types, as shown on the right side of the figure.

`   struct MyStruct                                        // Declare a struct.
{
public int X;                                       // Field
public int Y;                                       // Field
public MyStruct(int xVal, int yVal)                 // Constructor

}

class Program {
static void Main()
{
MyStruct? mSNull = new MyStruct(5, 10);
...` Image

图 25-2 。结构成员的可访问性不同于可空类型的可访问性。

例如,下面的代码使用了这个struct并创建了struct和相应的可空类型的变量。在第三和第四行代码中,直接读取了struct变量的值。在第五行和第六行中,它们必须从 nullable 的Value属性返回的值中读取。

`   MyStruct  mSStruct = new MyStruct(6, 11);      // Variable of struct
MyStruct? mSNull   = new MyStruct(5, 10);      // Variable of nullable type
Struct access
    ↓    
Console.WriteLine("mSStruct.X: {0}", mSStruct.X);
Console.WriteLine("mSStruct.Y: {0}", mSStruct.Y);

Console.WriteLine("mSNull.X: {0}",   mSNull.Value.X);
Console.WriteLine("mSNull.Y: {0}",   mSNull.Value.Y);

Nullable type access`

可空

可空类型是通过使用名为System.Nullable<T>的. NET 类型实现的,它使用 C# 泛型特性。C# 可空类型的问号语法只是创建类型为Nullable<T>的变量的快捷语法,其中T是底层类型。Nullable<T>获取底层类型,将其嵌入到一个结构中,并为该结构提供可空类型的属性、方法和构造函数。

您可以使用Nullable<T>的泛型语法或 C# 快捷语法。快捷语法更容易编写和理解,并且不容易出错。以下代码使用前面示例中声明的带有 struct MyStructNullable<T>语法,创建一个名为mSNullNullable<MyStruct>类型的变量:

   Nullable<MyStruct> mSNull = new Nullable<MyStruct>();

以下代码使用问号语法,但在语义上等效于Nullable<T>语法:

   MyStruct? mSNull = new MyStruct();

法主

每个 C# 程序必须有一个入口点——一个必须被称为Main的方法。

在本文的示例代码中,我使用了一个没有参数也不返回值的版本Main。然而,有四种形式的Main可以作为程序的入口点。这些形式如下:

  • static void Main()               {...}
  • static void Main( string[] args) {...}
  • static int  Main()               {...}
  • static int  Main( string[] args) {...}

当程序终止时,前两种形式不向执行环境返回值。后两种形式返回一个int值。返回值(如果使用的话)通常用于报告程序的成功或失败,其中 0 通常用于指示成功。

第二种和第四种形式允许您在程序启动时从命令行向程序传递实际参数,也称为参数。命令行参数的一些重要特征如下:

  • There can be zero or more command line arguments. Even if there is no parameter, the args parameter is not null. Instead, it is an array without elements.
  • Independent variables are separated by spaces or tabs.
  • Each parameter is interpreted by the program as a string, but you don't need to enclose it in quotation marks on the command line.

例如,以下名为CommandLineArgs的程序接受命令行参数,并打印出提供的每个参数:

   class Program { static void Main(string[] args) { foreach (string s in args) Console.WriteLine(s); } }

您可以从 Windows 的命令提示符程序中执行此程序。以下命令行使用五个参数执行程序CommandLineArgs:

   <ins>CommandLineArgs</ins> <ins>Jon Peter Beth Julia Tammi</ins> ↑                 ↑ Executable           Arguments Name

这会产生以下输出:


Jon Peter Beth Julia Tammi


关于Main需要了解的其他重要事项如下:

  • Main must always be declared as static.
  • Main can be declared either in a class or in a structure.

一个程序只能包含四个可接受的入口点形式Main的一个声明。然而,您可以合法地声明其他名为Main的方法,只要它们没有四种入口点形式中的任何一种——但是这样做会引起混乱。

主通道的可达性

Main可以声明为publicprivate:

  • If Main is declared as private, other assemblies cannot be accessed, and only the operating system can start the program.
  • If Main is declared as public, other assemblies can be executed.

然而,操作系统总是可以访问Main,而不管它声明的访问级别或者声明它的classstruct的访问级别。

默认情况下,当 Visual Studio 创建一个项目时,它会创建一个程序大纲,其中Main是隐式的private。如果你需要的话,你可以随时添加public修饰语。

文档注释

文档注释特性允许您以 XML 元素的形式包含程序的文档。我在第十九章中介绍了 XML。Visual Studio 甚至会帮助您插入元素,并从源文件中读取它们,然后将它们复制到一个单独的 XML 文件中。

图 25-3 给出了使用 XML 注释的概述。这包括以下步骤:

  • You can use Visual Studio to generate source files with embedded XML. Visual Studio can automatically insert most important XML elements.
  • Visual Studio reads XML from the source code file and copies the XML code into a new file.
  • Another program called document compiler can get XML files and generate various types of document files from it.

Image

图 25-3 。XML 注释过程

Visual Studio 的早期版本包含一个基本的文档编译器,但在 Visual Studio 2005 发布之前它已被移除。微软已经开发了一个新的文档编译器,叫做 Sandcastle .NET 框架文档。可以从[sandcastle.codeplex.com](http://sandcastle.codeplex.com)开始了解更多,免费下载。

插入文档注释

文档注释以三个连续的正斜杠开始。

  • The first two slashes indicate to the compiler that this is an end-of-line comment and should be ignored in program parsing.
  • The third diagonal line indicates that this is a document comment.

例如,在下面的代码中,前四行显示了关于类声明的文档注释。他们使用了<summary> XML 标签。在字段声明的上方是三行记录字段的代码—同样使用了<summary>标签。

   /// <summary>       ←  Open XML tag for the class. /// This is class MyClass, which does the following wonderful things, using /// the following algorithm. ... Besides those, it does these additional /// amazing things. /// </summary>      ←  Close XML tag. class MyClass                                    // Class declaration { /// <summary>    ←  Open XML tag for the field. /// Field1 is used to hold the value of ... /// </summary>   ←  Close XML tag. public int Field1 = 10;                       // Field declaration ...

当您在语言功能(如类或类成员)的声明上方键入三个斜杠时,Visual Studio 会自动插入每个 XML 元素。

例如,以下代码显示了类MyClass声明上方的两条斜线:

   // class MyClass { ...

只要添加了第三个斜杠,Visual Studio 就会立即将注释扩展为下面的代码,而无需您做任何事情。然后,您可以在标记之间的文档注释行中键入任何内容。

   /// <summary>        Automatically inserted ///                  Automatically inserted /// </summary>       Automatically inserted class MyClass { ...

使用其他 XML 标签

在前面的例子中,您看到了summary XML 标签的使用。C# 还可以识别许多其他的标签。表 25-2 列出了一些最重要的。

Image

嵌套类型

类型通常直接在命名空间中声明。但是,您也可以在classstruct声明中声明类型。

  • The type declared in another type declaration is called nested type . Like all type declarations, nested types are templates for type instances.

  • The nested type is declared as a member of *surrounding type .

    • The nesting type can be any type.
    • The closure type can be class or struct.*

例如,下面的代码显示了类MyClass,以及一个名为MyCounter的嵌套类。

   class MyClass                       // Enclosing class { class MyCounter                  // Nested class { ... } ... }

如果将一个类型声明为嵌套类型只是为了用作封闭类型的助手,那么这样做通常是有意义的。

不要被术语嵌套所迷惑。嵌套指的是声明的位置——而不是任何实例的内存位置。尽管嵌套类型的声明位于封闭类型的声明内部,但嵌套类型的对象不一定包含在封闭类型的对象中。嵌套类型的对象——如果创建了的话——位于内存中的任何位置,如果它们没有在另一个类型中声明的话。

例如,图 25-4 显示了MyClassMyCounter类型的对象,如前面的代码所示。图中还显示了一个名为Counter的字段,在类MyClass中,它是对嵌套类的一个对象的引用,该对象位于堆中的其他地方。

Image

图 25-4 。嵌套指的是声明的位置,而不是对象在内存中的位置。

嵌套类的例子

下面的代码将类MyClassMyCounter充实成一个完整的程序。MyCounter实现一个整数计数器,从 0 开始,可以使用++操作符递增。当调用MyClass的构造函数时,它会创建嵌套类的一个实例,并将引用分配给该字段。图 25-5 说明了代码中对象的结构。

`   class MyClass
{
class MyCounter                                        // Nested class
{
public int Count { get; private set; }

public static MyCounter operator ( MyCounter current )
{
current.Count
;
return current;
}
}

private MyCounter counter;                             // Field of nested class type

public MyClass() { counter = new MyCounter(); }        // Constructor

public int Incr() { return ( counter++ ).Count; }      // Increment method.
public int GetValue() { return counter.Count; }        // Get counter value.
}

class Program
{
static void Main()
{
MyClass mc = new MyClass();                         // Create object.

mc.Incr(); mc.Incr();  mc.Incr();                   // Increment it.
mc.Incr(); mc.Incr();  mc.Incr();                   // Increment it.

Console.WriteLine( "Total:  {0}", mc.GetValue() );  // Print its value.
}
}`

该代码产生以下输出:


Total:  6


Image

图 25-5 。嵌套类及其封闭类的对象

可见性和嵌套类型

在第七章中,你学习了类和一般类型可以有publicinternal的访问级别。然而,嵌套类型是不同的,因为它们有成员可访问性而不是类型可访问性。因此,以下内容适用于嵌套类型:

  • Nested types declared in a class can have any one of five accessibility levels of class members, which are public, protected, private, internal or protected internal.
  • Nested types declared inside a structure can have any one of three accessibility levels of structure members public, internal or private.

在这两种情况下,嵌套类型的默认访问级别是private,这意味着在封闭类型之外看不到它。

封闭类和嵌套类的成员之间的关系不那么简单,如图 25-6 中的所示。嵌套类型可以完全访问封闭类型的成员,而不管它们声明的可访问性如何,包括成员privateprotected

然而,这种关系是不对称的。尽管封闭类型的成员总是可以看到嵌套类型声明并创建其变量和实例,但他们没有对嵌套类型成员的完全访问权。相反,它们的访问仅限于嵌套类成员的声明访问,就像嵌套类型是一个单独的类型一样。也就是说,他们可以访问publicinternal成员,但不能访问嵌套类型的privateprotected成员。

Image

图 25-6 。嵌套类型成员和封闭类型成员之间的可访问性

您可以将这种关系总结如下:

  • Members of nested types always have full access to members of closed types.
  • A closed type
    • Members of can always access the nested type itself.
    • Only declared the nested type

成员的访问权限

嵌套类型的可见性也会影响基成员的继承。如果封闭类是派生类,嵌套类型可以隐藏同名的基类成员。和往常一样,在嵌套类的声明中使用new修饰符来使隐藏显式。

嵌套类型中的this引用是指嵌套类型的对象——而不是封闭类型的对象。如果嵌套类型的对象需要访问封闭类型,它必须有一个对它的引用。您可以通过让封闭对象将它的this引用作为参数提供给嵌套类型的构造函数来授予它这种访问权限,如下面的代码所示:

`   class SomeClass                         // Enclosing class
{
int Field1 = 15, Field2 = 20;        // Fields of enclosing class
MyNested mn = null;                  // Reference to nested class

public void PrintMyMembers()
{
mn.PrintOuterMembers();           // Call method in nested class.
}

public SomeClass()                   // Constructor
{
mn = new MyNested(this);          // Create instance of nested class.
}                     ↑
Pass in the reference to the enclosing class.
class MyNested                       // Nested class declaration
{
SomeClass sc = null;              // Reference to enclosing class

public MyNested(SomeClass SC)     // Constructor of the nested class
{
sc = SC;                       // Store reference to enclosing class.
}

public void PrintOuterMembers()
{
Console.WriteLine("Field1: {0}", sc.Field1);   // Enclosing field
Console.WriteLine("Field2: {0}", sc.Field2);   // Enclosing field
}
}                                    // End of nested class
}

class Program
{
static void Main( ) {
SomeClass MySC = new SomeClass();
MySC.PrintMyMembers();
}
}`

该代码产生以下输出:


Field1: 15 Field2: 20


析构函数和 Dispose 模式

在第六章中,我们看了构造函数,它创建并设置了一个使用的类对象。一个类还可以有一个析构函数,它可以在一个类的实例不再被引用后执行清理或释放非托管资源所需的操作。非托管资源是指使用 Win32 API 获得的文件句柄或非托管内存块。这些东西不是你用就能得到的 .NET 资源,所以如果您坚持使用 .NET 类,你不太可能必须写很多析构函数。

关于析构函数,需要知道的重要事情如下:

每个类只能有一个析构函数。* A constructor cannot have parameters.* A destructor cannot have an accessibility modifier.* The destructor has the same name as the class, but it is preceded by a tilduh character (pronounced TIL-duh).* The destructor only acts on the instance of the class; Therefore, there is no static destructor.* Destructor cannot be explicitly called in code . On the contrary, in the process of garbage collection, when the garbage collector analyzes your code and determines that there are no more paths in the code that may refer to the object, the system will call it.

例如,以下代码说明了名为Class1的类的析构函数的语法:

   Class1 { ~Class1()                   // The destructor { *CleanupCode* } ... }

使用析构函数的一些重要准则如下:

  • Don't implement destructors if you don't need them. They can be very expensive in terms of performance.
  • The destructor should only release the external resources owned by the object.
  • The destructor should not access other objects, because you can't assume that those objects have not been destructed.

Image 在 3.0 版本发布之前,析构函数有时被称为终结器。你有时可能仍然会在文献和 .NET API 方法名。

标准处置模式

与 C++析构函数不同,当实例超出范围时,不会立即调用 C# 析构函数。事实上,没有办法知道析构函数何时会被调用。此外,如前所述,您不能显式调用析构函数。你所知道的是,在对象从托管堆中移除之前,系统会在某个时候调用它。

如果您的代码包含需要尽快释放的非托管资源,您不应该将该任务留给析构函数,因为不能保证析构函数会很快运行。相反,您应该采用所谓的标准处置模式

标准处置模式包括以下特征:

  • Classes with unmanaged resources should implement the IDisposable interface, which consists of a method named Dispose. Dispose Contains the cleanup code for releasing resources.
  • When your code runs out of resources and you want to release them, your program code should call the Dispose method. Note that Dispose is called by your code , not the system. Your class should also implement a destructor that calls the Dispose method in case Dispose has not been called before.

这可能有点混乱,所以让我总结一下模式。您希望将所有清理代码放在一个名为Dispose的方法中,当您的代码处理完资源时会调用该方法。作为备份,万一没有调用Dispose,你的类析构函数应该调用Dispose。另一方面,如果调用了Dispose,那么你要告诉垃圾收集器不要调用析构函数,因为清理已经由Dispose处理了。

*你的析构函数和Dispose代码应该遵循以下准则:

  • Write the logic of your destructor and Dispose method, so that if your code cannot call Dispose for some reason, your destructor will call it, thus releasing resources.
  • At the end of the Dispose method, it should be a call to the GC.SuppressFinalize method, which tells CLR not to call the destructor of this object, because the cleaning has been completed.
  • Implement the code in Dispose so that it is safe for the method to be called many times. That is to say, if it is written in this way, if it has been called, then any subsequent calls will not do any extra work and will not throw an exception.

下面的代码显示了标准的 dispose 模式,如图 25-7 所示。关于代码的重要内容如下:

  • Dispose method has two overloads: public method and protected method. protected Overload is an overload that contains actual cleanup code.
  • The public version is the version that you will explicitly call from your code to perform cleanup. In turn, it calls the protected version.
  • The destructor calls protected version.
  • The bool parameter of the protected version allows the method to know where it was called-destructor or elsewhere in the code. This is very important for it, because according to what it is, it will do something slightly different. You can find the details in the code below.

Image

Image

图 25-7 。标准处置模式

比较构造函数和析构函数

表 25-3 提供了构造函数和析构函数被调用的总结和比较。

Image

与 COM 互操作

虽然本文没有涉及 COM 编程,但是 C# 4.0 有几个语言特性可以使 COM 编程变得更容易。其中之一是省略引用特性,当你不需要使用方法传回的值时,它允许你调用一个 COM 方法而不使用ref关键字。

例如,如果运行程序的计算机上安装了 Microsoft Word,则可以在自己的程序中使用 Word 的拼写检查功能。您将使用的方法是在Document类上的CheckSpelling方法,它在Microsoft.Office.Tools.Word名称空间中。这个方法有 12 个参数,都是ref参数。如果没有这个特性,您必须为每个参数提供引用变量,即使您不需要使用它们将数据传递给方法或从方法接收数据。省略ref关键字仅适用于 COM 方法——对于其他任何方法,您仍然会得到一个编译错误。

这段代码可能类似于下面所示的代码。请注意以下关于此代码的内容:

  • The call on the fourth line only uses the second and third parameters, both of which are Boolean values. However, since this method requires ref parameter, you must create two variables ignoreCase and alwaysSuggest of type object to save these values.
  • The third line creates a variable optional of type object for the other ten parameters.

   object ignoreCase    = true; object alwaysSuggest = false;             Objects to hold Boolean variables object optional      = Missing.Value; <ins>      ↓     </ins>  <ins>       ↓          </ins> tempDoc.CheckSpelling( ref optional,  ref ignoreCase, ref alwaysSuggest, ref optional, ref optional, ref optional, ref optional, ref optional, ref optional, ref optional, ref optional, ref optional );

有了省略引用特性,我们可以很好地解决这个问题,因为我们不必对那些不需要输出的参数使用ref关键字,我们可以对我们关心的两个参数使用内联bool s。简化的代码如下所示:

                                      bool  bool object optional = Missing.Value;   ↓    ↓ tempDoc.CheckSpelling( optional, true, false, optional, optional, optional, optional, optional, optional, optional, optional, optional );

但是我们也可以使用可选参数特性。同时使用这两个特性(省略 ref 和可选参数)会使最终表单比原始表单简单得多:

   tempDoc.CheckSpelling( Missing.Value, true, false );

下面的代码在一个完整的程序中包含了这个方法。要编译这段代码,您需要在您的计算机上安装 Visual Studio Tools for Office(VSTO ),并且您必须在项目中添加对Microsoft.Office.Interop.Word程序集的引用。要运行编译后的代码,还必须在计算机上安装 Microsoft Word。

`   using System;
using System.Reflection;
using Microsoft.Office.Interop.Word;

class Program
{
static void Main()
{
Console.WriteLine( "Enter a string to spell-check:" );
string stringToSpellCheck = Console.ReadLine();

string spellingResults;
int errors = 0;
if ( stringToSpellCheck.Length == 0 )
spellingResults = "No string to check";
else
{
Microsoft.Office.Interop.Word.Application app =
new Microsoft.Office.Interop.Word.Application();

Console.WriteLine( "\nChecking the string for misspellings ..." );
app.Visible = false;

Microsoft.Office.Interop.Word._Document tempDoc = app.Documents.Add( );

tempDoc.Words.First.InsertBefore( stringToSpellCheck );
Microsoft.Office.Interop.Word.ProofreadingErrors
spellErrorsColl = tempDoc.SpellingErrors;
errors = spellErrorsColl.Count;

//1.  Without using optional parameters
//object ignoreCase    = true;
//object alwaysSuggest = false;
//object optional      = Missing.Value;
//tempDoc.CheckSpelling( ref optional, ref ignoreCase, ref alwaysSuggest,
//    ref optional, ref optional, ref optional, ref optional, ref optional,
//    ref optional, ref optional, ref optional, ref optional );

//2. Using the "omit ref" feature
object optional = Missing.Value;
tempDoc.CheckSpelling( optional, true, false, optional, optional, optional,
optional, optional, optional, optional, optional, optional );

Image
//3. Using "omit ref" and optional parameters
//tempDoc.CheckSpelling( Missing.Value, true, false );

app.Quit(false);
spellingResults = errors + " errors found";
}

Console.WriteLine( spellingResults );
Console.WriteLine( "\nPress to exit program." );
Console.ReadLine();
}
}`

当您运行这段代码时,它会产生如图 25-8 中所示的控制台窗口,要求您输入想要通过拼写检查器运行的字符串。当它收到字符串时,它打开 Word 并对其运行拼写检查。这时,你会看到 Word 的拼写检查窗口出现,如图图 25-9 所示。

Image

***图 25-8。*要求将字符串发送到 Word 拼写检查器的控制台窗口

Image

图 25-9。 Word 的拼写检查器使用控制台程序的 COM 调用创建*

posted @ 2024-08-10 19:01  绝不原创的飞龙  阅读(26)  评论(0编辑  收藏  举报