TOC

This article is currently in the process of being translated into Chinese (~96% done).

类 :

Method parameters

我们之前提到过方法,也简单介绍了方法/函数参数的概念。本文中我们将从各个方面深入探讨这一主题。前面已说过,方法可以没有参数,不过方法通常都有一个或多个参数,以便实现其功能。

在之前的文章中我们已看过一个非常简单的参数应用场景:AddNumbers()方法使用了两个数字作为参数,并返回这两个数字的和。

public int AddNumbers(int number1, int number2)
{
	return number1 + number2;
}

这更进一步展示了方法这一概念多么的巧妙,由于应用了这一概念,你可以把所需功能封装在一个方法中,但通过带参数的调用又能响返回值。

AddNumbers(2, 3);

Result: 5

AddNumbers(38, 4);

Result: 42

这就是基本的参数类型,不过让我们更多讨论下可以用来改变参数行为的各类修饰符和选项。

请注意本文真正深入到了各种类型的参数和它们的应用,如果你才开始学习C#,只想看看某些效果,接下来的内容对于你来说可能会有点复杂,过于技术性了。不防跳过以下内容,等准备好后再回来。

可选参数

调用带参数的方法时,默认必需为所有的参数赋值。不过某些情况下,可能需要让某个或几个参数可选。对某些编程语言,你或许可以简单地把该参数标记为可选,但在C#中,要在方法声明中给参数赋一个默认值来使其可选。其实这种做法也很不错,因为你就不再需要为处理调用方没有提供此参数值的情况而额外编码了。

下面是一个带可选参数的方法的例子:

public int AddNumbers(int number1, int number2, int number3 = 0)
{
	return number1 + number2 + number3;
}

最后一个参数(number3)是可选的,因为它已被赋了一个默认值(0)。调用这个方法时,使用两个或三个参数都可能,如下所示:

public void Main(string[] args)
{
	AddNumbers(38, 4);
	AddNumbers(36, 4, 2);
}

可选参数可以有多个 - 其实,方法可以只含可选参数。只需记住在方法定义中,可选参数必需位于最后 - 不允许位于首位或非可选参数之间。

params修饰符

如果要定义多个可选参数,还一种方法,使用params修饰符来允许任意数量的参数。如下所示:

public void GreetPersons(params string[] names) { }

然后就可以象这样调用了:

GreetPersons("John", "Jane", "Tarzan");

使用params修饰符进行定义的方式有个额外的好处,也允许传递0个参数。带params参数的方法也可以有常规参数,只要保证由params修饰符定义的参数在最后即可。此外,每个方法只允许有一个参数用params修饰符定义。以下是在GreetPersons()方法中用params修饰符定义输出不同数量名字的完整示例:

public void Main(string[] args)
{
	GreetPersons("John", "Jane", "Tarzan");
}

public void GreetPersons(params string[] names)
{
	foreach(string name in names)
		Console.WriteLine("Hello " + name);
}

参数类型:值传递和引用传递

C#与其它编程语言一样,区分两种参数类型:“值传递”“引用传递”。C#中的默认类型是“值传递”,意思就是当调用某方法传递一个变量时,传递的实际是该变量对象的一个拷贝,而不是该变量的引用。这也表示可以在方法内部改变该参数,而不会影响作为参数进行传递的那个对象。

用一个例子很容易就能展示这种情况:

public void Main(string[] args)
{
	int number = 20;
	AddFive(number);
	Console.WriteLine(number);
}

public void AddFive(int number)
{
	number = number + 5;
}

一个很基本的方法AddFive(),功能是把传入的数字加5。在Main()方法中,我们定义了一个变量number,值为20,然后调用AddFive()方法。当下一行代码输出number变量值时,你可能期待会是25,然而该值保持了为20。为什么呢?由于参数默认是作为一个原始对象的拷贝(值传递)被传入的,因此当AddFive()方法针对此参数进行操作时,是在对一份拷贝进行操作,从而永远也不会影响到原始变量。

有两种方法可以改变这种行为,让AddFive()方法可以改变原始变量值:使用ref 修饰符in/out修饰符

ref修饰符

ref修饰符是“引用(reference)”一词的简写,其作用就是把参数的行为从默认的“值传递”改为“引用传递”,意思就是传入原始变量的一个引用而不是其值的一份拷贝。以下是改变后的示例:

public void Main(string[] args)
{
	int number = 20;
	AddFive(ref number);
	Console.WriteLine(number);
}

public void AddFive(ref int number)
{
	number = number + 5;
}

注意有两个地方都加上了“ref”关键字:在方法定义中和在方法调用传入参数时。通过此改变,就得到之前预期过的行为了 - 现的的输出结果为25而不是20了,因为ref修饰符允许此方法针对作为参数传入的原始值进行操作,而不是对其拷贝进行操作了。

out修饰符

ref修饰符一样,out修饰符确保参数通过引用传递,而不是值传递,不过有一点重要的区别:当使用ref修饰符时,传入的是一个初始化过的值,方法内部可能也可能不会对其进行改变。而使用out修饰符时,必需在方法内部对该参数进行初始化。这也表示使用out修饰符时可以传入未初始化的值 - 如果方法内部未对某个out参数赋值,编译器会报错。

在C#中,方法只能返回一个值,不过通过使用out修饰符就可以绕过此限制,传入任意数量带out修饰符的参数 - 方法调用完成时,所有参数就已经被赋值了。以下示例中,先传入两个数字,然后使用两个带out修饰符的参数同时返回这两数字相加和相减的结果。

public void Main(string[] args)
{
	int addedValue, subtractedValue;
	DoMath(10, 5, out addedValue, out subtractedValue);
	Console.WriteLine(addedValue);
	Console.WriteLine(subtractedValue);
}

public void DoMath(int number1, int number2, out int addedValue, out int subtractedValue)
{
	addedValue = number1 + number2;
	subtractedValue = number1 - number2;
}
Output:

15
5

上述示例先定义了两个变量(addedValuesubtractedValue),然后再把它们作为out参数进行传递。这是旧版C#语言的要求,从C#版本6之后,就可以在方法调用时直接定义这些变量了,如下所示:

DoMath(10, 5, out int addedValue, out int subtractedValue);
Console.WriteLine(addedValue);
Console.WriteLine(subtractedValue);

in修饰符

out修饰符一样,in修饰符确保参数传递的是引用,而不一份拷贝,但与out修饰符不同,in修饰符禁止在方法内部对该变量进行任何改变。

你可能觉得:如果不能改变参数值,那不如就传递常规参数得了,反正即使改变了也不会影响原始变量。确实,结果看似一样的,但是仍然有充分的理由使用in修饰符:通过引用传递,而不是值传递,可以节省资源,因为这样系统就不需要象传递常规参数那样花时间来生成原始对象的拷贝了。

多数情况下,这样做区别并不很大,因为多数参数都是简单的字符串或整数,但是当需要在某个循环中重复地调用同一方法很多次或是需要传递很大的值,如非常巨大的字符串或结构体时,这样做就可能显著提升程序的执行速度。

以下是一个使用in修饰符的示例:

public void Main(string[] args)
{
	string aVeryLargeString = "Lots of text...";
	InMethod(aVeryLargeString);
}

public void InMethod(in string largeString)
{
	Console.WriteLine(largeString);
}

在方法InMethod()中largeString参数是一个指向原始变量(aVeryLargeString)的只读引用,因此可用使用它,但是不要改变它的值。如果试图这样做,编译器就会报错:

public void InMethod(in string largeString)
{
	largeString = "We can't do this...";
}

Error: Cannot assign to variable 'in string' because it is a readonly variable

命名参数

上述所有的示例中,在方法定义时每个参数都有个唯一的名称。在方法内部可以使用此名称引用该参数,但是在调用方法时却不需要这些名称 - 只需按其定义的顺序依次提供参数值。这样对于只有2,3个参数的简单方法来说不是问题,但有些方法很复杂,需要很多参数。此时就可能很难识别出方法调用时所用的变量值对应那个参数了,象下述例子一样:

PrintUserDetails(1, "John", 42, null);

在此方法调用中不同参数的意义就不是很清晰,但是如果看一下方法定义,那就很清楚了:

public void PrintUserDetails(int userId, string name, int age = -1, List<string> addressLines = null)
{
	// Print details...
}

不过如果需要时不时地查看方法定义才能清楚每个参数是做什么的,肯定很麻烦,因此对于复杂的方法,可以在方法调用时直接加上参数名称。这样做还允许以任何顺序使用参数名称,而不必使用其定义顺序。以下是一个例子:

PrintUserDetails(name: "John Doe", userId: 1);

这样做的另一个好处是,可以只传递任一可选参数值,而不用传递定义在此参数之前的所有可选参数。也就是说,在上述例子中,如果要传递addressLines参数值,就得也传递age参数值,因为其定义顺序居前。但是如果使用命名参数,就不用管顺序了,只需传递全部非可选参数及任意可选参数,如下所示:

PrintUserDetails(addressLines: new List<string>() { }, name: "Jane Doe", userId: 2);

以下是使用命令参数的完整示例:

public void Main(string[] args)
{
	PrintUserDetails(1, "John", 42, null);
	PrintUserDetails(name: "John Doe", userId: 1);
	PrintUserDetails(addressLines: new List<string>() { }, name: "Jane Doe", userId: 2);
}

public void PrintUserDetails(int userId, string name, int age = -1, List<string> addressLines = null)
{
	// Print details...
	Console.WriteLine(name + " is " + age + " years old...");
}

总结

本文介绍了参数的各种形式及类型。幸好常用的都只是简单的,大家熟知常规参数,但是只要开始深入了解C#语言,就会从学习本文中介绍的所有类型和修饰符中受益。


This article has been fully translated into the following languages: Is your preferred language not on the list? Click here to help us translate this article into your language!