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局部变量包含指向存储位置的指针, 但是当以这种方式使用时, 他会传递值而不是引用
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律