C--2012-说明指南-一-

321 阅读1小时+

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    {       public int High = 75;       public int Low  = 45;    }

   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 { public int Val = 20; }

   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    {       public int Val = 20;    }

   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。*用递归方法构建和展开堆栈的例子