C#入门经典(第7版):C# 6.0 & Visual Studio 2015(.NET开发经典名著)
上QQ阅读APP看书,第一时间看更新

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关键字一样。