要学会各种参数,重点是理解参数参数传递的内存原理。理解了内存,各种参数使用的作用、效果等便可以自己分析出来,而不用死记硬背。
1.按值传递参数(值参数)
值参数是最常见的一种参数,也很好判断。在方法声明与调用时,参数前不加ref或out关键字的参数,便是按值传递。
值参数是将实参变量在“栈”中存储的值复制一份副本,将副本传递给方法的形参。
按值传递参数又分为两种不同情况:传递值类型,与传递引用类型。
(注意这里的值类型、引用类型是数据类型,而值参数、引用参数是参数传递方式。二者不是同一概念,不可混淆。)
(1)传递值类型参数
示例:
1 class Program 2 { 3 static void Main(String[] args) 4 { 5 int a = 4; 6 int b = 5; 7 Add(a, b); 8 Console.WriteLine($"a={a},b={b}"); 9 } 10 public static void Add(int x, int y) 11 { 12 x += 1; 13 y += 1; 14 } 15 }
输出为:
a=4,b=5
内存原理:在Add方法中对形参x与y的操作,并不会对实参a与b的值产生影响,因为在Add方法中操作的是a与b的副本。
(2)传递引用类型参数
示例1:
1 class Program 2 { 3 static void Main(String[] args) 4 { 5 Animal a = new Animal() { Age = 1 }; 6 Add(a); 7 Console.WriteLine($"a.Age={a.Age}"); 8 } 9 public static void Add(Animal animal) 10 { 11 animal.Age += 1; 12 } 13 } 14 class Animal 15 { 16 public int Age { get; set; } 17 }
输出为:
a.Age=2
内存原理:
在Add方法中形参animal拿到了实参a在栈中的副本,副本中存储着对象的地址。所以,animal与a指向了同一个对象。
这时操作的不是副本,而是副本所指的对象,即操作a所指对象。
示例2:
1 class Program 2 { 3 static void Main(String[] args) 4 { 5 Animal a = new Animal() { Age = 1 }; 6 Add(a); 7 Console.WriteLine($"a.Age={a.Age}"); 8 } 9 public static void Add(Animal animal) 10 { 11 animal = new Animal() { Age = 10 }; 12 animal.Age += 1; 13 } 14 } 15 class Animal 16 { 17 public int Age { get; set; } 18 }
输出为:
a.Age=1
内存原理:
与示例1不同的是,在示例2中Add方法中改变了animal所指向的对象,指向一个new的Animal对象,此时a仍指向原对象,这时再对animal进行操作,
便不再会影响实参a所指的对象。
2.按引用传递参数(引用参数)
形参与实参之前,都要加ref关键字。
需要注意的是,实参在使用之前必须赋值,否则编译器会报错。(如下图)
此时不再复制实参在栈中的副本,而是将实参在栈中的地址传给形参,也就是实参与形参共用栈中的值。此时在方法中对形参所做的任何操作,都会影响实参。
按引用传递也分为值类型和引用类型两种情况。
(1)传递值类型参数
示例:
1 class Program 2 { 3 static void Main(String[] args) 4 { 5 int a = 4; 6 int b = 5; 7 Add(ref a, ref b); 8 Console.WriteLine($"a={a},b={b}"); 9 } 10 public static void Add(ref int x, ref int y) 11 { 12 x += 1; 13 y += 1; 14 } 15 }
输出为:
a=5,b=6
内存原理:
由于a与x,b与y指向了栈中相同的地址,在Add方法中对形参x与y的操作,会影响实参a与b的值。
(2)传递引用类型参数
示例1:
1 class Program 2 { 3 static void Main(String[] args) 4 { 5 Animal a = new Animal() { Age = 1 }; 6 Add(ref a); 7 Console.WriteLine($"a.Age={a.Age}"); 8 } 9 public static void Add(ref Animal animal) 10 { 11 animal.Age += 1; 12 } 13 } 14 class Animal 15 { 16 public int Age { get; set; } 17 }
输出为:
a.Age=2
内存原理:
这时得到的效果,与按值传递参数是一样的,但内存原理却不相同。
这时Add方法中形参animal指向的不再是实参a在栈中的副本的地址,而是与a指向了栈中相同的位置。
示例2:
1 class Program 2 { 3 static void Main(String[] args) 4 { 5 Animal a = new Animal() { Age = 1 }; 6 Add(ref a); 7 Console.WriteLine($"a.Age={a.Age}"); 8 } 9 public static void Add(ref Animal animal) 10 { 11 animal = new Animal() { Age = 10 }; 12 animal.Age += 1; 13 } 14 } 15 class Animal 16 { 17 public int Age { get; set; } 18 }
输出为:
a.Age=11
内存原理:
这个示例便可以看出与按值传递时示例2的不同。
Add方法中改变了animal所指向的对象,指向一个new的Animal对象,此时a所指向的对象也随之改变。这是由于形参animal
与实参a指向的是栈中相同的地址(这个地址中存储的是对象在堆中的地址)。当栈中存储的堆地址改变时,animal与a会同时指向新的对象。
3.输出参数
用法:
(1)形参与实参之前,都要加out关键字。
(2)输出参数主要是用于函数需要有多个返回值时,作为返回值使用。
与引用变量相同的是:
(1)输出参数也不复制实参在栈中的副本,而是将实参在栈中的地址传给形参。在这点上,输出参数与引用参数相同。
(2)实参必须是可以赋值的变量,而不能是常亮。
与引用参数不同的是:
(1)实参在使用之前不必赋值。
(2)事实上,在使用之前对实参的赋值没有任何意义,因为在调用方法的最开始,便会将其值抹去,使其成为未赋值的变量。
(3)在方法返回之前,必须对out参数进行赋值。
由以上特点所决定的是,输出参数无法向方法中传递信息,其唯一作用便是,当一个参数需要返回多个值时,作为返回值返回。
示例1:
1 class Program 2 { 3 static void Main(String[] args) 4 { 5 int a; 6 Add(1, 2, out a); 7 Console.WriteLine(a); 8 } 9 public static bool Add(int x, int y, out int result) 10 { 11 result = x + y; 12 return true; 13 } 14 }
输出为:
3
引用类型的输出参数同理,请自行编写示例代码。
请继续阅读参数学习的下篇:C#基础之参数(二) 数组参数、可选参数与命名参数