值参数
值参数是使用最多的参数。
声明时不带任何修饰符
的形参就是值参数(形式参数)。值形参对应一个局部变量,它的初始化值来自调用方法时提供的实参(实际参数)。方法内部对形参的改变不会影响方法外部的实参,言外之意就是形参是方法内的一个局部变量,方法调用时形参拷贝了传入的实参的值,此后,形参和实参再无半分瓜葛。
class Program
{
static void Main(string[] args)
{
int y = 100;
Program.PrintAdd(y); // 输出 101
Console.WriteLine(y); // 输出100
}
static void PrintAdd(int x)
{
x = x + 1;
Console.WriteLine(x);
}
}
值参数是引用数据类型
当值参数是引用数据类型时,形参拷贝的是实参的引用,但本质上实参和形参还是两个不同的变量。不同的点在于:
基本数据类型
存的直接是基本数据的值,如int类型存的就是整数。引用数据类型
存的是被引用对象的地址,如类的对象的变量存的就是对象的地址。
容易搞混的一点是:
当传入一个对象给一个方法时,方法内通过该对象对其字段进行修改后,方法外部的对象的对应字段会被修改,这可能会被一些人认为此时不是值参数。其实还是值参数,形参拷贝实参的值,只是这个值在引用数据类型中它是地址。拷贝之后,形参和实参各自保存的值是同一个地址,此时在方法内部可以通过该形参对该地址进行访问。如果这时使用形参.字段
(字段类型可以是基本数据类型和引用数据类型)的方式修改某一字段的值,修改的是形参指向的地址存的对象的字段,并没有修改形参本身,因为形参保存的只是被引用对象的地址。只有形参 = 新对象
的方式才是修改形参本身。形参 = 新对象
并不会导致实参 == 新对象
,以此,还是符合值参数的定义。
class Student
{
public string name;
public int age;
}
class Program
{
static void Main(string[] args)
{
Student stu = new Student();
stu.name = "yy";
stu.age = 100;
Program.Test(stu);
// 到这里stu的字段的值分别是:
name = "xx";
age = 100;
// 新旧hash code 是不一样的
Console.WriteLine("hash code: {0}", stu.GetHashCode());
}
static void Test(Student stu)
{
// 通过形参stu访问了一个Student对象的字段name,并修改之,此修改外部能感知
stu.name = "xx";
stu = new Student(); // 形参新赋值了一个新对象,外部实参依旧是旧的对象
// 这两次赋值是对新对象的字段的赋值,并不会对
// 旧对象的字段造成影响
stu.name = "bb";
stu.age = 20;
Console.WriteLine("hash code: {0}", stu.GetHashCode());
}
}
引用参数
用ref
修饰符声明的形参就是引用参数
。与值参数不同的是,引用参数并不创建新的存储位置。也就是说,引用参数的实参和形参引用的就是同一个存储位置。
class Program
{
static void Main(string[] args)
{
double a = 20;
Add(ref a); // a == 21
Console.WriteLine(a); // a == 21
}
static void Add(ref int a)
{
a = a + 1;
Console.WriteLine(a);
}
}
输出参数
用out
修饰符声明的形参就是输出参数
。与引用参数类似,输出参数不创建新的存储位置,输出参数的形参的存储位置恰恰就是实参的存储位置,即输出参数的形参
和实参
完全就是同一个变量。
输出参数的设计就是希望从方法的内部得到值。输出参数的初值会在方法内部被覆盖,因此初值没有意义,无需赋初值。可以把输出参数理解成额外的函数返回值。
class Program
{
static void Main(string[] args)
{
double result;
// 调用时也显式地写上out,让人一看就知道是输出参数
bool b = TryAdd("1", "100", out result);
if (b)
Console.WriteLine("{0} + {1} = {2}", "1", "100", result);
}
// a和b能被解析成double并相加成功的话才返回true
static bool TryAdd(string a, string b, out double res)
{
try
{
res = double.Parse(a) + double.Parse(b);
return true;
} catch {
res = 0;
return false;
}
}
}
引用参数 VS 输出参数
关键字out
和关键字ref
是等效的
,就是说,无论使用哪个关键字,都会生成相同的元数据和IL代码。
但是,C#编译器将两者区别对待。这两个关键字的区别在于谁负责初始化
。
out
: 调用者不希望在调用方法之前初始化对象,被调用的方法不能读取对象的值,而且被调用的方法必须在返回之前对其赋值。ref
: 调用者必须在调用方法之前首先初始化参数的值,被调用的方法可以读取参数或为参数赋值。
数组参数
用params
修饰符声明的形参就是数组参数
。
// 普通的数组参数声明
void Method(int[] arr){...}
// 调用时需要先创建一个数组,然后再传入
int[] array = new int[]{1, 2, 3};
XXX.Method(array);
// 如果加上修饰符params就可以直接在调用方法时顺序输入值
// 声明方法
void Method(params int[] arr){...}
// 调用
XXX.Method(1, 2, 3);
// 可以直接输入而无需创建数组
注意:一个方法的参数列表只能有0个或1个params
参数,并且params
参数只能在参数列表的最后。
可选参数
声明方法时给参数提供默认值的就是可选参数
。参数因为具有默认值而变得可选
。
...
// 声明一个参数有默认值的方法
public void Method(string name = "李四", int age = 20){...}
...
// 因为Method方法有默认值,所以参数是可选的
// 不传入实参,形参默认使用的是默认值
XXX.Method(); // name = "李四", age = 20
具名参数调用
具名参数调用可以算是一种编写代码的习惯,一般来说,我们调用函数时只填写参数的值(或者变量的名字),如XXX.Method(100, "你好");
,这算是匿名调用。而具名调用要求把参数的名称也显式的写出来。例如:
...
// 方法
public void Method(int id, string name){...}
...
// 匿名调用
XXX.Method(1, "张三");
// 具名调用
XXX.Method(id:1, name:"张三");
// 具名调用可以忽略参数列表的顺序
XXX.Method(name:"张三", id:1); // 这样写等同于上面
this参数(扩展方法)
扩展方法又称this参数,可以对一个不能修改源码,但又需要增加方法的类进行增加方法。
方法必须是public staic修饰的。
下面的例子是对string类增加了ToInt方法:
class Pragram
{
static void Main(string[] args)
{
string a = "100";
// 因为input参数就是this,会
// 自动传入a,所以不用显式传入
// 参数
int b = a.ToInt();
}
}
// 声明一个静态类,这是string的扩展类
static class StringExtension
{
// 对string扩展了ToInt方法
// 该方法可以将字符串转成int
// this修饰的必须是第一个参数
public static int ToInt(this string input)
{
int res;
try
{
res = int.Parse(input);
}
catch
{
res = -1;
}
return res;
}
}