目录
5.1 方法的结构
5.2 方法体内部的代码执行
5.3.1 类型推断和Var关键字
5.3.2 嵌套块中的本地变量
5.4 本地常量
5.5 控制流
5.6 方法调用
5.7 返回值
5.8 返回语句和void 方法
5.9 参数
5.9.1 形参
5.9.2 实参
位置参数示例
5.10 值参数
5.11 引用参数
5.12 引用类型作为值参数和引用参数
5.13 输出参数
5.14 参数数组
5.14.1 方法调用
5.14.2 用数组作为实参
5.15 参数类型总结
5.16 方法的重载
5.17 命名参数
5.18 可选参数
5.19 栈帧
5.20 递归
方法是一块具有名称的代码。可以使用方法的名称从别的地方执行代码,也可以把数据传入方法并接受数据输出。
如前一章所属,方法是类的函数成员。方法有两个主要部分,如图5-1所示:方法头和方法体。
下面的示例展示了方法头的形式。接下来阐述其中的每一部分。
例如,下面的代码展示了一个名称为MyMethord的简单方法,它多次轮流调用WriteLine方法。
尽管前面几章都描述了类,但是还有另外一种用户定义的类型,叫做struct,我们会在第10章中介绍。本章中介绍的大多数有关方法的内容同样适用于struct方法。
方法体是一个块,是大括号起的语句序列(参考第二章)。块可以包含以下项目:
图5-2展示了方法体及其组成的示例。
5.3 本地变量
和第4章介绍的字段一样 ,本地变量也保存数据。字段通常保存和对象状态有关的数据,而创建本地变量经常是用于保存本地的临时的计算数据。表5-1对比了本地变量和实例字段的差别。
下面这行代码展示了本地变量生明的语法。可选的初始化语句由等号和用于初始化的值组成。
下面的示例展示了两个本地变量的声明和使用。第一个是int类型,第二个是SomeClass类型变量。
如果观察下面的代码,你会发现在声明的开始部分提供类型名时,你提供的时编译器能从初始化语句右边推断的信息。
所以在这两种情况中,在声明的开始部分包括显示的类型名是多余的。
var关键字并不是特定类型变量的符号。它只是句法上的速记,在表示任何可以从初始化语句的右边推断出的类型。在第一个声明中,他是int的速记;在第二个声明中,他是MyExcellentClass的速记。前文中使用显示类型名的代码片段和使用var关键字的代码片段在语义上是等价的。
使用var关键字有一些重要条件:
方法体内部可以嵌套其他的块。
图5-3阐述了两个本地变量的生存期,展示了代码和栈的状态。箭头标出了刚执行过的行。
本地常量很想本地变量,只是一旦被初始化,它的值就不能改变了。如同本地变量,本地常量必须在块的内部。
常量的两个最重要的特征如下。
常量的核心声明如下所示。语法与字段或变量的声明相同,除了下面内容。
就像本地变量,本地常量声明在方法体或代码块里,在声明它的块结束的地方失效。列如,在下面的代码中,类型为内嵌类型double的本地常量在PI在方法DisplayRadii结束后失效。
方法包含了大部分组成程序行为的代码。剩余部分在其他的函数成员中,如属性和运算符。
术语控制流指的是程序从头到尾的执行流程。默认情况下,程序执行持续地从一条语句到下一条语句,控制流语句允许你改变执行的顺序。
在这一节,只会提及一些能用于代码的控制语句,第九章会详细阐述它们。
例如,下面的方法展示了两个控制流语句,先别管那些细节。
可以从方法体的内部调用其他方法。
例如下面的类声明了一个名称为PrintDateAndTime的方法,在Main方法内会调用该方法。
图5-4阐明了调用方法时的动作顺序。
方法可以向调用代码返回一个值。返回值被插入到调用代码中发起调用的表达式所在的位置。
下面的代码展示了两个方法声明。第一个返回int型值,第二个不返回值。
声明了返回类型的方法必须使用下面形式的返回语句从方法中返回一个值。返回语句包括关键字return以及其后面的表达式。每一条贯穿方法的路径都必须以一条这种形式的return语句结束。
在上一节,我们看到有返回值得方法必须包含返回语句。void方法不需要返回语句,当控制流到达方法体的关闭大括号时,控制返回到调用代码,并且没有值被插入到调用代码中。
不过,当特定条件符合的时候,我们通常会提前退出方法以简化程序逻辑。
return;
例如,下面的代码展示了一个名称为SomeMethord的void方法的声明。它可以在三个可能的地方返回到调用代码。前两个在if语句的分之内。if语句将在第九章阐述。最后一个方法体的结尾处。
下面的代码展示了一个带有一条返回语句的void方法示例,该方法只有当时间是下午的时候才能写出一条消息,如5-5所示,其过程如下。
迄今为止,你已经看到方法是可以被程序中很多地方调用的命名代码单元,它能把一个值返回给调用代码。返回一个值的确有用,但如果返回多个值呢?还有,能在方法开始执行时候把数据传入方法也会有用。参数就是允许做着两件事的特殊变量。
形参是本地变量,它声明在方法的参数列表中,而不是在方法体中。
下面的方法头展示了参数声明的语法。它声明了两个形参:一个是int型,另一个是float型。
形参在整个方法体内使用,在大部分地方就像其他本地变量一样。例如,下面的PrintSum方法的声明使用两个形参x和y,以及一个本地变量Sum,它们都是int型。
当代码调用一个方法时,形参的值必须在方法的代码开始执行之前被初始化。
l例如,下面的代码展示了方法PrintSum的调用,他有两个int型的实参。
当方法被调用的时候,每个实参的值都被用于初始化相应的形参,方法体随后被执行。图5-6阐明了实参和形参的关系。
注意在之前那段实例代码以及图5-6中,实参的数量必须和形参的数量一致,并且每个实参的类型也必须和对应的形参类型一致。这种形式的参数叫做位置参数。我们稍后会看其他的一些选项,不过现在我们先来看看位置参数。
在如下代码中,MyClass类声明了两个方法 -----一个方法接受两个整数并且返回他们的和,另一个方法接受了两个float并且返回它们的平均值。对于第二次调用,注意编译器把int值5和someInt隐式转换成了float类型。
参数有几种,各自以略微不同的方式从方法传入或传出数据。你到现在一直看到的·这种类型是默认的类型,称为值参数(value parameter)。
使用值参数,通过将实参的值复制到形参的方法把数据传递给方法。方法被调用时,系统做如下操作。
值参数的实参不一定是变量。它可以是任何能计算成相应数据类型的表达式。例如,下面的代码展示了两个方法调用。在第一个方法调用中,实参是float类型的变量;第二个方法调用中,它是计算成float的表达式。
例如,下面的代码展示了一个名称为MyMethord的方法,它有两个参数,一个MyClass型变量和一个int。
图5-7说明了实参和形参在方法执行的不同阶段时的值,他表示以下3点。
第二种参数类型是引用参数。
例如,下面的代码阐明了引用参数和调用的语法:
在之前的内容中我们已经认识到了,对于值参数,系统在栈上为形参分配内存,相反,引用参数具有以下特征。
由于形参名和实参名的行为就好像指向相同的内存位置,所以在方法的执行过程中对形参作的任何改变在方法完成后依然有效(表现在实参变量上)。
图5-8阐明了在方法执行的不同阶段实参和形参的值。
在前几节中我们看到了,对于一个引用参数,不管是将其作为值参数传递还是作为引用参数传递,我们都可以在方法成员内部修改它的成员。不过我们并没有在方法内部设置形参本身。本节我们来看看那在方法内部设置引用类型形参时会发生什么。
下面代码展示了第一种情况-----将引用类型对象作为值参数传递:
图5-9阐明了上述代码的以下几点。
下面的代码演示了将引用类型对象作为引用参数的情况。除了方法声明和方法调用时要使用ref关键字外,与上面的代码完全相投。
你肯定还记得,引用参数的行为就像是实参作为形参的别名。这样一来上面的代码就很好解释了。图5-10阐明了代码的以下几点。
输出参数用于从方法体内把数据传出到调用代码,它们的行为与引用参数非常类似。如同引用参数,输出参数有以下要求。
例如,下面的代码声明了名称为MyMethod的方法,它带有单个输出参数。
与引用参数类似,输出参数的形参担当实参的别名。形参和实参都是同一块内存位置的名称。显然,方法内对形参的任何改变在方法执行完成之后通过实参变量都是可见的。
与引用参数不同,输出参数有以下要求。
因为方法内的代码在读取输出变量之前必须对其写入,所以不可能使用输出参数把数据传入方法。事实上,如果方法中有任何执行路径试图在输出参数被方法赋值之前读取他,编译器就会产生一条错误信息。
图5-11阐明了在方法执行的不同阶段段中实参和形参的值。
至此,本书所描述的参数类型都必须严格地一个实参对应一个形参。参数数组则不同,它允许零个或多个实参对应一个特殊的形参。参数数组的重点如下。
声明一个参数数组必须做到的事如下。
下面的方法头展示了int型参数数组的声明语法。在这个示例中,形参inVals可以代表零个或多个int实参。
类型名后面的空方括号指明了参数是一个整数数组。在这里不必在意数组细节,它们将在第12章详细阐述。而现在,所有你需要了解的内容如下。
可以使用两种方式为参数数组提供实参。
请注意,在这些实例中,没有调用时使用params修饰符。参数数组中修饰符的使用与其他参数类型的模式并不相符。
延伸式
参数数组方法调用的第一种形式有时被称为延伸式,这种形式在调用中使用分离的实参。例如,下面代码中的方法ListInsts的声明可以匹配它下面所有的方法调用,虽然它们有不同数目的实参。
在使用一个为参数数组分离实参的调用时,编译器做下面的事。
例如,下面的代码声明了一个名称为ListInts的方法,它带有一个参数数组。Main声明了3个整数并把他们传给了数组。
图5-12阐明了在方法执行的不同阶段实参和形参的值。
关于参数数组,需要记住的重要一点是当数组在堆中被创建时,实参的值被复制到数组中在这方面,它们像值参数。
也可以在方法调用之前创建并组装一个数组,把单一的数组变量作为实参传递。这种情况下编译器使用你的数组而不是重新创建一个。
例如,下面代码使用前一个示例中声明的方法ListIns.在这段代码中,Main创建一个数组并用数组变量而不是使用分离的整数作为实参。
因为有4种参数类型,有时很难记住它们的不同特征。表5-2对它们做了总结,使之更易于比较和对照。
一个类中可以有一个以上的方法拥有相同的名称,这叫做方法重载(method overload)。使用相同的名称的每一个方法必须有一个和其他方法不同的签名(signature)。
下面的代码展示了一个非法的重载方法。两个方法仅返回类型和形参名不同,但它们仍有相同的签名,因为它们有相同的方法名,而参数的数目、类型和顺序也相同。编译器会对这段代码生成一条错误信息。
至今我们所有用到的参数都是位置参数,也就是说每一个实参的位置都必须与相应的形参位置一一对应。
此外,C#还允许我们使用命名参数(named parameter)。只要显示指定参数名字,就可以以任意顺序在方法调用中列出实参。细节如下
图5-13 在使用命名参数的时候,需要在方法调用中包含参数名字。而方法的声明不需要任何改变
在调用的时候,你可以即使用位置参数有使用命名参数,但如果这么做,所有位置参数必须先列出。例如,下面的代码演示了Calc方法的声明及其使用位置参数和命名参数不同组合的5种不同的调用方式
命名参试对于自描述的程序来说很有用,因为我们可以在方法调用的时候显示那个值赋给那个形参。例如,下面代码调用了两次GetCyLinderVolume,第二次调用具有更多的信息并且更不容易出错。
C#还允许可选参数(optional parameter)。所谓可选参数就是我们可以在调用方法的时候包含这个参数,也可以省略它。
为了表明某个参数是可选的,你需要在方法声明的时候为参数提供默认值。指定默认值得语法和初始化本地变量的语法一样,如下面的代码的方法声明所示,在代码中,
对于可选参数的声明,我们需要知道如下几个重要事项。
在之前的示例中我们已经看到了,可以在方法调用的时候省略相应的实参从而认为可选参数使用默认值。但是在许多情况下,不能随意省略可选参数的组合,因为在很多情况下这么做会导致使用那些可选参数不明确,规则如下。
如果需要随意省略可选参数列表中的可选参数,而不是从列表的最后开始,那么必须使用可选参数的名字来消除赋值的歧义。在这种情况下,你需要结合利用命名参数和可选参数特性。下面的代码演示了位置参数。可选参数和命名参数的这种用法。
至此,我们已经知道了局部变量和参数是位于栈上的,让我们再来深入讨论一下其组织。
在调用方法的时候,内存从栈的顶部开始分配,保存和方法关联的一些数据项。这块内存叫做方法的栈帧(stack frame)。
例如如下代码声明了3个方法。Main调用MethodA,MethodbB,创建了3个栈帧。在方法退出的时候,栈展开。
除了调用其他方法,方法也可以调用自身。这叫做递归。
递归会产生很优雅的代码,比如下面计算阶乘数的方法就是如此。注意在本例的方法内部,方法使用比输入参数小1的实参调用自身。
调用方法自身的机制和调用其他方法其实完全一样的。都是为每一次方法调用把新的栈帧压如栈顶。
例如,在下面的代码中,Count方法比输入参数小1的值调用自身然后打印输入参数。随着递归也来越深,栈也越来越大。
图5-17演示了这段代码。注意,如果输入值3,那么Count方法就有4个不同的独立栈帧。每一个都有其自己的输出参数值inVal。