C#学习笔记 -- 方法的参数
1、值参数
当你使用值参数, 通过将实参的值复制到形参的方式把数据传递给方法,方法被调用时, 系统执行如下操作
-
在栈中为形参分配空间
-
将实参的值复制给形参
class MyClass
{
public int Val = 20;
}
class Program
{
static void MyMethod(Myclass f1, int f2)
{
f1.Val = f1.Val + 5;
f2 = f2 + 5;
CW.(f1.Val, f2);
}
static void Main(string[] args)
{
MyClass a1 = new MyClass();
int a2 = 10;
MyMethod(a1, a2);
CW.(a1.Val, a2);// 25 15 25 10
}
}
-
a1传入方法后, a1复制变成f1, 但f1引用仍然指向堆中的对象, 修改了a1.Val, 所以a1.Val f1.Val都是15
-
a2传入方法后, 复制一份a2 -> f2给方法去执行, 此时a2, f2都在栈里, 方法里修改f2的值, 修改完f2, f2就, a2没修改, 所以a2还是为10, f2为15
2、引用参数
特点
-
不会在栈上为形参分配内存
-
形参的参数名将作为实参变量的别名, 指向相同的内存位置
使用注意
-
使用引用参数时, 必须在方法的声明和调用中都使用ref修饰符
-
实参必须是变量, 在用做实参前必须被赋值, 如果是引用类型变量, 可以赋值为一个引用或null
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;
CW.(f1.Val, f2);
}
static void Main(string[] args)
{
MyClass a1 = new MyClass();
int a2 = 10;
MyMethod(a1, a2);
CW.(a1.Val, a2);// 25 15 25 15
}
}
-
使用ref关键字, 进入方法a1不复制, 直接给f1, f1指向堆中的对象,
-
使用ref关键字, 进入方法a2不复制, f2就是a2, 在栈里是同一个元素, 所以直接是修改的a2
-
方法执行之后, 形参的名称已经失效, 但是值类型a2的值和引用类型a1所指向的对象的值都被方法内的行为改变了
(1)引用类型作为值参数和引用参数
对于一个引用类型对象, 不管是将其作为值参数传递还是作为引用参数传递, 都可以在方法成员内部修改它的成员
A.将引用类型对象作为值参数传递
-
如果在方法内创建一个新对象并赋值给形参, 将切断形参与实参之间的关联, 并且在方法调用结束后, 新对象也将不复存在
class MyClass
{
public int Val = 20;
}
class Program
{
static void RefAsParameter(MyClass f1)
{
f1.Val = 50;
Console.WriteLine(f1.Val);
f1 = new MyClass();
Console.WriteLine(f1.Val);
}
static void Main(string[] args)
{
MyClass a1 = new MyClass();
Console.WriteLine(a1.Val);
RefAsParameter(a1);
Console.WriteLine(a1.Val);
//20 50 20 50
}
}
-
一开始new出来的a1.Val = 20,第一个Main方法中输出20
-
进入RefAsParameter方法,
-
f1是a1的复制品, 指向同一个堆中的对象, 修改值为50, 此时修改f1就是修改a1第二个输出50
-
新new了一个对象, 这个对象和之前那个对象没关系, 所以这里的val还是类初始化的20, 第三个输出20
-
-
因为在方法中已经把堆中的a1.Val修改为50, 所以第四个输出50
B.将引用类型对象作为引用参数传递
-
如果在方法内创建一个新对象并赋值给形参, 在方法结束后该对象依然存在, 并且是实参所引用的值
class MyClass
{
public int Val = 20;
}
class Program
{
static void RefAsParameter(ref MyClass f1)
{
f1.Val = 50;
Console.WriteLine(f1.Val);
f1 = new MyClass();
Console.WriteLine(f1.Val);
}
static void Main(string[] args)
{
MyClass a1 = new MyClass();
Console.WriteLine(a1.Val);
RefAsParameter(ref a1);
Console.WriteLine(a1.Val);
//20 50 20 20
}
}
-
在方法调用时, 形参和实参都指向堆中相同的对象
-
对成员值修改会同时影响到形参和实参
-
当方法创建新的对象并赋值给形参时, 形参和实参的引用都指向该新对象
-
在方法结束后, 实参指向方法内创建的新对象
3、输出参数
输出参数用于方法体内把数据传出到调用代码, 他们的行为与引用参数类似. 要求如下
-
必须在声明和调用中都使用out参数
-
实参必须是变量, 不能是其他类型的表达式, 因为方法需要内存位置来保存返回值
void Method(out int val)
{
...
}
int y = 1;
Method(out y);
输出参数的形参充当实参的别名. 形参和实参都是同一块内存的名称, 在方法内堆形参做的任何改变在方法执行完成后, 都是可见的, 要求如下
-
因为方法内的代码在读取输出参数之前必须对其写入, 所以不可能使用输出参数把数据传入方法
public void Add(out int outValue) { int var1 = outValue + 2;;//这样是错误的, 在方法赋值之前,无法读取输出变量 }
-
在方法内部, 给输出参数复制后才能读取他, 参数的初始值是无关的, 而且没有必要在方法调用之前为实参赋值
-
在方法内部, 方法返回之前, 代码中每条可能的路径和都必须为所有输出参数赋值
-
对于输出参数, 形参就好像是实参的别名一样, 但是还有一个要求, 那就是它必须在方法内进行赋值
class MyClass
{
public int Val = 20;
}
class Program
{
static void MyMethod(out MyClass, out int f2)
{
f1 = new MyClass();
f1.Val = 25;
f2 = 15;
}
static void Main(string[] args)
{
MyClass a1 = null;
int a2;
Mymethod(out a1, out a2);
}
}
-
在方法调用之前, 将要被用作实参的变量a1, a2已经在栈中
-
在方法的开始, 形参的名称被设置为实参的别名, 可以认为变量a1和f1指向的是相同内存位置, a2和f2也是相同位置. a1和a2不在作用域之内, 所以不能在MyMethod中访问
-
方法执行之后, 形参的名称已经失效, 但是引用类型的a1和值类型的a2的值都被方法内的行为改变了
从C#7.0开始, 不再需要预先声明一个变量来用作out参数. 可以在调用方法时在参数列表中添加一个变量类型, 他将作为变量声明
static void Main()
{
MyMethod(out MyClass a1, out int a2);
a2 += 5;
CW(a2); //20
}
4、参数数组
-
作用: 允许多个特定类型的0个或多个实参对于一个特定的形参
-
在一个参数列表中, 只能有一个参数数组
-
如果有, 他必须是列表的最后一个
-
由参数数组表示的所有参数必须是同一类型
声明参数数组需要注意
-
在数据类型前使用params修饰符
-
在数据类型后放置一组空的方括号
-
在如下示例中, 形参intVals可以代表0个或多个int实参
void ListInts(param int[] intVals)
{
...
}
(1)方法调用
为参数数组提供实参
-
使用逗号分隔列表, 所有元素必须是声明方法头中的类型
ListInts(1, 2, 3);
-
使用一个该数据类型元素的一维数组
int[] intArray = new int[]{1, 2, 3}; ListInts(int Array);
-
在调用时, 无需加params修饰符
延伸式
方法调用的第一种形式被称为延伸式, 这种形式在调用中是哟个独立的实参
void ListInts(params int[] intVals)
{
...
}
ListInts();
ListInts(1, 2, 3);
ListInts(4, 5, 6, 7);
ListInts(8, 9 ,10 ,11 ,12);
在使用一个为参数数组使用独立实参的调用时, 编译器将
-
接受实参列表, 用实参列表在堆中创建并初始化一个数组
-
把数组的引用保存到栈中的形参里
-
如果在对应形参数组的位置没有实参, 编译器会创建一个有0个元素的数组来使用
class MyClass
{
public void ListInts(params int[] inVals)
{
if(inVals != null) && (inVals.length != 0)
{
for(int i = 0; i < intvals.length; i++)
{
intVals[i] *= 10;
Console.WriteLine("${inVal[i]}");
}
}
}
}
static void Main(string[] args)
{
MyClass myClass = new MyClass();
int first = 5;
int second = 6;
int third = 7;
myclass.ListInts(first, second, third);
Console.WriteLine("${first}, {second}, {third}");
//50 60 70
//5, 6, 7
}
(2)将数组作为实参
static void Main(string[] args)
{
MyClass myClass = new MyClass();
int[] myArr = new int[]{5, 6, 7};
myClass.ListInts(myArr);
foreach(int var in myArr)
{
Console.WriteLine("${var}");
}
//50 60 70
//50 60 70
}
数组的元素在堆内被更改了
5、参数类型总结
参数类型 | 修饰符 | 是否在声明时使用 | 是否在调用时使用 | 执行 |
---|---|---|---|---|
值参数 | 无 | - | - | 系统把实参的值复制到形参 |
引用参数 | ref | 是 | 是 | 形参是实参的别名 |
输出参数 | out | 是 | 是 | 进包含一个返回的值, 形参是实参的别名 |
数组参数 | params | 是 | 否 | 允许传递可变数目的实参到方法 |
12、ref局部变量和ref返回
前面章节中ref关键字传递一个对象引用给方法调用, 在调用上下文中, 对象的任何改动方法返回后依旧可见.
ref返回功能则恰好相反, 他允许将一个引用发送到方法外, 然后在调用上下文内使用这个引用, 一个相关的功能是ref局部变量, 他允许一个变量是另一个变量的别名
(1)ref局部变量
-
你可以使用它创建一个变量的别名, 即使引用的对象是值类型
-
对任意一个变量的赋值都会反应到另一个变量上, 因为他们是引用同一个对象, 即使是值类型
-
创建别名的语法需要使用关键字ref两次, 一次是在别名声明的类型前面, 另一次是在赋值运算符的右边, 被别名的变量前面
ref int y = ref x;
ref int 别名 = ref 被别名;
class Program
{
static void Main(string[] args)
{
int x = 2;
ref int y = ref x;
Console.WriteLine($"x = {x}, y = {y}");
x = 5;
Console.WriteLine($"x = {x}, y = {y}");
y = 6;
Console.WriteLine($"x = {x}, y = {y}");
//2 2 5 5 6 6
}
}
(2)ref返回
ref最常与ref返回功能一起使用. ref返回功能提供了一种使方法返回变量引用而不是变量值的方法, 这里需要的额外语法也使用了ref关键字两次
ref int var = ref Method(ref param);
-
一次是在方法的返回类型声明之前
-
另一次在return关键字之后, 被返回对象的变量名之前
class Simple
{
private int Score = 5;
public ref int RefToValue()
{
return ref Score;
}
public void Display()
{
Console.WriteLine($"{Score}");
}
}
class Program
{
static void Main(string[] args)
{
Simple s = new Simple();
s.Display();//5
//此时返回的是成员在内存上的引用地址, 而不单纯是值
ref int v1OutSide = ref s.RefToValue();
//在调用域外面修改值, 直接修改了成员的值
v1OutSide = 10;
s.Dsiplay();//5
}
}
Math库中的Max方法的变形, 提供两个数字类型的变量, Math.Max能够返回两个值中比较大的那个, 但是假设你想返回的是包含较大值的变量的引用, 可以用ref返回
class Program619
{
public static ref int Max(ref int p1, ref int p2)
{
if (p1 > p2)
{
return ref p1;
}
else
{
return ref p2;
}
}
}
class Program619Test
{
static void Main(string[] args)
{
int v1 = 10;
int v2 = 20;
Console.WriteLine("start");
Console.WriteLine($"v1 = {v1}, v2 = {v2}");// 10, 20
ref int max = ref Program619.Max(ref v1, ref v2);
Console.WriteLine("after method");
Console.WriteLine($"max = {max}");//20
max++;
Console.WriteLine("after increment");
Console.WriteLine($"v1 = {v1}, v2 = {v2}");// 10, 21
}
}
(3)ref的限制
-
不能将void方法声明为ref返回方法
-
ref return 不能返回如下内容
-
空值
-
常量
-
枚举成员
-
类或结构体属性
-
指向只读位置的指针
-
-
ref return 只能指向原先在调用域内的位置, 或者字段, 所以它不能指向方法的局部变量
-
ref局部变量只能被赋值一次, 一旦初始化, 他就不能指向不同的存储位置了
-
即使将一个方法声明为ref返回方法, 如果在调用该方法时省略了ref关键字, 则返回的将是值, 而不是指向值的内存位置的指针
-
如果将ref局部变量作为常规的实际参数传递给其他方法, 则该方法仅获取该变量的一个副本. 尽管ref局部变量包含指向存储位置的指针, 但是当以这种方式使用时, 他会传递值而不是引用