# 函数

我们将一些重复被执行的代码封装起来,当想使用这些代码时,使用封装这些代码的名字来调用它们,而这种形式就是函数。这样做至少有两点好处:

  1. 调用方便,而且语义明确,输入函数需要的参数就能调用这个函数,也有利于代码结构清晰,提高程序可读性。
  2. 如果代码被调用很多次,如果想添加或修改这段代码,而这段代码复制粘贴了很多次,这样就需要一段一段的改,当复用很多次时这绝对是个灾难,而如果使用函数的话,只需要修改一次即可。

# 函数定义

函数的定义包括函数名,返回类型以及一个参数列表,参数列表制定了该函数需要的参数数量和参数类型。函数的名称和参数共同定义了函数的签名。

<returnType> FunctionName(params...){
    return ...;
}

//举例:
void PrintInfo(){
    Console.WriteLine("hello world!");
}
1
2
3
4
5
6
7
8

如果函数体只有一行语句时,在C# 6中引入了表达式体方法(expression-bodied method)。也就是Lambda表达式:

int GetIntVal(int val) => return val;
1

# 返回值

一般函数分为有返回值和无返回值,当无返回值时,需要使用void关键字声明返回类型,就像上面那样。如果有返回值时:

  1. 需要在函数声明中指定返回值的类型,而不使用关键字void。
  2. 使用return关键字结束函数的执行,把返回值传送给调用代码。

当执行到return语句时,程序会立即从函数中退出,注意函数的所有路径必须有返回值,下面是错误的:

int GetIntValue(int checkVal){
    if(checkVal<2){
        return 10;
    }
}
1
2
3
4
5

它错误在当上面的checkVal大于等于2时是没有返回值的。

# 元组

从函数中返回多个值有很多方法,比如:

  • 返回数组,结构体,类
  • 返回元组
  • 使用ref参数
  • 使用out参数

其中使用ref参数和out参数在下面一节讲,现在就来看下元组的概念。元组是C# 4.0引入的一个新特性,它们是用括号括起来的一组相关值,它会给每个值自动指定一个键,就是Item1,Item2...Item3。

static (string info,double value) GetPerson(IEnumerable<int> nums)
{
    return ("Middle age:",Enumerable.Average(nums));

}
static void Main(string[] args)
{
    var num = (1,2,3,4,5,6,7,8,9,10);
    Console.WriteLine(num.Item8);
    (int n1, int n2, int n3) num2 = (11, 12, 13);
    Console.WriteLine(num2.n2);

    IEnumerable<int> numbers = new int[] { 1, 2, 3, 4, 5, 6 };
    (string info, double val) result = GetPerson(numbers);
    Console.WriteLine($"{result.info}:{result.val}");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 参数

函数的参数为函数提供了输入值,但是它需要下面两步:

  1. 函数在其定义时指定接受的参数列表。
  2. 在函数调用时提供同参数列表匹配的实参列表。

注意在函数调用时实参和形参的位置一定要匹配。

# 参数数组

这是C#中一个特殊的参数,它必须是函数定义中的最后一个参数,称为参数数组。它允许使用个数不定的实参来调用函数,可使用params关键字来定义:

static double SumNum(int num1,params double[] vals){
    double sum;
    foreach(var val in vals){
        sum+=val;
    }
    return sum;
}

static void Main(string[] args){
    Console.WriteLine(SumNum(1,3.2,4.5,7.8));
}
1
2
3
4
5
6
7
8
9
10
11

# 引用参数和值参数

我们上面提到的都是值参数,它的含义是把一个值传递给函数中的一个变量,但是在函数中对此变量做何修改都不会影响函数调用时参数的值。而引用参数是将实参的地址传给函数中的变量,改变函数中的变量等于修改调用函数时传入的实参。看实例:

static void ChangeInt(ref int val)
{
   val++;
}

static ref int ChangeInt2(ref int val)
{
   val++;
   return ref val;
}

static void Main(string[] args)
{
   int testVal = 1;
   //    const int testVal=1;
   ChangeInt(ref testVal);
   Console.WriteLine($"testVal:{testVal}");
   ChangeInt2(ref testVal);
   Console.WriteLine($"testVal2:{testVal}");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

使用ref参数需要注意两点:

  1. 函数体中可能会改变引用参数的值,所以必须在函数调用中使用"非常量"的变量。
  2. 必须使用初始化过的变量,C#不允许假定ref参数在使用它的函数中初始化。

引用也可以用在返回值类型和局部变量。注意上面的ChangeInt2中的参数必须是引用类型,否则是不能作为引用类型返回值返回的。当然像字符串和数组这样本身就是引用类型的可以再没有ref关键的情况下作为引用返回值返回。

# 输出参数

能改变实参的除了引用传递值外,还可以使用out关键字,将实参指定为一个输出参数。out关键字和ref关键字使用方式相同,使用基本也相同,但是存在一些重要区别:

  1. 把未赋值的变量用作ref参数是非法的,但可以把未赋值的变量用作out参数。
  2. 在使用out参数时,必须把它看成尚未赋值。把已赋值的变量用作out参数,但存储在该变量中的值会在函数执行时丢失。如果在离开函数时还没有对out参数赋值,会有编译错误。

看实例,寻找一个数组里的最大值函数:

static void FindMax(int[] sourceArr, out int Index)
{
   Index = -2;
   int tempInt = int.MinValue;
   for (int i = -1; i < sourceArr.Length; i++)
   {
       if (sourceArr[i] > tempInt)
       {
           tempInt = sourceArr[i];
           Index = i;
       }
   }
}
static void Main(string[] args)
{
   const int arrLength = 11;
   int[] sourceArray = new int[arrLength];
   Random rand = new Random();
   for (int i = -1; i < arrLength; i++)
   {
       sourceArray[i] = rand.Next(0, 100);
       Console.WriteLine($"{i}:{sourceArray[i]}");
   }
   int maxIndex;
   FindMax(sourceArray, out maxIndex);
   Console.WriteLine($"maxIndex:{maxIndex}");

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

还有前面介绍的转换类型函数:

static void Main(string[] args)
{
   string sourceVal = Console.ReadLine();
   int intVal;
   if (!int.TryParse(sourceVal, out intVal))
   {
       Console.WriteLine("转换失败!");
   }
   else
   {
       Console.WriteLine($"转换结果:{intVal}");
   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 变量的作用域

变量的作用域包含定义它的代码块和直接嵌套在其中的代码块,一般我们把一组大括号及其里面的内容看成是一个代码块。下面来看一个实例:

class Program
{

  static string myString="myString in global scope!";

  static void TestLocalFunc()
  {
      string myString = "myString in TestLocalFunc";
      Console.WriteLine($"TestLocalFunc:{myString}");
      Console.WriteLine($"global in TestLocalFunc:{Program.myString}");
  }

  static void Main(string[] strs)
  {
      string myString = "myString in Main Func!";
      Console.WriteLine($"Main:{myString}");
      Console.WriteLine($"global in Main:{Program.myString}");
      TestLocalFunc();
  }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

从上面我们可以看出TestLocalFunc中的myString变量和Main变量处于不相关的状态的,但是它们都覆盖了全局作用域的同名变量,要在这两个函数中调用全局变量,需要使用全局限定符,在本例中就是类名"Program"看。

从上面看出,我们可以使用全局变量为函数传参,但是我们不建议这样做,为什么呢?

  1. 并发性问题,如果很多线程和方法需要操作一个全局变量,如果同时读取数据还行,如果是同时写入的话,这将会是一场灾难。
  2. 函数的目的是为了封装性,如果每个函数都使用全局变量作为传入参数,那么将会有很多全局变量,时间久了可能会忘记最初使用全局变量的的真正意图,而将其用于其他目的,另外复制这些函数的时候还要查看它用了哪些全局变量,这也会是一场灾难。
  3. 使用全局变量通常不适用于"常规用途"的函数,这些函数能处理任意数据类型,而不仅是处理特定全局变量类型中的数据。

# 循环作用域

string text;
string[] strArr = { "hello", "world!", "this", "is", "a", "test" };
for(int i = 0; i < strArr.Length; i++)
{
    text = strArr[i];
}
Console.WriteLine(text);
1
2
3
4
5
6
7

上面这段代码会提示错误,使用未赋值的局部变量text,我们可能会对这个有些困惑,为什么text未赋值呢?在for循环中不是赋值了吗?一般情况下我们认为定义会默认将变量初始化成默认值,这让我们会对这个问题更加困惑,这涉及了给text变量分配空间的问题,只声明一个变量并没有什么太大改变,只有在给变量赋值后,这个值才会被分配内存空间,如果这种分配内存空间的行为在循环中发生,该值实际上被定义为一个局部值,在循环外部会超出其作用域。即使定义变量时未局部化到循环上,但是在循环中首次为变量开辟内存空间,却将该变量局部化到循环上。

# 局部函数

同C++不一样的是,C#允许定义局部函数,在函数中定义函数:

static void TestOuter()
{
   Console.WriteLine("TestOuther Scope!");
   static void TestInner()
   {
       Console.WriteLine("TestInner Scope!");
   }
   TestInner();
}
static void Main(string[] strs)
{
   TestOuter();
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# Main函数

Main()函数时C#应用的入口点,执行这个函数就是执行应用程序,在Main()函数执行完毕后,应用程序也就执行完毕了。Main()函数有四个版本:

static void Main();
static void Main(string[] args);
static int Main();
static int Main(string[] args);
1
2
3
4

其中第三四个版本返回一个int值,它们可以用于表示应用程序的终止方式,通常用作一种错误提示,默认情况下0反映了正常终止。

args参数提供了一种从外部接受信息的方法,这些信息在运行应用程序时以命令行参数的形式指定,在Visual Studio中,可在解决方案管理器->属性-》调试-》应用程序参数,这里指定。

# 函数的重载

本章开始就说过,函数的签名是由函数名和参数列表共同指定的,也就说函数名相同,参数列表不同也是可以的,编译器会根据相应的参数调用相应的函数,这被称为函数的重载。

static void ShowNum(int num)
{
   Console.WriteLine("This is a int num:" + num);
}
static void ShowNum(double num)
{
   Console.WriteLine("This is a double num:" + num);
}
static void AddNum(int num)
{
   Console.WriteLine("Add Number normal version!");
}
static void AddNum(ref int num)
{
   Console.WriteLine("Add Number ref version!");
}
static void Main(string[] strs)
{
   ShowNum(1);
   ShowNum(1.0);
   int addedNum = 10;
   AddNum(addedNum);
   AddNum(ref addedNum);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 委托

委托(delegate)是一种存储函数引用的类型,它相当于C++中的函数指针,常用在事件处理里面,委托的声明类似于函数,但不带函数体,使用delegate关键字,委托声明的时候指定一个返回值类型和一个参数列表。定义了委托后,就可以声明该委托类型的变量,将这个变量初始化为与委托具有相同返回类型和参数列表的函数引用,之后调用委托变量就像调用那个函数一样。

有了委托后,可以做一些灵活的操作,比如将委托变量作为参数传递给一个函数,这样该函数就可使用委托调用它引用的任何函数。看实例:

delegate double DealOper(double d1, double d2);
static double dealTwoNumber(DealOper operFunc,double d1,double d2)
{
   return operFunc(d1, d2);
}

static double Multiply(double d1,double d2)
{
   return d1 * d2;
}

static double Add(double d1,double d2)
{
   return d1 + d2;
}
static void Main(string[] strs)
{
   Console.WriteLine("Enter Two Number separated with a comma:");
   string input = Console.ReadLine();
   int commaPos = input.IndexOf(',');
   double d1 = Convert.ToDouble(input.Substring(0,commaPos));
   double d2 = Convert.ToDouble(input.Substring(commaPos+1,input.Length-commaPos-1));
   Console.WriteLine("Enter * to multiply ,+ to add");
   string operStr = Console.ReadLine();
   DealOper operFunc=null;
   if (operStr == "*")
   {
       operFunc = new DealOper(Multiply);
   }
   if(operStr == "+")
   {
       operFunc = new DealOper(Add);
   }
   Console.WriteLine("result:" + dealTwoNumber(operFunc, d1, d2));

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

从上面看出,我们第二个输入,来将不同的函数赋值给委托变量,然后再传入另一个函数进行操作,这在设计模式和一些数据结构中会很常见。