值参数
将实参的值复制到形参的方式把数据传递给方法
- 在栈中为形参分配空间
- 将实参的值复制给形参
例如
float func1(float val) { //形参 val就是指参数
float j = 2.6f;
float k = 5.1f;
return j + k - val;
}
float k = 2.0f;
float fVlaue1 = fun1(k); //实参
实参在作为参数前必须被复制
实例分析
class MyClass
{
public int val = 20;
}
class Program
{
static void MyMethod(MyClass f1, int f2)
{
f1.val = f1.val + 5;
f2 = f2 + 5;
Console.WriteLine($"输出2:f1.val:{f1.val},f2:{f2}");
}
static void Main(string[] args)
{
MyClass a1 = new MyClass();
int a2 = 10;
MyMethod(a1, a2);
Console.WriteLine($"输出2:f1.val:{a1.val},f2:{a2}");
}
}
输出结果
输出2:f1.val:25,f2:15
输出1:f1.val:25,f2:10
分析
在方法被调用前,用作实参的变量a2已经在栈里了
在方法开始时,系统在栈中为形参分配空间,并从实参复制值。
- 因为a1是引用类型的,所以引用被复制,结果实参和形参都引用堆中的同一个对象。也就是a1.val和f1.val地址都是同一val地址,所以一边改变,另一边也跟着改变
- 因为a2是值类型的,所以值被复制,产生了一个独立的数据项。
值类型与引用类型区别
在方法的结尾,f2和对象f1的字段都被加上了5- 方法执行后,形参从栈中弹出。
- a2,值类型,它的值不受方法行为的影响。
- a1,引用类型,但它的值被方法的行为改变了。
引用参数
与实参共享同一段内存,注意与引用类型是不同的
使用
- 使用引用参数时,必须在方法的声明和调用中都使用ref修饰符。
- 实参必须是变量,在用作实参前必须被赋值。如果是引用类型变量,可以赋值为一个引用或null。
语法
void MyMethod(ref int val) //方法声明
{
....
}
int y = 1; //实参变量
MyMethod(ref y); //方法调用
MyMethod(ref 3+5); //错误 必须是变量
特征
- 不会为形参在栈上分配内存
- 实际情况是,形参的参数名将作为实参变量的别名,指向相同的内存地址。所以对形参进行改变,实参也会发生改变
实例分析
class MyClass
{
public int val = 20;
}
class Program
{
static void MyMethod(ref MyClass f1, ref int f2)
{
f1.val = f1.val + 5;
f2 = f2 + 5;
Console.WriteLine($"输出2:f1.val:{f1.val},f2:{f2}");
}
static void Main(string[] args)
{
MyClass a1 = new MyClass();
int a2 = 10;
MyMethod(ref a1, ref a2);
Console.WriteLine($"输出1:f1.val:{a1.val},f2:{a2}");
Console.ReadKey();
}
}
结果
输出2:f1.val:25,f2:15
输出1:f1.val:25,f2:15
分析
结果为什么和之前的不同?
参考引用参数特征
在方法调用之前,将要被用作实参的变量a1和a2已经在栈里了。
在方法的开始,形参名被设置为实参的别名。变量a1和f1引用相同的内存位置,a2和f2引用相同的内存位置。
在方法的结束位置,f2和f1的对象的字段都被加上了5。
方法执行之后,形参的名称已经失效,但是值类型a2的值和引用类型a1所指向的对象的值都被方法内的行为改变了。
引用类型作为值参数和引用参数
引用类型作为值参数传递
如果在方法内创建一个新对象并赋值给形参,将切断形参与实参之间的关联,并且在方法调用结束后,新对象也将不复存在。
实例
class MyClass
{
public int val = 20;
}
class Program
{
static void RefAsParameter(MyClass f1)
{
f1.val = 50;
Console.WriteLine($"After member assignment:{f1.val}");
f1 = new MyClass();
Console.WriteLine($"After member object creation:{f1.val}");
}
static void Main(string[] args)
{
MyClass a1 = new MyClass();
Console.WriteLine($"Before method call:{a1.val}");
RefAsParameter(a1);
Console.WriteLine($"After method call:{a1.val}");
Console.ReadKey();
}
}
结果
Before method call:20
After member assignment:50
After member 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:{f1.val}");
f1 = new MyClass();
Console.WriteLine($"After member object creation:{f1.val}");
}
static void Main(string[] args)
{
MyClass a1 = new MyClass();
Console.WriteLine($"Before method call:{a1.val}");
RefAsParameter(ref a1);
Console.WriteLine($"After method call:{a1.val}");
Console.ReadKey();
}
}
结果
Before method call:20
After member assignment:50
After member object creation:20
After method call:20
分析
引用参数是将实参作为形参的别名
在方法调用时,形参和实参都指向堆中相同的对象。
对成员值的修改会同时影响到形参和实参。
当方法创建新的对象并赋值给形参时,形参和实参的引用都指向该新对象。
在方法结束后,实参指向在方法内创建的新对象。
输出参数
用于从方法体内把数据传出到调用代码。和引用参数类似
用法
- 必须在声明和调用中都使用修饰符。输出参数的修饰符是out而不是ref。
- 和引用参数相似,实参必须是变量,而不能是其他类型的表达式。
语法
void MyMethod(out int val) {
....
}
int y = 1;
MyMethod(out y);
与引用参数
同
- 声明和调用都需要使用修饰符
- 形参担当实参别名
- 形参和实参都是同一块内存位置
不同
- 在方法内部,输出参数在能够被读取之前必须被赋值。这意味着参数的初始值是无关的,而且没有必要在方法调用之前为实参赋值。
- 在方法返回之前,方法内部贯穿的任何可能路径都必须为所有输出参数进行一次赋值。
pubilc boid Add2(out int outValue) {
int car1 = outValue + 2; //错误!在方法赋值之前 要先被赋值
}
实例分析
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;
Console.WriteLine($"{f2}");
}
static void Main(string[] args)
{
MyClass a1 = null;
int a2 = 25;
MyMethod(out a1, out a2);
Console.WriteLine($"{a2}");
}
}
结果
15
15
分析
在方法调用之前,将要被用作实参的变量a1和a2已经在栈里了。
在方法的开始,形参的名称设置为实参的别名。你可以认为变量a1和f1指向的是相同的内存位置,也可以认为az和f2指向的是相同的内存位置。a1和a2不在作用域之内,所以不能在MyMethod中访问。
在方法内部,代码创建了一个MyClass类型的对象并把它赋值给f1。然后赋一个值给f1的字段,也赋一个值给f2。对f1和f2的赋值都是必需的,因为它们是输出参数。
方法执行之后,形参的名称已经失效,但是引用类型的a1和值类型的a2的值都被方法内的行为改变了。
参数数组
允许零个或多个参数对应一个特殊形参,数组是引用类型
使用规则
- 在一个参数列表中只能有一个参数数组。如果有,它必须是列表中的最后一个。
- 由参数数组表示的所有参数都必须具有相同的类型。
声明一个参数数组必须做的事如下
- 在数据类型前使用params修饰符。
- 在数据类型后放置一组空的方括号。
语法
void ListInts(params int[] inVals) { //参数数组声明
....
}
调用
LinstInts(10, 20, 30);
//或
int[] intArray = {1, 2, 3};
ListInts(intArray);
内存表示
- 接受实参列表,用它们在堆中创建并初始化一个数组。
- 把数组的引用保存到栈中的形参里。
- 如果在对应的形参数组的位置没有实参,编译器会创建一个有零个元素的数组来使用。
在参数中
- 如果数组参数是值类型,那么值被复制,实参不受方法内部的影响。
- 如果数组参数是引用类型,那么引用被复制,实参引用的对象可以受到方法内部的影响。
总结
参数类型 | 修饰符 | 是否声明时使用 | 是否在调用时使用 | 执行 |
---|---|---|---|---|
值 | / | / | / | 系统把实参的值复制到形参 |
引用 | ref | 是 | 是 | 形参是实参的别名 |
输出 | out | 是 | 是 | 仅包含一个返回的值。形参是实参的别名 |
数组 | params | 是 | / | 允许传递可变数目的实参到方法 |
函数重载
一个类中可以有一个以上的方法拥有相同的名称。与方法覆盖不同
使用相同名称的每个方法必须有一个和其他方法不相同的签名。
方法签名组成
- 方法的名称;
- 参数的数目;
- 参数的数据类型和顺序;
- 参数修饰符。
返回类型不是签名的一部分
请注意,形参的名称也不是签名的一部分
例
long AddVaules(int a, int b){
return a + b;
}
long AddVaules(int a, int b, int c) {
return a + b + c;
}
long AddVaules(float f, float g) {
return (long) (f + g);
}
命名参数
只要显示指定参数的名字,就可以以任意顺序在方法调用中列出实参
和上面的参数不同,形参无需与每个实参位置一一对应
使用规则
- 方法声明和普通参数一样
- 不过在调用方法的时候,形参的名字后面跟着冒号和实际的参数值或表达式
语法
c.Calc(a:2, b:4, c:3);
例
class MyClass
{
public int Calc(int a, int b, int c)
{
Console.WriteLine($"a: {a}, b:{b}, c:{c}");
return (a + b) * c;
}
static void Main(string[] args)
{
MyClass mc = new MyClass();
int resule = mc.Calc(c: 2, a: 4, b: 6);
Console.ReadKey();
}
}
结果
a: 4, b:6, c:2
分析
可以看到,调用方法的实参与函数形参位置并不一一对应,但是实参还是一一对应赋值给形参了
注意
如果位置参数和命名参数一起使用,位置参数必须先列出
可选参数
在调用方法的时候包括这个参数,也可以省略他
使用规则
在方法声明的时候,为参数提供默认值
语法
int Calc(int a, int b = 3) {
....
}
实例
class MyClass
{
public int Calc(int a, int b = 3)
{
return a + b;
}
static void Main(string[] args)
{
MyClass mc = new MyClass();
int r0 = mc.Calc(5, 6);
int r1 = mc.Calc(5);
Console.WriteLine($"r0:{r0},r1:{r1} ");
Console.ReadKey();
}
}
结果
r0:11,r1:8
分析
可以看到r1,只给了一个参数,方法里计算的时候,自动把参数b的默认值计算进去
总结
- 不是所有的参数类型都可以作为可选参数。如下,可以看到合适使用可选参数
- 只要值类型的默认值在编译的时候可以确定,就可以使用值类型作为可选参数。
- 只有在默认值是null的时候,引用类型才可以作为可选参数来使用。
\ | 值 | ref | out | params |
---|---|---|---|---|
值类型 | 是 | 否 | 否 | 否 |
引用类型 | 只允许null的默认值 | 否 | 否 | 否 |
- 所有必填参数必须在可选参数声明之前声明。如果有params参数,必须在可选参数之后声明
(int x, deimal y,....int op1 = 17, double op2 = 36,...., params int [] intVals)