1方法是什么
方法就是把一组相关的语句打包、起个名字,用来完成一件事。写好一次,到处都能调用,同样的代码就不用重复写。
你写过的程序里都有一个方法 Main,程序从它开始运行。完整写法的程序至少有一个 Main;用顶级语句写法(不写类和 Main,代码从第一行开始执行)时,编译器会替你生成入口方法:
| 1 | Console.WriteLine("Hello, World!"); | // 整个程序只有这一行,照样能运行,输出 Hello, World! |
使用方法分两步:先定义,再调用。
2定义方法
定义方法有五个部分:
- 访问修饰符:决定谁能调用,常见的有 public、private、protected、internal,不写默认是 private。
- 返回类型:方法交回的结果是什么类型,什么都不交回就写
void。 - 方法名:习惯用每个单词首字母大写的 PascalCase。
- 参数列表:圆括号里,方法要接收的输入,多个用逗号隔开,也可以一个都没有。
- 方法体:花括号里,真正干活的代码。
C# 里的方法签名只看方法名和参数列表,访问修饰符和返回类型都不算,讲重载时会用到这一点。
3调用方法
FindMax 找出两个数里较大的那个:public,返回 int,接收两个 int 参数。方法定义里接收输入的变量叫形参,调用时传进去的值叫实参。
| 8 | public int FindMax(int num1, int num2) | // public、返回 int、方法名 FindMax、两个 int 形参 |
| 9 | { | // FindMax 开始 |
| 10 | if (num1 > num2) | // 比较两个数:100 > 200 为假 |
| 11 | return num1; | // num1 大就交回 num1 |
| 12 | else | // 否则 |
| 13 | return num2; | // return 把 200 交回去 |
| 14 | } | // FindMax 结束 |
| 19 | NumberHelper helper = new NumberHelper(); | // 先 new 一个 NumberHelper 对象 |
| 20 | int result = helper.FindMax(100, 200); | // 实参 100、200 交给形参,返回的 200 存进 result |
| 21 | | |
| 22 | Console.WriteLine($"最大值是:{result}"); // 最大值是:200 | // $"..." 里的 {result} 换成变量的值,输出“最大值是:200” |
调用分四步:传实参、形参接收、执行方法体、返回结果。
FindMax 没有写 static,它属于对象,得先有对象才能调用。去掉对象,直接在 Main 里调用就报错:
| 17 | int result = FindMax(100, 200); | // 没有对象,直接调用非 static 方法 |
Program.cs(17,26): error CS0120: 对象引用对于非静态的字段、方法或属性“NumberHelper.FindMax(int, int)”是必需的
方法是 public 的,另一个类也能先创建对象再调用它。写了 static 的方法属于这个类本身,不用创建对象,后面就会见到。
4三种传参方式
- 按值传递(默认):传的是值的副本。
- ref(按引用传递):方法改的就是原来的变量。
- out(输出参数):让方法把结果写回调用方。
区别在于方法和调用方用不用同一块内存。这里只讲最常用的这三种。
按值传递
| 8 | public void Swap(int x, int y) | // 按值传递:x、y 是 a、b 的副本 |
| 9 | { | // Swap 开始 |
| 10 | int temp = x; | // temp 先存下 x(100) |
| 11 | x = y; | // x 变成 200 |
| 12 | y = temp; | // y 变成 100 |
| 13 | Console.WriteLine($" 方法内:x={x}, y={y}"); // 交换成功 | // 方法里面确实换过来了 |
| 14 | } | // Swap 结束:副本扔掉 |
Main 里 a 是 100、b 是 200,调用 p.:
调用前:a=100, b=200 方法内:x=200, y=100 调用后:a=100, b=200
ref:按引用传递
定义和调用时参数前面都要写 ref(定义是 Swap()。这次 x 不是副本,它和 a 用的是同一块内存,交换 x 和 y 就是在交换 a 和 b。
| 21 | p.Swap(ref a, ref b); // 调用时也必须加 ref | // 交换的是 a、b 本身 |
| 22 | Console.WriteLine($"调用后:a={a}, b={b}"); // 交换成功! | // 实测输出“调用后:a=200, b=100” |
调用时忘了写 ref,编译器报错 CS1620(参数必须与关键字“ref”一起传递)。ref 的变量传进去之前还必须先赋值:
| 17 | int a, b; | // 只声明,没赋值 |
| 18 | p.Swap(ref a, ref b); | // 把没赋值的变量用 ref 传进去 |
Program.cs(18,24): error CS0165: 使用了未赋值的局部变量“a” Program.cs(18,31): error CS0165: 使用了未赋值的局部变量“b”
out:输出参数
out 正好反过来:变量传进去之前不用赋值,但方法结束之前必须给 out 参数赋值,忘了就报错 CS0177(控制离开当前方法之前必须对 out 参数赋值)。
教程的 GetValues 用两个 out 参数一次交回两个值;更常见的是 TryDivide 这种写法:返回值只说成没成功,真正的结果从 out 参数交出来。
| 19 | public bool TryDivide(int a, int b, out double result) | // 返回 bool 表示成没成功,结果从 out 参数 result 交出 |
| 20 | { | // TryDivide 开始 |
| 21 | if (b == 0) | // 除数是 0 |
| 22 | { | // if 块开始 |
| 23 | result = 0; | // out 参数也得赋值 |
| 24 | return false; // 除数为零 | // 返回 false:没成功 |
| 25 | } | // if 块结束 |
| 26 | result = (double)a / b; | // (double) 先把 a 转成小数再除:10 / 3 = 3.333… |
| 27 | return true; | // 返回 true:成功了 |
查看完整代码(44 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | | |
| 3 | namespace ParameterDemo | // 定义命名空间 ParameterDemo |
| 4 | { | // 命名空间开始 |
| 5 | class Program | // 定义 Program 类 |
| 6 | { | // Program 类开始 |
| 7 | // out 参数:方法负责赋值 | |
| 8 | public void GetValues(out int x, out int y) | // 两个 out 参数,一次交回两个值 |
| 9 | { | // GetValues 开始 |
| 10 | Console.Write("请输入第一个值:"); | // 提示输入,不换行 |
| 11 | x = Convert.ToInt32(Console.ReadLine()); | // 读入一行文字,Convert.ToInt32 转成整数,写进 x |
| 12 | | |
| 13 | Console.Write("请输入第二个值:"); | // 提示输入 |
| 14 | y = Convert.ToInt32(Console.ReadLine()); | // 读入并写进 y |
| 15 | // 注意:方法结束前必须为所有 out 参数赋值,否则编译错误 | |
| 16 | } | // GetValues 结束 |
| 17 | | |
| 18 | // 实用示例:TryParse 模式 | |
| 19 | public bool TryDivide(int a, int b, out double result) | // 返回成没成功,结果从 result 交出 |
| 20 | { | // TryDivide 开始 |
| 21 | if (b == 0) | // 除数是 0 |
| 22 | { | // if 块开始 |
| 23 | result = 0; | // 也要给 result 赋值 |
| 24 | return false; // 除数为零 | // 返回 false |
| 25 | } | // if 块结束 |
| 26 | result = (double)a / b; | // 转成小数再除 |
| 27 | return true; | // 返回 true |
| 28 | } | // TryDivide 结束 |
| 29 | | |
| 30 | static void Main(string[] args) | // 程序入口 Main 方法 |
| 31 | { | // Main 开始 |
| 32 | var p = new Program(); | // var 让编译器自己推断类型,这里是 Program |
| 33 | | |
| 34 | // out 参数无需初始化 | |
| 35 | int a, b; | // 只声明,不赋值 |
| 36 | p.GetValues(out a, out b); | // 两次都输入 5 |
| 37 | Console.WriteLine($"a={a}, b={b}"); | // 打印 a=5, b=5 |
| 38 | | |
| 39 | // TryParse 模式 | |
| 40 | if (p.TryDivide(10, 3, out double res)) | // 调用时直接声明 out 变量 res |
| 41 | Console.WriteLine($"10 / 3 = {res:F2}"); // 3.33 | // :F2 表示保留两位小数 |
| 42 | } | // Main 结束 |
| 43 | } | // Program 类结束 |
| 44 | } | // 命名空间结束 |
运行时两次都输入 5:
请输入第一个值:5 请输入第二个值:5 a=5, b=5 10 / 3 = 3.33
int 自带的解析方法 int. 就是这种写法。从 C# 7.0 开始,还能在调用时直接声明 out 变量:
| 10 | if (int.TryParse("123", out int number)) | // 把文字 "123" 解析成整数;number 不用提前声明 |
| 11 | Console.WriteLine($"解析成功:{number}"); | // 解析成功,打印“解析成功:123” |
5递归
方法调用它自己,叫递归。递归要有两样东西:基准条件让递归停下来;递归步骤把问题拆成更小的同类问题。教程的例子是阶乘:n 的阶乘等于 n 乘以 n − 1 的阶乘。
| 10 | // 基准条件:0! = 1,1! = 1 | |
| 11 | if (n <= 1) | // n 小于等于 1 |
| 12 | return 1; | // 直接返回 1,不再往下调用 |
| 13 | | |
| 14 | // 递归步骤:n! = n × (n-1)! | |
| 15 | return n * Factorial(n - 1); | // 调用自己算 n - 1 的阶乘,再乘以 n |
拿 4 来看:每调用一次,调用栈(记录方法调用到哪一层的一块内存)上就叠一层。4 调用 3,3 调用 2,2 调用 1,到了 1 碰上基准条件;然后从最上面一层往回交结果:1、2、6,最后是 24(实测输出 4! = 24)。Main 里再算 6、7、8 的阶乘:
6! = 720 7! = 5040 8! = 40320
把基准条件删掉,方法会一直调用自己,调用栈越叠越高,最后栈溢出,程序直接崩溃退出:
| 10 | return n * Factorial(n - 1); | // 去掉了 if (n <= 1) return 1;,永远在调用自己 |
查看完整输出(4 行)
Stack overflow. Repeated 174212 times: -------------------------------- at RecursionDemo.Program.Factorial(Int32)
这就是 StackOverflowException(栈溢出异常),try catch(捕获异常的语句,后面会讲)也接不住。递归层数可能很深时,可以考虑改用循环。
6默认参数和命名参数
参数可以设默认值,调用时不传就用默认值;命名参数是在实参前面写上参数名和冒号,写了参数名,顺序就能打乱。
| 8 | static double Power(double baseNum, int power = 2) | // power 默认值是 2;写了 static,Main 里不用创建对象 |
| 17 | static void PrintInfo(string name, int age = 18, string city = "未知") | // age 默认 18,city 默认“未知” |
| 25 | Console.WriteLine(Power(3)); // 9.0(使用默认 power=2) | // 只传 3,power 用默认的 2:实测打印 9 |
| 26 | Console.WriteLine(Power(3, 3)); // 27.0 | // 默认值被换掉:3 × 3 × 3,实测打印 27 |
| 27 | | |
| 28 | // 命名参数:跳过 age,只指定 city | |
| 29 | PrintInfo("小明", city: "北京"); | // city: 是命名参数;跳过 age,age 用默认的 18 |
| 30 | // 输出:小明, 18岁, 北京 | |
| 31 | | |
| 32 | // 命名参数可以不按顺序 | |
| 33 | PrintInfo(city: "上海", name: "小红", age: 25); | // 先写 city,顺序打乱,实测输出“小红, 25岁, 上海” |
double 值是整数时打印出来不带小数点,所以 Power(3) 打印 9,Power(3, 3) 打印 27。
7表达式体方法
方法体只有一句 return 时,可以去掉花括号和 return,用箭头 => 接上表达式,C# 6.0 开始支持。箭头后面只能跟一个表达式,不能放 if 这样的语句。
| 8 | public int Add(int a, int b) | // 传统写法 |
| 9 | { | // Add 开始 |
| 10 | return a + b; | // 只有一句 return |
| 11 | } | // Add 结束 |
| 12 | | |
| 13 | // 表达式体写法(等价) | |
| 14 | public int Add2(int a, int b) => a + b; | // 箭头后面直接写要交回的表达式 |
两种写法算 1 加 2,都输出 3。
8方法重载
同一个类里可以有好几个同名的方法,只要参数列表不同(类型、个数或顺序不一样),这叫重载。Calculator 里有三个 Add:两个 int、三个 int、两个 double,调用时编译器看你传了什么参数,自动挑出对应的那一个。
| 19 | Console.WriteLine(calc.Add(1, 2)); // 3(调用重载 1) | // 两个 int,选重载 1,打印 3 |
| 20 | Console.WriteLine(calc.Add(1, 2, 3)); // 6(调用重载 2) | // 三个 int,选重载 2,打印 6 |
| 21 | Console.WriteLine(calc.Add(1.5, 2.5)); // 4.0(调用重载 3) | // 两个 double,选重载 3,实测打印 4 |
只有返回类型不同,不算重载:
| 9 | public double Add(int a, int b) => a + b; | // 参数和上一个 Add 完全一样,只是返回 double |
Program.cs(9,23): error CS0111: 类型“Calculator”已定义了一个名为“Add”的具有相同参数类型的成员
一个类里,方法名可以重复,不能重复的是签名。
9小结
- 定义方法要写返回类型、方法名和参数,不返回值就写 void。
- 按值传递改的是副本,ref 共享内存,out 必须在方法里赋值。
- 默认参数调用时可以省略,命名参数可以不按顺序。
- 单行方法用箭头简写,同名不同参数就是重载。
下一节:int 这种值类型,能不能“没有值”?
