在本章中,我们将介绍方法的概念,并学习一些使用VS调试程序的基本技巧。
PS:在本教程中不特别区分函数和方法两个概念。
创建方法
声明方法
方法是具名的语句序列,每个方法都有名称和主体,方法名必须是一个合法的标识符,而方法主体包含了方法被调用时实际执行的语句。方法必须在类的内部定义。
声明一个方法的语法如下所示:
returnType methodName ( parameterList ) { //在这里写方法主体 }
-
returnType 是一个类型名称,它指定了方法的返回值类型,可以是
int
,string
之类的基元数据类型,也可以是自己定义的类型。如果要声明没有返回值的方法,应将returnType写为
void
。 -
methodName 是方法名,遵循标识符的命名规则。
-
parameterList 是参数列表,描述了允许传给方法的数据的类型和名称。参数列表可以是空的。
-
方法主体必须写在一对大括号中。
几个方法的具体例子如下,我们在 Program
类中声明这些方法。为了使这些方法能在 Main()
函数中使用,我们将它标记为 static
,关于 static
的用法以及可访问性问题在第7章中有详细的介绍。
internal class Program { static int CalcAdd(int a, int b) { int c = a + b; return c; //计算a+b的值存储在变量c中,然后将c的值作为返回值。 } static void Print(string text) { Console.WriteLine(text); //输出文本,无返回值。 } static void Main(string[] args) { //入口点 } }
如果你要复制这段代码,请记得把上面的类复制到命名空间中。
注意,你不能在参数列表和返回值类型上使用 var
关键字。
从方法返回数据
如果你希望从方法中返回数据(返回类型不是void),那么你必须在方法内使用 return
语句。正如上面的 CalcAdd()
一例,先写出 return
关键字,然后添加计算返回值的表达式,最后加上分号。注意,表达式的结果类型必须和方法指定的返回类型相同。
static int CalcAdd(int a, int b) { int c = a + b; return ""; //计算a+b的值存储在变量c中,然后将c的值作为返回值 }
上面的写法不能通过编译,因为 CalcAdd()
方法的返回类型已被声明为 int
,你不能用 return
关键字返回一个 string
。
return语句会立刻结束方法。如果你在 return
语句后写了其他语句,VS会提示你”检测到无法访问的代码“。此外,在 void
方法中,你可以通过 return;
这一语句立刻结束方法。标记为 void
的方法中可以没有 return
关键字,但除此之外方法必须保证有返回值。
使用表达式主体方法
如果一个方法的主体非常简单,我们可以用下面的写法来简化方法声明:
static void ShowResult(int answer) => Console.WriteLine(answer);
这种写法和使用花括号的写法没有任何区别,=>
符号右边的表达式的结果将自动作为方法的返回值。这种在功能上没有区别,而使语法简化的写法称为语法糖。
调用方法
声明方法自然是为了调用方法,就好比学习C#技术是为了写mod一样。你可以通过方法名来调用方法,提供方法需要的参数(如果有),并获得方法的返回值。调用方法例子如下:
int a = 1; CalcAdd(a,2); //OK,忽略了返回值,尽管大多数时候不应该这么做 int c = CalcAdd(a,2); //OK,将方法的返回值赋给变量c Print("爬"); //OK,没有返回值
可以注意到方法名后小括号中的内容,它们称为参数。调用方法时,必须为每个形式参数(形参)提供值,也就是实际参数(实参)。对于接受多个参数的方法,你需要使用逗号隔开不同的参数。
让我们以 CalcAdd()
为例具体分析方法的调用。
static int CalcAdd(int a, int b) { int c = a + b; return c; //计算a+b的值存储在变量c中,然后将c的值作为返回值。 } static void Main(string[] args) { int arg1 = 1, arg2 = 2; int result = CalcAdd(arg1, arg2); Console.WriteLine(result); }
该方法有两个 int
参数,所以调用方法时必须提供两个以逗号为分隔的 int
实参。你可以使用变量作为实参,变量的值会被传递给方法。然后在第4行方法通过 return
语句返回两个参数相加的值,方法执行结束,回到 Main()
方法中,然后返回值被赋值给 result
变量,最后我们将它打印到控制台中。
区别形参和实参
形参是编写方法时使用的参数名,而实参是程序运行过程中实际传给方法的数据。换言之,形参是占位符,实参是实际的数据。
使用作用域
作用域的概念
我们已经在C#中见到了许多使用花括号的地方。对于方法中声明的变量,其有效期开始于被声明的位置,结束于方法结束的位置。换言之,在同一个方法中,后续的语句都可以使用该变量,而方法执行完毕后,变量随之“消失”,不再能继续使用。如果某变量能在程序的特定位置使用,我们就称变量在那个位置“处于作用域”内。作用域是指该变量能起作用的程序区域,除了变量有作用域之外,标识符也有。它们的作用域和声明的位置有关。
定义局部作用域
我们尝试在 Main()
方法中输出 c
的值:
static int CalcAdd(int a, int b) { int c = a + b; return c; //计算a+b的值存储在变量c中,然后将c的值作为返回值。 } static void Main(string[] args) { int arg1 = 1, arg2 = 2; int result = CalcAdd(arg1, arg2); Console.WriteLine(result); Console.WriteLine(c); //非法 }
可以发现编译器报错,因为 c
只能由 CalcAdd()
方法使用。界定方法的大括号也同时界定方法的作用域,在方法主题中声明的任何变量都在该作用域中,只能由方法内部的代码访问,它们称为局部变量。你不能在其他方法中使用此方法内定义的局部变量。
定义类作用域
如果我们将 c
的声明从 CalcAdd()
方法中挪出,放入 Program
类的大括号中,会发生什么呢?
internal class Program { static int c; static int CalcAdd(int a, int b) { c = a + b; return c; //计算a+b的值存储在变量c中,然后将c的值作为返回值。 } static void Main(string[] args) { int arg1 = 1, arg2 = 2; int result= CalcAdd(arg1, arg2); Console.WriteLine(result); Console.WriteLine(c); //合法 } }
可以发现此时第15行合法,因为此时的 c
定义在 Program
类中。这些定义在类中的变量称为字段。不同于局部变量,字段可以在该类的任何方法中使用。你可以通过字段在方法之间共享信息。关于字段的问题我们会在第7章中讨论。
此外,你可以在类的任何位置(方法主体外)(不许故意找茬!)中定义字段,而无需像在局部作用域中那样先声明后使用。编译器会为你打点好一切的。
重载方法
如果两个标识符同名且在同一作用域内,我们就称它们被重载。重载的标识符往往是bug,VS会捕捉并报错。例如,你无法在同一局部定义域中声明两个 a
变量:
int a = 1; Console.WriteLine(a); int a; //非法
同理,在同一个类中声明两个相同的字段,或者两个完全一样的方法,也会发生编译错误。但有一种办法可以真正地重载标识符,而且这种重载十分有用。
前面我们讲到,声明方法时参数的类型是确定的,但我们一直在使用的 Console.WriteLine()
方法为什么能接收多种不同类型的参数?
如果我们借助VS的智能感知,我们会发现它具有许多不同的版本:
编译时,VS检查你传入参数的类型,并自动调用方法的合适版本以处理数据。因此,要针对不同数据类型或信息组别执行相同的操作,重载是一项非常有用的技术。如果方法有多个实现,每个实现的参数列表都不相同,就可以重载该方法。注意,你不能重载方法的返回类型,编译器并没有那么强大到那种程度。
下面是 Console.WriteLine()
方法进行重载的例子,不同的实现具有不同的参数列表来重载:
public static class Console { //... public static void WriteLine(int val) { //... } public static void WriteLine(string val) { //... } public static void WriteLine(float val) { //... } public static void WriteLine(double val) { //... } //... }
编写方法
接下来我们尝试编写方法,实现从控制台读入两个整数,输出它们相加的和。
编写子方法
首先声明一个 readInt()
方法,我们调用该方法来从控制台读入整数:
private static int readInt() { string t = Console.ReadLine(); int ret = int.Parse(t); return ret; }
我们调用 Console.ReadLine()
方法从控制台读入一行字符串,将其存储到变量 t
中,然后使用 int.Pasre
方法将它转换为 int
存储到变量 ret
中,最后用 return
语句将 ret
返回。
然后我们使用之前的 CalcAdd()
方法:
private static int CalcAdd(int a, int b) { int c = a + b; return c; }
调用方法
然后回到 Main()
方法中,调用上面的方法来完成任务:
static void Main(string[] args) { int num1 = readInt(); int num2 = readInt(); int sum = CalcAdd(num1, num2); Console.WriteLine(sum); }
然后你可以尝试运行程序,5和7为通过键盘输入的值,12是输出值。
使用可选参数和具名参数
使用可选参数
声明方法时,你可以“初始化”一些参数,就像下面这样:
public static void DoSomeWorks(int a, int b = 0, int c = 0) { //... }
这时如果你尝试调用此方法,可以发现VS给出的提示有所不同:
填写参数时,你可以仅传入一个实参,此时 b
和· c
将使用声明时初始化的值0,你也可以传入多个参数:
DoSomeWorks(1,1); // a将被赋值为1,b将被赋值为1,c仍为0 DoSomeWorks(1,1,1); // a,b,c均为1
使用具名参数
调用方法时,C#允许你按名称指定参数,这样就可以按照不同顺序传递实参。
下面的例子和编写方法一节中的代码功能相同,但参数按照名称指定:
CalcAdd(b: 9, a: 10); //OK CalcAdd(10,b: 9) //混合使用按位置和按名称,但必须先按位置指定实参
调用具有可选参数的方法时,你可以通过具名参数省略实参:
DoSomeWorks(a: 1, c: 9); //OK,b将为初始值0
消除可选参数和具名参数的歧义
假定一组重载方法:
static void OptMethod(int first, double second = 0.0, string third = "hello") { //... } static void OptMethod(int first, double second = 1.0, string third = "bye", int fourth = 100) { //... }
如果我们按下面的代码调用方法:
OptMethod(1, 2.5, "World"); //哪一个版本会被调用?
这样的代码是合法的。编译器会调用和方法调用最匹配的方法,即具有3个参数的版本,而非4个。
假如我们使用具名参数:
OptMethod(1, fourth: 101); //显然是第二个版本会被调用
很显然,只有第二个方法能匹配该调用。但下面的调用该如何考虑呢?
OptMethod(1, 2.5);
这时两种实现都不能完全匹配提供的实参。两个版本中,second
,third
,fourth
都是可选参数。所以无法确认应当选取哪种版本。编译器将其视为有歧义的方法调用,会提示错误。同理,下面的例子也是有歧义的调用:
OptMethod(1, third: "World"); OptMethod(1); OptMethod(second: 2.5, first: 1);
要想消去歧义,必须妥善安排参数位置并利用具名参数。
调试代码
让我们回到之前编写方法的例子。
internal class Program { private static int readInt() { string t = Console.ReadLine(); int ret = int.Parse(t); return ret; } static int CalcAdd(int a, int b) { int c = a + b; return c; //计算a+b的值存储在变量c中,然后将c的值作为返回值。 } static void Main(string[] args) { int num1 = readInt(); int num2 = readInt(); int sum = CalcAdd(num1, num2); Console.WriteLine(sum); } }
先前我们提到过,使用最左侧的按钮将以调试模式运行程序:
接下来让我们利用调试功能分析代码是如何运行的。
使用断点
我们在编辑器最左侧的灰色边框上单击,可以发现本行被标红:
然后我们以调试模式执行程序,输入两个整数后,发现程序中断在断点处:
其中的箭头和高亮行指示了当前正准备执行的代码。
查看和修改局部变量
在暂停状态下,你可以将鼠标悬停在变量上查看它们的值:
此外,在代码编辑器下方的窗口中也可以查看并修改局部变量:
我们可以尝试将c修改为8,观察现象:
可以发现程序的输出被修改。
此外,监视窗口中可实时计算表达式的值。VS的实时调试功能十分强大,你需要逐步探索相关的功能。
在下一章中,我们将学习判断语句的使用。
這些很基礎的C#語言,有些在Java語言有,卻也有Java裡面沒有的,不仔細看的話很容易漏掉。
像是方法後面用 => 的語法,不知道是不是lambda,抱歉我還沒學Java 8,只有學到Java 7。
還有調用overload(重載)方法的具名參數也是Java沒有的,因為Java只看調用參數順序,
所以不懂為什麼C#可以用具名參數,只要方法參數名稱一改變,不就調用錯誤了?
這麼說來也要避免落入語法糖的陷阱吧,雖然Java也不是沒有,但看起來C#好像有不少語法糖?