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