第五章 方法
5.3 本地变量
- 本地变量的存在性和生存期仅限于创建它的块及其内嵌的块
它从声明它的那一点开始存在
它在块完成执行时结束存在 - 可以在方法体内任意位置声明本地变量,但必须在使用它们前声明
下表对比了实例字段和本地变量
实例字段 | 本地变量 | |
---|---|---|
生存期 | 从实例被创建时开始, 直到实例不再被访问时结束 |
从它在块中被声明的那点开始, 在块完成执行时结束 |
隐式初始化 | 初始化成该类型的默认值 | 没有隐式初始化。如果变量在使用之前 没有被复制,编译器就会产生一条错误信息 |
存储区域 | 由于实例字段是类的成员, 所以所有字段都存储在堆里, 无论它们是值类型还是引用类型 |
值类型:存储在栈中 引用类型:引用存储在栈里,数据存储在堆里 |
5.3.1 类型推断和var关键字
var关键字并不是特定类型变量的符号。
它只是语法上的速记,表示任何可以从初始化语句的右边推断出的类型。
使用显式类型名的代码片段和使用var关键字的代码片段在语义上是等价的。
使用var关键字的重要条件:
- 只能用于本地变量,不能用于字段
- 只能在变量声明中包含初始化时使用
- 一旦编译器推断出变量的类型,它就是固定且不能更改的
与dynamic区别
var类型的变量是程序在编译时,编译器会根据语句右边自动推断出var变量的实际类型,并用实际类型替换该变量的声明,看上去好像是我们在编码的时候用实际类型进行声明的。
dynamic类型被编译后,实际是个object类型,只是编译器会对dynamic类型进行特殊处理,使其在编译期间不进行任何类型检查,而是将类型检查放到了运行期。
5.4 本地常量
本地常量和本地变量一样必须声明在块的内部。
常量有两个重要特征:
- 常量在声明时必须初始化
- 常量在声明后不能改变
常量的核心声明如下所示:
- 在类型之前增加关键字const
- 必须有初始化语句。初始化值必须在编译期决定,通常是一个预定义简单类型或由其组成的表达式。它还可以是null引用,但它不能是某对象的引用,因为对象的引用是在运行时决定的。
说明
关键字const不是一个修饰符,而是核心声明的一部分。它必须直接放在类型的前面
5.9 参数
5.9.1 形参
形参是本地变量,它声明在方法的参数列表中,而不是方法体中。
下例中声明了两个形参:一个int型,一个float型
public void PrintSum(int x, float y) -->方法头
{ -->
... --> 方法体
} -->
- 因为形参是变量,所以它们有类型和名称,并能被写入和读取
- 和方法中的其他本地变量不同,参数在方法体的外面定义并在方法开始之前初始化
- 参数列表中可以有任意数目的形参声明,而且声明必须用逗号隔开
5.9.1 实参
当代码调用一个方法时,形参的值必须在方法的代码开始执行之前被初始化
- 用于初始化形参的表达式或变量称作实参(actual parameter)
- 实参位于方法调用的参数列表
- 每一个实参必须与对应形参的类型相匹配,或是编译器必须能够把实参隐式转换为那个类型
5.10 值参数
值参数,是指在形式参数表中前面没有Var,后面没有类型说明的这一类参数。只传递数值,在函数中对之所进行的改动,不会造成原始变量值的改变。
它类似于局部变量,仅为过程和函数的执行提供初值而不影响调用时的实际参数的值,在调用函数时值参数所对应的实际参数可以是表达式。
使用值参数,通过将实参的值复制到形参的方式把数据传递给方法。方法被调用时,系统做如下操作:
- 在栈中为形参分配空间
- 将实参的值复制给形参
在把变量用作实参之前,变量必须被赋值(输出参数out除外)。对于引用类型,变量可以被设置为一个实际的引用或null。
说明
值类型是指类型本身包含其值。
值参数是把实参的值复制给形参。两个概念不要搞混
class MyClass
{
public int Val = 20; //初始化字段为20
}
class Program
{
static void MyMethod(MyClass f1, int f2)
{
f1.Val = f1.Val + 5; //参数字段加5
f2 = f2 + 5; //另一参数加5
Console.WriteLine("f1.Val:{0}, f2:{1}", f1.Val, f2);
}
static void Main()
{
MyClass a1 = new MyClass();
int a2 = 10;
MyMethod(a1, a2);
Console.WriteLine("f1.Val:{0}, f2:{1}", a1.Val, a2);
}
}
输出结果:
f1.Val:25, f2:15
f1.Val:25, f2:10
下图为实参和形参在方法执行不同阶段的值
值参数
- 在方法被调用前,用作实参的变量a2已经在栈里了
- 在方法开始前,系统在栈中为形参分配空间,并从实参复制值
因为a1是引用类型的,所以引用被复制,结果实参和形参都引用堆中的同一个对象。
因为a2是值类型,所以值被复制,产生了一个独立的数据项 - 在方法的结尾,f2和对象f1的字段都被加上了5
方法执行后,形参从栈中弹出
a2,值类型,它的值不受方法行为的影响
a1,引用类型,但它的值被方法的行为改变了
5.11 引用参数
- 使用引用参数时,必须在方法的声明和调用中都使用ref修饰符
- 实参必须是变量,在用作实参前必须被赋值。如果是引用类型变量,可以赋值为一个引用或null
引用参数具有如下特征: - 不会为形参在栈上分配内存
- 实际情况是,形参的参数名将作为实参变量的别名,指向相同的内存位置
由于形参名和实参名的行为就好像指向相同的内存位置,所以在方法的执行过程中对形参作的任何改变在方法完成后依然有效(表现在实参变量上)
说明
记住要在方法的声明和调用上都使用ref关键字
class MyClass
{
public int Val = 20; //初始化字段为20
}
class Program
{
static void MyMethod(ref MyClass f1, ref int f2)
{
f1.Val = f1.Val + 5; //参数字段加5
f2 = f2 + 5; //另一参数加5
Console.WriteLine("f1.Val:{0}, f2:{1}", f1.Val, f2);
}
static void Main()
{
MyClass a1 = new MyClass();
int a2 = 10;
MyMethod(ref a1, ref a2);
Console.WriteLine("f1.Val:{0}, f2:{1}", a1.Val, a2);
}
}
输出结果:
f1.Val:25, f2:15
f1.Val:25, f2:15
下图展示了方法执行不同阶段实参和形参的值
引用参数,形参就像实参的别名- 在方法调用之前,将要被用作实参的变量a1和a2已将在栈里了
- 在方法的开始,形参名被设置为实参的别名。变量a1和f1引用相同的内存位置,a2和f2引用相同的内存位置
- 在方法的结束位置,f2和f1的对象字段都被加上了5
- 在方法执行之后,形参的名称已经失效,但是值类型a2的值和引用类型a1所指向的对象的值都被方法内的行为改变了
5.12 引用类型作为值参数和引用参数
- 将引用类型对象作为值参数传递
如果在方法内创建一个新对象并赋值给形参,将切断形参和实参之间的关联,并且在方法调用结束后,新对象也将不复存在。 - 将引用类型对象作为引用参数传递
如果在方法内创建一个新对象并赋值给形参,在方法结束后该对象依然存在,并且是实参所引用的值。
下面代码展示第一种情况—将引用类型对象作为值参数传递:
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(string[] args)
{
MyClass a1 = new MyClass();
Console.WriteLine("Before method Call: {0}", a1.Val);
RefAsParameter(a1);
Console.WriteLine("After method Call: {0}", a1.Val);
Console.ReadKey();
}
}
输出结果:
Before method Call: 20
After member assignment: 50
After new object creation: 20
After method Call: 50
下图展示了程序具体的运行原理:
- 在方法开始时,实参和形参都指向堆中相同的对象
- 在为对象的成员赋值之后,它们仍指向堆中相同的对象
- 当方法分配新的对象并赋值给形参时,(方法外部)实参仍指向原始对象,而形参指向的是新对象
- 在方法调用之后,实参指向原始对象,形参和新对象都会消失
第二种情况将引用类型对象作为引用参数的情况:
class MyClass { public int Val = 20; }
class Program
{
static void RefAsParameter(ref 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(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);
Console.ReadKey();
}
}
输出结果:
Before method Call: 20
After member assignment: 50
After new object creation: 20
After method Call: 20
下图展示了程序的运行原理:
- 在方法调用时,形参和实参都指向堆中的对象。
- 对成员值的修改会同时影响到实参和形参。
- 当方法创建新的对象并赋值给形参时,形参和实参的引用都指向该新对象。
- 在方法结束后,实参指向在方法中创建的新对象。
5.13 输出参数
输出参数用于从方法体内把数据传出到调用代码。输出参数有以下要求:
- 必须在声明和调用中都是用修饰符。输出参数的修饰符是out不是ref
- 和引用参数类似,实参必须是变量,而不能是其他类型的表达式。因为方法需要内存位置保存返回值。
使用方法如下:
void MyMethod(out int val) //方法声明
{ ...}
...
int y = 1; //实参变量
MyMethod(out y); //方法调用
与引用参数类似,输出参数的形参担当实参的别名。形参和实参都是同一块内存位置的名称。因此,方法内对形参的任何改变在方法执行完成之后通过实参变量都是可见的。
与引用参数不同,输出参数有以下要求:
- 在方法内部,输出参数在方法内能够被读取之前必须被赋值。这意味着参数的初始值是无关的,而且没有必要在方法调用之前为实参赋值。
- 方法内的代码在读取输出变量之前必须对其写入,所以不可能使用输出参数把数据传入方法。事实上,如果方法中有任何执行路径试图在输出参数被赋值之前读取它,编译器就会报错。
public void Add2(out int outValue)
{
int var1 = outValue + 2; //出错了,使用了未赋值的输出参数outValue
}
下面代码展示使用方法:
class MyClass { public int Val = 20; }
class Program
{
static void MyMethod(out MyClass f1, out int f2)
{
f1 = new MyClass(); //创建一个类变量
f1.Val = 25; //赋值类字段
f2 = 15; //赋值int参数
}
static void Main(string[] args)
{
MyClass a1 = null;
int a2;
MyMethod(out a1, out a2); //调用方法
}
}
- 在方法调用之前,将要被用作实参的变量a1和a2已经在栈里了
- 在方法的开始,形参的名称设置为实参的别名。可以认为a1和f1指向的是相同的内存位置,也可认为a2和f2指向的是相同的内存位置。a1和a2不在作用域内,所以不能在MyMethod中访问。
- 在方法内部,代码创建了一个MyClass类型的对象并把它赋值给f1,然后赋一个值给f1的字段,也赋一个值给f2,对f1和f2的赋值都是必需的,因为它们是输出参数。
-
方法执行之后,形参的名称已经失效,但是引用类型的a1值和值类型的a2值都被方法内的行为改变了。
5.14 参数数组
之前所述的参数类型都 必须严格地一个实参对应一个形参。参数数组则不同,它允许零个或多个实参对应一个特殊地形参。
- 一个参数列表中只能有一个参数数组
- 如果有,必须是列表中地最后一个
- 由参数数组表示地所有参数都必须具有相同地类型
声明参数数组地方法:
- 在数组类型前使用 params 修饰符
- 在数据类型后放置一组空方括号
下图展示了 int 型参数数组地声明方法,此例中,形参 inVals 可代表零个或多个 int 实参
- 数组是一组整齐的相同类型的数据项
- 数组使用一个数字索引进行访问
- 数组是一个引用类型,因此它所有的数据项都保存在堆中
5.14.1 方法调用
可使用两种方式为参数数组提供实参
- 一个逗号分隔的该数据类型元素的列表。所有元素必须是方法声明中指定的类型。
ListInts( 10, 20, 30 ) //3个int
- 一个该数据类型元素的一维数组
int[] intArray = { 1, 2, 3 }
ListInts( intArray ); //一个数组变量
注意,在以上示例中,不需要在调用时使用 params 修饰符。参数数组中修饰符的使用与其他参数类型的模式不一样。
- 其他参数类型是一致的,要么都使用修饰符,要么都不适用修饰符
值类型的声明和调用都不带修饰符
引用参数和输出参数在两个地方都需要修饰符 - params 修饰符的用法总结
在声明中需要修饰符
在调用中不允许使用修饰符