6.1 定义和使用函数
本节介绍如何将函数添加到应用程序中,以及如何在代码中使用(调用)它们。首先从基础知识开始,看看不与调用代码交换任何数据的简单函数,然后介绍更高级的函数用法。首先分析一个示例。
试一试:定义和使用基本函数:Ch06Ex01\Program.cs
(1)在C:\BegVCSharp\Chapter06目录中创建一个新的控制台应用程序Ch06Ex01。
(2)把下述代码添加到Program.cs中:
class Program { static void Write() { WriteLine("Text output from function."); } static void Main(string[] args) { Write(); ReadKey(); } }
(3)执行代码,结果如图6-1所示。
图6-1
示例说明
下面的4行代码定义了函数Write():
static void Write() { WriteLine("Text output from function."); }
这些代码把一些文本输出到控制台窗口中。但此时这些并不重要,我们更关心定义和使用函数的机制。
函数定义由以下几部分组成:
● 两个关键字:static和void
● 函数名后跟圆括号,如Write()
● 一个要执行的代码块,放在花括号中
注意:一般采用PascalCase形式编写函数名。
定义Write()函数的代码非常类似于应用程序中的其他代码:
static void Main(string[] args) { ... }
这是因为,到目前为止我们编写的所有代码(类型定义除外)都是函数的一部分。函数Main()是控制台应用程序的入口点函数。当执行C#应用程序时,就会调用它包含的入口点函数,这个函数执行完毕后,应用程序就终止了。所有C#可执行代码都必须有一个入口点。
Main()函数和Write()函数的唯一区别(除了它们包含的代码)是函数名Main后面的圆括号中还有一些代码,这是指定参数的方式,详见后面的内容。
如上所述,Main()函数和Write()函数都是使用关键字static和void定义的。关键字static与面向对象的概念相关,本书在后面讨论。现在只需要记住,本节的应用程序中所使用的所有函数都必须使用这个关键字。
void更容易解释。这个关键字表明函数没有返回值。本章后面将讨论函数有返回值时需要编写什么代码。
继续下去,调用函数的代码如下所示:
Write();
键入函数名,后跟空括号即可。当程序执行到这行代码时,就会运行Write()函数中的代码。
注意:在定义和调用函数时,必须使用圆括号。如果删除它们,将无法编译代码。
6.1.1 返回值
通过函数进行数据交换的最简单方式是利用返回值。有返回值的函数会最终计算得到这个值,就像在表达式中使用变量时,会计算得到变量包含的值一样。与变量一样,返回值也有数据类型。
例如,有一个函数GetString(),其返回值是一个字符串,可以在代码中使用该函数,如下所示:
string myString; myString = GetString();
还有一个函数GetVal(),它返回一个double值,可在数学表达式中使用它:
double myVal; double multiplier = 5.3; myVal = GetVal() * multiplier;
当函数返回一个值时,可以采用以下两种方式修改函数:
● 在函数声明中指定返回值的类型,但不使用关键字void。
● 使用return关键字结束函数的执行,把返回值传送给调用代码。
从代码角度看,对于我们讨论的控制台应用程序函数,其使用返回值的形式如下所示:
static <returnType> <FunctionName>() { ... return <returnValue>; }
这里唯一的限制是<returnValue>必须是<returnType>类型的值,或者可以隐式转换为该类型。但是,<returnType>可以是任何类型,包括前面介绍的较复杂类型。这段代码可以很简单:
static double GetVal() { return 3.2; }
但是,返回值通常是函数执行的一些处理的结果。上面的结果使用const变量也可以简单地实现。
当执行到return语句时,程序会立即返回调用代码。这条语句后面的代码都不会执行。但这并不意味着return语句只能放在函数体的最后一行。可以在前边的代码里使用return,例如放在分支逻辑之后。把return语句放在for循环、if块或其他结构中会使该结构立即终止,函数也立即终止。例如:
static double GetVal() { double checkVal; // checkVal assigned a value through some logic (not shown here). if (checkVal < 5) return 4.7; return 3.2; }
根据checkVal的值,将返回两个值中的一个。这里的唯一限制是,必须在函数的闭合花括号 }之前处理return语句。下面的代码是不合法的:
static double GetVal() { double checkVal; // checkVal assigned a value through some logic. if (checkVal < 5) return 4.7; }
如果checkVal>= 5,就不会执行到return语句,这是不允许的。所有处理路径都必须执行到return语句。大多数情况下,编译器会检查是否执行到return语句,如果没有,就给出错误“并不是所有的处理路径都返回一个值”。
执行一行代码的函数可使用C# 6引入的一个功能:表达式体方法(expression-bodied method)。以下函数模式使用= >(Lambda箭头)来实现这一功能。
static <returnType> <FunctionName>() => <myVal1 * myVal2>;
例如, C# 6之前的Multiply()函数如下:
static double Multiply(double myVal1, double myVal2) { return myVal1 * myVal2; }
现在可以使用= >(Lambda箭头)编写它。下述代码用更简单和统一的方式表达方法的意图:
static double Multiply(double myVal1, double myVal2) => mVal1 * MyVal2;
6.1.2 参数
当函数接受参数时,必须指定以下内容:
● 函数在其定义中指定接受的参数列表,以及这些参数的类型。
● 在每个函数调用中提供匹配的实参列表。
注意:仔细阅读C#规范会发现形参(parameter)和实参(argument)之间存在一些细微的区别:形参是函数定义的一部分,而实参则由调用代码传递给函数。但是,这两个术语通常被简单地称为参数,似乎没有人对此感到十分不满。
示例代码如下所示,其中可以有任意数量的参数,每个参数都有类型和名称:
static <returnType> <FunctionName>(<paramType> <paramName>, ...) { ... return <returnValue>; }
参数之间用逗号隔开。每个参数都在函数的代码中用作一个变量。例如,下面是一个简单的函数,带有两个double参数,并返回它们的乘积:
static double Product(double param1, double param2) => param1 * param2;
下面看一个较复杂的示例:
试一试:通过函数交换数据(1):Ch06Ex02\Program.cs
(1)在C:\BegVCSharp\Chapter06目录中创建一个新的控制台应用程序Ch06Ex02。
(2)把下列代码添加到Program.cs中:
class Program { static int MaxValue(int[] intArray) { int maxVal = intArray[0]; for (int i = 1; i < intArray.Length; i++) { if (intArray[i] > maxVal) maxVal = intArray[i]; } return maxVal; } static void Main(string[] args) { int[] myArray = { 1, 8, 3, 6, 2, 5, 9, 3, 0, 2 }; int maxVal = MaxValue(myArray); WriteLine($"The maximum value in myArray is {maxVal}"); ReadKey(); } }
(3)执行代码,结果如图6-2所示。
图6-2
示例说明
这段代码包含一个函数,它执行的任务就是本章开头的示例函数所完成的任务。该函数以一个整数数组作为参数,并返回该数组中的最大值。该函数的定义如下所示:
static int MaxValue(int[] intArray) { int maxVal = intArray[0]; for (int i = 1; i < intArray.Length; i++) { if (intArray[i] > maxVal) maxVal = intArray[i]; } return maxVal; }
函数MaxValue()定义了一个参数,即int数组intArray,它还有一个int类型的返回值。最大值的计算是很简单的。局部整型变量maxVal初始化为数组中的第一个值,然后把这个值与数组中后面的每个元素依次进行比较。如果一个元素的值比maxVal大,就用这个值代替当前的maxVal值。循环结束时,maxVal就包含数组中的最大值,用return语句返回。
Main()中的代码声明并初始化一个简单的整数数组,用于MaxValue()函数:
int[] myArray = { 1, 8, 3, 6, 2, 5, 9, 3, 0, 2 };
调用MaxValue(),把一个值赋给int变量maxVal:
int maxVal = MaxValue(myArray);
接着,使用WriteLine()把这个值写到屏幕上:
WriteLine($"The maximum value in myArray is {maxVal}");
1.参数匹配
在调用函数时,必须使提供的参数与函数定义中指定的参数完全匹配,这意味着要匹配参数的类型、个数和顺序。例如,下面的函数:
static void MyFunction(string myString, double myDouble) { ... }
不能使用下面的代码调用:
MyFunction(2.6, "Hello");
这里试图把一个double值作为第一个参数传递,把string值作为第二个参数传递,参数顺序与函数声明中定义的顺序不匹配。这段代码不能编译,因为参数类型是错误的。本章后面的“重载函数”一节将介绍解决这个问题的一个有效技术。
2.参数数组
C#允许为函数指定一个(只能指定一个)特殊参数,这个参数必须是函数定义中的最后一个参数,称为参数数组。参数数组允许使用个数不定的参数调用函数,可使用params关键字定义它们。
参数数组可以简化代码,因为在调用代码中不必传递数组,而是传递同类型的几个参数,这些参数会放在可在函数中使用的一个数组中。
定义使用参数数组的函数时,需要使用下列代码:
static <returnType> <FunctionName>(<p1Type> <p1Name>, ..., params <type>[] <name>) { ... return <returnValue>; }
使用下面的代码可以调用该函数:
<FunctionName>(<p1>, ..., <val1>, <val2>, ...)
其中<val1>和<val2>等都是<type>类型的值,用于初始化<name>数组。可以指定的参数个数几乎不受限制,但它们都必须是<type>类型。甚至根本不必指定参数。
下面的示例定义并使用带有params类型参数的函数。
试一试:通过函数交换数据(2):Ch06Ex03\Program.cs
(1)在C:\BegVCSharp\Chapter06目录中创建一个新的控制台应用程序Ch06Ex03。
(2)把下述代码添加到Program.cs中:
class Program { static int SumVals(params int[] vals) { int sum = 0; foreach (int val in vals) { sum += val; } return sum; } static void Main(string[] args) { int sum = SumVals(1, 5, 2, 9, 8); WriteLine($"Summed Values = {sum}"); ReadKey(); } }
(3)执行代码,结果如图6-3所示。
图6-3
示例说明
这个示例用关键字params定义函数sumVals(),该函数可以接受任意个int参数(但不接受其他类型的参数):
static int SumVals(params int[] vals) { ... }
这个函数对vals数组中的值进行迭代,将这些值加在一起,返回其结果。
在Main()中,用5个整型参数调用函数SumVals():
int sum = SumVals(1, 5, 2, 9, 8);
也可以用0、1、2或100个整型参数调用这个函数—— 参数的数量不受限制。
注意:C# 引入了指定函数参数的新方式,包括用一种可读性更好的方式来包含可选参数。第13章将介绍这些方法,该章讨论C#语言。
3.引用参数和值参数
本章迄今定义的所有函数都带有值参数。其含义是:在使用参数时,是把一个值传递给函数使用的一个变量。在函数中对此变量的任何修改都不影响函数调用中指定的参数。例如,下面的函数使传递过来的参数值加倍,并显示出来:
static void ShowDouble(int val) { val *= 2; WriteLine($"val doubled = {0}", val); }
参数val在这个函数中被加倍,如果按以下方式调用它:
int myNumber = 5; WriteLine($"myNumber = {myNumber}"); ShowDouble(myNumber); WriteLine($"myNumber = {myNumber}", );
输出到控制台的文本如下所示:
myNumber = 5 val doubled = 10 myNumber = 5
把myNumber作为一个参数,调用ShowDouble()并不影响Main()中myNumber的值,即使把myNumber赋值给val后将val加倍,myNumber的值也不变。
这很不错,但如果要改变myNumber的值,就会有问题。可以使用一个为myNumber返回新值的函数:
static int DoubleNum(int val) { val *= 2; return val; }
并使用下面的代码调用它:
int myNumber = 5; WriteLine($"myNumber = {myNumber}"); myNumber = DoubleNum(myNumber); WriteLine($"myNumber = {myNumber}");
但这段代码一点也不直观,且不能改变用作参数的多个变量值(因为函数只有一个返回值)。
此时可以通过“引用”传递参数。即函数处理的变量与函数调用中使用的变量相同,而不仅仅是值相同的变量。因此,对这个变量进行的任何改变都会影响用作参数的变量值。为此,只需使用ref关键字指定参数:
static void ShowDouble(ref int val) { val *= 2; WriteLine($"val doubled = {val}"); }
在函数调用中再次指定它(这是必需的):
int myNumber = 5;
WriteLine($"myNumber = {myNumber}", );
ShowDouble(ref myNumber);
WriteLine($"myNumber = {myNumber}");
输出到控制台的文本如下所示:
myNumber = 5 val doubled = 10 myNumber = 10
用作ref参数的变量有两个限制。首先,函数可能会改变引用参数的值,所以必须在函数调用中使用“非常量”变量。所以,下面的代码是非法的:
const int myNumber = 5; WriteLine($"myNumber = {myNumber}", ); ShowDouble(ref myNumber); WriteLine($"myNumber = {myNumber}");
其次,必须使用初始化过的变量。C#不允许假定ref参数在使用它的函数中初始化,下面的代码也是非法的:
int myNumber; ShowDouble(ref myNumber); WriteLine("myNumber = {myNumber}");
4.输出参数
除了按引用传递值外,还可以使用out关键字,指定所给的参数是一个输出参数。out关键字的使用方式与ref关键字相同(在函数定义和函数调用中用作参数的修饰符)。实际上,它的执行方式与引用参数几乎完全一样,因为在函数执行完毕后,该参数的值将返回给函数调用中使用的变量。但是,二者存在一些重要区别:
● 把未赋值的变量用作ref参数是非法的,但可以把未赋值的变量用作out参数。
● 另外,在函数使用out参数时,必须把它看成尚未赋值。
即调用代码可以把已赋值的变量用作out参数,但存储在该变量中的值会在函数执行时丢失。
例如,考虑前面返回数组中最大值的MaxValue()函数,略微修改该函数,获取数组中最大值的元素索引。为简单起见,如果数组中有多个元素的值都是这个最大值,只提取第一个最大值的索引。为此,修改函数,添加一个out参数,如下所示:
static int MaxValue(int[] intArray, out int maxIndex) { int maxVal = intArray[0]; maxIndex = 0; for (int i = 1; i < intArray.Length; i++) { if (intArray[i] > maxVal) { maxVal = intArray[i]; maxIndex = i; } } return maxVal; }
可采用以下方式使用该函数:
int[] myArray = { 1, 8, 3, 6, 2, 5, 9, 3, 0, 2 }; int maxIndex; WriteLine($"The maximum value in myArray is {MaxValue(myArray, out maxIndex)}"); WriteLine($"The first occurrence of this value is at element {maxIndex + 1}");
结果如下:
The maximum value in myArray is 9 The first occurrence of this value is at element 7
注意,必须在函数调用中使用out关键字,就像ref关键字一样。