我们之前一直是通过方法的return语句来返回数据。本节描述了如何通过方法参数来返回数据(而且方法参数的数量是可变的)。
初学者主题:匹配调用者变量与参数名 在前面一些代码清单中,我们故意使调用者中的变量名与目标方法中的参数名相匹配。这种匹配纯粹是为了增强可读性,名称是否匹配与方法调用的行为是完全没有关系的。 |
4.5.1 值参数
参数默认是传值(pass by value)的。换言之,变量的栈数据会完整地复制到目标参数中。例如在代码清单4-11中,在调用Combine()的时候,Main()使用的每个变量都会复制到Combine()方法的参数中。输出4-5展示了这个代码清单的结果。
代码清单4-11 以传值方式来传递变量
class Program
{
static void Main()
{
// ...
string fullName;
string driveLetter = "C:";
string folderPath = "Data";
string fileName = "index.html";
fullName = Combine(driveLetter, folderPath, fileName);
Console.WriteLine(fullName);
// ...
}
static string Combine(
string driveLetter, string folderPath, string fileName)
{
string path;
path = string.Format("{1}{0}{2}{0}{3}",
System.IO.Path.DirectorySeparatorChar,
driveLetter, folderPath, fileName);
return path;
}
}
输出4-5
C:\Data\index.html
Combine()方法返回之前,即使将null值赋给driveLetter、folderPath和fileName等变量,Main()中对应的变量仍会保持它们的初始值不变,因为在调用一个方法时,只是将变量的值复制了一份给方法。调用栈在一次调用的末尾“展开”的时候,当初复制的那个副本会被丢弃。
高级主题:引用类型与值类型的对比 出于本节的目的,传递的参数是值类型还是引用类型并不重要。重要的是目标方法是否能将一个新值赋给调用者的初始变量。由于会生成一个副本,所以不可能重新对调用者的副本进行赋值。 更具体地说,引用类型的变量包含实际数据所在的内存地址。假如以传值方式来传递一个引用类型的变量,地址会从调用者复制给方法参数。其结果就是,目标方法不能更新调用者变量的地址值。另一方面,假如方法参数是一个值类型,值本身会复制到参数中,更改参数不影响调用者的原始变量。 |
4.5.2 引用参数(ref)
来看看代码清单4-12的例子,它调用一个方法来交换两个值,输出4-6展示了结果。
代码清单4-12 以传引用的方式来传递变量
class Program
{
static void Main()
{
// ...
string first = "first";
string second = "second";
Swap(ref first, ref second);
System.Console.WriteLine(
@"first = " "{0}"", second = ""{1}""",
first, second);
// ...
}
static void Swap(ref string first, ref string second)
{
string temp = first;
first = second;
second = temp;
}
}
输出4-6
first = "second", second = "first"
赋给first和second的值被成功地交换,即使Swap()方法无任何返回值。为此,我们要以传引用(pass by reference)的方式来传递变量。将这个例子的Swap()调用与代码清单4-11的Combine()调用进行比较。不难发现,二者最明显的区别就是本例在参数的数据类型之前使用了关键字ref。这个关键字使参数以传引用的方式进行传递,使被调用的方法可以用新值来更新原始调用者的变量。
如果调用方法将参数指定为ref,那么调用者在调用这个方法的时候,需要在准备传递的变量之前添加一个ref关键字。这样一来,调用者就显式地指定了目标方法可以对它接收到的任何ref参数进行重新赋值。除此之外,调用者应该对传引用的变量进行初始化,因为目标方法可能直接从ref参数读取数据而不先对它们进行赋值。例如在代码清单4-12中,我们一开始就将first的值赋给temp——假定通过first传递的变量已经由调用者进行了初始化。
4.5.3 输出参数(out)
除了将参数单向传入一个方法(传值),或者同时将参数传入和传出一个方法(传引用)之外,还可以将数据从一个方法内部单向传出方法。为此,代码需要使用关键字out来修饰参数类型。例如代码清单4-13的GetPhoneButton()方法,它能返回与一个字符对应的电话按键。
代码清单4-13 仅传出的变量
class ConvertToPhoneNumber
{
static int Main(string[] args)
{
char button;
if(args.Length == 0)
{
Console.WriteLine(
"ConvertToPhoneNumber.exe <phrase>");
Console.WriteLine(
"'_' indicates no standard phone button");
return 1;
}
foreach(string word in args)
{
foreach(char character in word)
{
if(GetPhoneButton(character, out button))
{
Console.Write(button);
}
else
{
Console.Write('_');
}
}
}
Console.WriteLine();
return 0;
}
static bool GetPhoneButton(char character, out char button)
{
bool success = true;
switch( char.ToLower(character) )
{
case '1':
button = '1';
break;
case '2': case 'a': case 'b': case 'c':
button = '2';
break;
case '3': case 'd': case 'e': case 'f':
button = '3';
break;
case '4': case 'g': case 'h': case 'i':
button = '4';
break;
case '5': case 'j': case 'k': case 'l':
button = '5';
break;
case '6': case 'm': case 'n': case 'o':
button = '6';
break;
case '7': case 'p': case 'q': case 'r': case 's':
button = '7';
break;
case '8': case 't': case 'u': case 'v':
button = '8';
break;
case '9': case 'w': case 'x': case 'y': case 'z':
button = '9';
break;
case '*':
button = '*';
break;
case '0':
button = '0';
break;
case '#':
button = '#';
break;
case ' ':
button = ' ';
break;
case '-':
button = '-';
break;
default:
// Set the button to indicate an invalid value
button = '_';
success = false;
break;
}
return success;
}
}
输出4-7展示了代码清单4-13的结果。
输出4-7
>ConvertToPhoneNumber.exe CSharpIsGood
274277474663
在这个例子中,假如能成功判断与一个字符对应的电话按键,GetPhoneButton()方法就返回true。方法还会使用被声明为out的button参数来返回对应的按钮。
如果一个参数被标记为out,编译器就会核实方法内的所有代码路径是否都设置了该参数。例如,假定在某个代码执行路径中,没有对button进行赋值,编译器就会报告一个错误,指出代码没有对button进行初始化。在代码清单4-13中,方法最后将"_"值赋给button,因为即使它无法判断正确的电话按键,也必须对button进行赋值。
4.5.4 参数数组(params)
在迄今为止讲到的例子中,参数数量都由方法声明进行了固定。然而,我们有时希望参数的数量是可变的。以代码清单4-11的Combine()方法为例。在那个方法中,传递了驱动器号、文件夹路径以及文件名(driveLetter、folderPath、fileName)等参数。假如路径中的文件夹数量大于1,调用者希望将额外的文件夹联结起来,以构成一个完整的路径,那么应该如何编写代码呢?也许最好的办法就是为文件夹传递一个字符串数组。然而,这会使调用代码变得稍微复杂一些,因为需要事先构造好一个数组,再将这个数组作为参数来传递。
为了简化编码,C#提供了一个特殊的关键字,它允许在调用一个方法时提供数量可变的参数,而不是由方法事先固定好参数的数量。在具体讨论这种方法声明之前,先观察一下代码清单4-14的Main()方法中的调用代码。
代码清单4-14 传递一个长度可变的参数列表
using System.IO;
class PathEx
{
static void Main()
{
string fullName;
// ...
// Call Combine() with four parameters
fullName = Combine(
Directory.GetCurrentDirectory(),
"bin", "config", "index.html");
Console.WriteLine(fullName);
// ...
// Call Combine() with only three parameters
fullName = Combine(
Environment.SystemDirectory,
"Temp", "index.html");
Console.WriteLine(fullName);
// ...
// Call Combine() with an array
fullName = Combine(
new string[] {
"C:\", "Data",
"HomeDir", "index.html"} );
Console.WriteLine(fullName);
// ...
}
static string Combine(params string[] paths)
{
string result = string.Empty;
foreach (string path in paths)
{
result = System.IO.Path.Combine(result, path);
}
return result;
}
}
输出4-8展示了代码清单4-14的结果。
输出4-8
C:\Data\mark\bin\config\index.html
C:\WINDOWS\system32\Temp\index.html
C:\Data\HomeDir\index.html
在第一个Combine()调用中,我们指定了4个参数。第二个调用只指定了3个参数。在最后一个调用中,参数用一个数组来传递。换言之,Combine()方法可接受数量可变的参数,不管这些参数是以逗号分隔的,还是整体作为一个数组来传递的。
为了获得这样的效果,Combine()方法需要:
(1) 在方法声明的最后一个参数之前,添加一个params关键字;
(2) 将最后一个参数声明为一个数组。
像这样声明了一个参数数组(parameter array)之后,就可以将每个参数作为参数数组的一个成员来访问了。在Combine()方法的实现中,我们遍历paths数组的每个元素,并调用System.IO. Path.Combine()。这个方法能自动合并一个路径中的各个部分,并能正确地使用平台特有的目录分隔符。注意,PathEx.Combine()完全等价于Path.Combine(),只是PathEx.Combine()能处理数量可变的参数,而非只能处理两个。
参数数组有以下一些值得注意的特征。
l 参数数组不一定是方法声明中的唯一参数。但是,参数数组必须是方法声明中的最后一个参数。由于只有最后一个参数才可能是参数数组,所以方法最多只能有一个参数数组。
l 调用者可以为参数数组指定零个参数,这会造成包含零个数据项的一个数组。
l 参数数组是类型安全的——类型必须匹配于数组指定的类型。
l 调用者可以显式地提供一个数组,而不是以逗号分隔的参数列表。最终生成的CIL代码是一样的。
l 假如目标方法的实现要求一个最起码的参数数量,请在方法声明中显式指定必须提供的参数。这样一来,假如要求的参数遗失了,就会强迫编译器报错,而不是依赖于运行时错误处理。例如,使用int Max(int first, params int[] operads)而不是int Max(params int[] operads),确保至少有一个值传给Max()。
使用参数数组,我们可以将相同类型的、数量可变的多个参数传给一个方法。本章后面的4.7节讨论了如何支持不同类型的、数量可变的参数。
文章评论(0条评论)
登录后参与讨论