Day 5 - 方法与参数
建议用时:180-210 分钟
你将学会什么
- 方法是什么,为什么不能把代码一直堆在
Program.cs - 方法名、参数、返回值、方法体分别是什么
- 形参和实参有什么区别
void表示什么,什么时候用return是返回结果还是提前结束- 局部变量和作用域是什么
- 方法重载是什么,什么时候有用
- 如何把输入、校验、计算、输出拆成多个方法
本页只讲 C# 基础。今天的目标不是把代码拆得很多,而是让每个方法只负责一件清楚的事。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
从 Day 4 走到 Day 5
Day 4 的小票程序已经能完成输入、计算和输出,但所有代码都挤在一起。Day 5 不增加新的业务功能,只做一件事:给已有代码分组并起名字。
Day 4:decimal total = price * count;
Day 5:把这行计算装进 CalculateTotal 方法所以今天不是重新学一套 C#。你会继续使用 Day 4 的变量、判断和计算,只是把它们整理成有名字的动作。
今天的最低通过线
第一次学习只要求做到:
- 看见
CalculateTotal(10m, 2),知道这是“调用方法”。 - 看见
decimal CalculateTotal(...),知道这是“定义方法”。 - 知道参数是方法需要的数据,返回值是方法交回的结果。
- 能独立写出一个接收两个数字并返回相加结果的方法。
out、方法重载和复杂输入方法属于第二遍内容。第一次看不懂,可以先跳过,不影响完成今天的核心练习。
第一部分:先学原理和最小知识
这一部分先解决“这是什么、为什么要这样写”。每看到一个短例子就亲手敲一遍并运行,不要先背完整程序。
1. 方法到底是什么
方法就是一段有名字、可以重复使用的代码。
没有方法时,如果要打印两个标题,只能重复写:
Console.WriteLine("==== 商品小票 ====");
Console.WriteLine("==== 库存报表 ====");把“打印标题”这件事写成方法后,先写方法定义,再调用它:
void PrintTitle(string title)
{
Console.WriteLine("==== " + title + " ====");
}
PrintTitle("商品小票");
PrintTitle("库存报表");按从上往下阅读的顺序看:
第 1 步:定义 PrintTitle,说明这个方法怎么工作
第 2 步:调用 PrintTitle,把“商品小票”传进去
第 3 步:再次调用 PrintTitle,把“库存报表”传进去从今天开始,本课程的入门示例统一采用“先定义方法,后调用方法”。C# 编译器虽然也允许某些方法写在调用位置后面,但那是语法允许的排版,不适合作为第一次学习方法时的阅读顺序。
2. 一个方法由哪几部分组成
先只看定义,不急着调用:
decimal CalculateTotal(decimal price, int count)
{
return price * count;
}它由 4 个部分组成:
| 部分 | 这里是什么 | 用人话解释 |
|---|---|---|
| 返回类型 | decimal | 方法做完后会交回一个小数 |
| 方法名 | CalculateTotal | 这个动作叫“计算总价” |
| 参数列表 | decimal price, int count | 方法工作前需要单价和数量 |
| 方法体 | { ... } | 方法真正要执行的代码 |
定义方法时,末尾不要写分号:
decimal CalculateTotal(decimal price, int count)
{
return price * count;
}调用方法时,才在末尾写分号:
decimal total = CalculateTotal(19.9m, 3);3. 方法有几种
第一次学习方法,不要先背十几个名字。只问两个问题:
- 这个方法需要外面给它数据吗?
- 这个方法做完后需要交回结果吗?
组合起来就是 4 种基础方法:
| 种类 | 有参数吗 | 有返回值吗 | 适合做什么 |
|---|---|---|---|
| 第 1 种 | 没有 | 没有 | 固定打印一句话 |
| 第 2 种 | 有 | 没有 | 根据传入内容执行动作 |
| 第 3 种 | 没有 | 有 | 取得一个固定或内部产生的结果 |
| 第 4 种 | 有 | 有 | 根据传入数据计算并交回结果 |
第 1 种:无参数、无返回值
void SayHello()
{
Console.WriteLine("你好,开始学习方法");
}
SayHello();() 里面是空的,表示不需要参数;void 表示不返回结果。
第 2 种:有参数、无返回值
void PrintTitle(string title)
{
Console.WriteLine("==== " + title + " ====");
}
PrintTitle("商品小票");方法需要一个 string 参数,但只负责输出,所以返回类型是 void。
第 3 种:无参数、有返回值
string GetShopName()
{
return "小白商店";
}
string shopName = GetShopName();
Console.WriteLine(shopName);方法不需要参数,但会交回一个 string。
第 4 种:有参数、有返回值
int Add(int firstNumber, int secondNumber)
{
return firstNumber + secondNumber;
}
int result = Add(2, 3);
Console.WriteLine(result);方法需要两个 int 参数,做完后也会交回一个 int。
最常用的是第 2 种和第 4 种。今天只要能独立写出这两种,就已经达到最低通过线。
4. 定义和调用不是一件事
定义方法,是把规则写出来:
int Add(int firstNumber, int secondNumber)
{
return firstNumber + secondNumber;
}仅仅定义方法不会打印任何内容,也不会自动计算。程序执行到调用位置时,它才真正工作:
int result = Add(2, 3);
Console.WriteLine(result);可以记成:
定义方法 = 先写好一张做事说明书
调用方法 = 拿具体数据使用说明书5. 参数是什么
参数是方法工作时需要的数据。
decimal CalculateTotal(decimal price, int count)
{
return price * count;
}
decimal total = CalculateTotal(100m, 3);
Console.WriteLine(total);这里有两组容易混淆的名字:
| 位置 | 名字 | 这里的内容 |
|---|---|---|
| 方法定义里 | 形参 | price、count |
| 方法调用里 | 实参 | 100m、3 |
调用时,数据按位置传进去:
第 1 个实参 100m -> 第 1 个形参 price
第 2 个实参 3 -> 第 2 个形参 count所以参数的数量、顺序和类型都要对应。下面把顺序写反了,含义也会跟着变:
string BuildMessage(string name, int count)
{
return name + ",购买数量:" + count;
}
string message = BuildMessage("Keyboard", 2);
Console.WriteLine(message);6. 返回值是什么
有返回值的方法会把结果交回调用位置:
decimal CalculateTotal(decimal price, int count)
{
return price * count;
}
decimal total = CalculateTotal(50m, 4);
Console.WriteLine($"总价: {total:F2}");执行过程是:
price 得到 50m
count 得到 4
计算 50m * 4
return 把 200m 交回去
total 接住 200m返回类型和 return 后面的结果类型必须一致。方法写的是 decimal,就要交回 decimal。
7. void 是什么
void 表示“只做动作,不交回结果”。
void PrintLine(string name, decimal price, int count)
{
Console.WriteLine($"{name} x {count} = {(price * count):F2}");
}
PrintLine("Keyboard", 199m, 2);不能用变量接住 void 方法:
// decimal total = PrintLine("Keyboard", 199m, 2); // 编译错误因为 PrintLine 只负责显示,没有返回 decimal。
8. return 有哪两种作用
在有返回值的方法里,return 交回结果并结束方法:
decimal CalculateTotal(decimal price, int count)
{
return price * count;
}
Console.WriteLine(CalculateTotal(10m, 2));在 void 方法里,return; 可以提前结束方法:
void PrintName(string? name)
{
if (string.IsNullOrWhiteSpace(name))
{
Console.WriteLine("名称不能为空");
return;
}
Console.WriteLine(name.Trim());
}
PrintName(" Keyboard ");
PrintName("");9. 作用域是什么
方法里的参数和局部变量,通常只能在这个方法里面使用:
void PrintTotal()
{
decimal total = 100m;
Console.WriteLine(total);
}
PrintTotal();
// Console.WriteLine(total); // 编译错误:这里看不到 totaltotal 从 { 开始存在,到对应的 } 结束。这样不同方法里的变量不会互相干扰。
10. 方法什么时候该拆
满足下面任意一种情况,就可以考虑拆方法:
| 情况 | 示例 |
|---|---|
| 一段代码重复出现 | 多次打印小票 |
| 一段代码有明确动作 | 计算总价、读取数量 |
| 主流程太长 | 输入、校验、计算、输出混在一起 |
| 需要单独测试规则 | 折扣计算、库存判断 |
不要为了拆而拆。一个方法先只做一件清楚的事。
11. 方法重载是什么(第二遍选学)
重载就是方法名相同,但参数数量或参数类型不同。你之前一直在用的 Console.WriteLine 就有很多重载:
Console.WriteLine("Keyboard");
Console.WriteLine(199);
Console.WriteLine(true);输出:
Keyboard
199
True三行都调用 WriteLine,但传入的参数类型分别是 string、int、bool。C# 会根据参数类型选择对应版本。第一次学习只要能认出来,不要求今天自己写重载。
方法命名怎么写
方法名要表达动作,通常用动词开头:
| 不清楚 | 更清楚 |
|---|---|
DoIt | PrintReceipt |
Handle | ReadPrice |
Calc | CalculateTotal |
Check | IsValidName |
能用一句话解释的方法,通常更容易维护。
方法常用写法速查
方法最常见的不是一种写法,而是下面几种模板。以后写代码时,先判断自己属于哪一种。
| 需求 | 方法写法 | 例子 |
|---|---|---|
| 只做动作,不返回结果 | void | 打印标题、打印小票 |
| 计算后返回结果 | decimal、int、string | 计算总价、生成编号 |
| 判断是否满足条件 | bool | 判断名称是否有效 |
| 读取失败时返回空 | string?、Product? | 读取必填文本、按 Id 查商品 |
| 尝试转换并返回真假 | bool TryXxx(..., out T value) | TryReadInt、TryReadDecimal |
| 同名不同参数 | 重载 | 有折扣和无折扣的 CalculateTotal |
模板 1:只做动作
void PrintTitle(string title)
{
Console.WriteLine("==== " + title + " ====");
}这个方法只输出,不需要把结果交给外面,所以用 void。
模板 2:返回计算结果
decimal CalculateTotal(decimal price, int count)
{
return price * count;
}这个方法的重点是“给我单价和数量,我交回总价”。
模板 3:返回真假
bool IsValidPrice(decimal price)
{
return price > 0;
}返回 bool 的方法通常用 Is、Has、Can 开头。
模板 4:失败时返回 null
string? ReadRequiredText(string title)
{
Console.Write(title);
string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input))
{
return null;
}
return input.Trim();
}调用方看到 null,就知道读取失败或输入无效。
模板 5:TryXxx 加 out
这是第二遍选学模板。第一次学习只要知道 TryParse 会用到 out,不要求你自己设计带 out 的方法。
bool TryReadDecimal(string title, out decimal value)
{
Console.Write(title);
string? input = Console.ReadLine();
return decimal.TryParse(input, out value);
}这种写法的意思是:方法用返回值告诉你成功还是失败,用 out 把成功后的结果带出来。
今天写代码时按这个顺序
- 先写能运行的直线版代码。
- 找出重复或独立的动作。
- 给这段动作起一个清楚的方法名。
- 判断它需要哪些参数。
- 判断它是否要返回结果。
- 把方法调用放回主流程。
- 运行,确认拆方法前后结果一样。
第二部分:把知识组合成完整例子
前面已经学过最小知识。现在把它们组合起来,先读懂执行顺序,再完整敲一遍。今天最终要写一个小票程序,但不再把所有代码挤在一起,而是拆成几个方法。
先找两个动作,不要逐字符硬啃:
CalculateTotal -> 负责算
PrintReceipt -> 负责显示代码先定义 CalculateTotal,再定义会使用它的 PrintReceipt,最后两行才真正调用 PrintReceipt。阅读顺序和今天的学习顺序一致。
最终你要写出这个程序
decimal CalculateTotal(decimal price, int count)
{
return price * count;
}
void PrintReceipt(string name, decimal price, int count)
{
decimal total = CalculateTotal(price, count);
Console.WriteLine("----- 小票 -----");
Console.WriteLine($"商品: {name}");
Console.WriteLine($"单价: {price:F2}");
Console.WriteLine($"数量: {count}");
Console.WriteLine($"总价: {total:F2}");
}
PrintReceipt("Keyboard", 199m, 2);
PrintReceipt("Mouse", 59m, 3);输出:
----- 小票 -----
商品: Keyboard
单价: 199.00
数量: 2
总价: 398.00
----- 小票 -----
商品: Mouse
单价: 59.00
数量: 3
总价: 177.00今天要理解的是:PrintReceipt 负责打印小票,CalculateTotal 负责计算总价。两个方法职责不同,代码就更容易读。
第三部分:跟着敲代码
从这里开始动手。每个例子都可以直接放进 Program.cs 运行。
每个例子按三遍练:第一遍照着敲;第二遍关掉代码只看例子标题;第三遍更换参数和返回值。三遍都能运行,才进入下一个例子。
动手前先做这 3 件事
- 打开控制台项目,确认
dotnet run能运行。 - 每做一个例子,就把
Program.cs旧内容替换掉。 - 每次只改一个参数,看方法返回或输出怎么变。
例子 1:第一个有返回值的方法
decimal CalculateTotal(decimal price, int count)
{
return price * count;
}
decimal total = CalculateTotal(19.9m, 3);
Console.WriteLine($"总价: {total:F2}");预期输出:
总价: 59.70例子 2:同一个方法传不同参数
decimal CalculateTotal(decimal price, int count)
{
return price * count;
}
Console.WriteLine(CalculateTotal(10m, 2));
Console.WriteLine(CalculateTotal(20m, 3));
Console.WriteLine(CalculateTotal(5.5m, 4));预期输出:
20
60
22.0方法不关心你传的是哪件商品,只关心单价和数量。
例子 3:void 方法只负责输出
void PrintTitle(string title)
{
Console.WriteLine("==== " + title + " ====");
}
PrintTitle("商品小票");
PrintTitle("库存报表");预期输出:
==== 商品小票 ====
==== 库存报表 ====例子 4:返回 bool 的校验方法
bool IsValidName(string? name)
{
return !string.IsNullOrWhiteSpace(name);
}
Console.WriteLine(IsValidName("Keyboard"));
Console.WriteLine(IsValidName(""));
Console.WriteLine(IsValidName(" "));预期输出:
True
False
False返回 bool 的方法通常可以用 Is、Has、Can 开头。
例子 5(选学):用 out 读取整数
bool TryReadInt(string title, out int value)
{
Console.Write(title);
string? input = Console.ReadLine();
return int.TryParse(input, out value);
}
if (TryReadInt("数量: ", out int count))
{
Console.WriteLine($"数量是 {count}");
}
else
{
Console.WriteLine("数量格式不正确");
}输入 3 时输出:
数量: 3
数量是 3输入 abc 时输出:
数量: abc
数量格式不正确例子 6:把打印小票拆成方法
decimal CalculateTotal(decimal price, int count)
{
return price * count;
}
void PrintReceipt(string name, decimal price, int count)
{
decimal total = CalculateTotal(price, count);
Console.WriteLine("----- 小票 -----");
Console.WriteLine($"商品: {name}");
Console.WriteLine($"单价: {price:F2}");
Console.WriteLine($"数量: {count}");
Console.WriteLine($"总价: {total:F2}");
}
PrintReceipt("Keyboard", 199m, 2);
PrintReceipt("Mouse", 59m, 3);这里 PrintReceipt 调用了 CalculateTotal。一个方法可以调用另一个方法。
例子 7(选学):观察 WriteLine 的重载
Console.WriteLine("库存数量");
Console.WriteLine(20);
Console.WriteLine(19.9m);
Console.WriteLine(false);预期输出:
库存数量
20
19.9
False这里不是定义 4 个新方法,而是在调用 .NET 已经提供好的不同 WriteLine 重载。
常见错误和修法
| 错误 | 为什么错 | 修法 |
|---|---|---|
| 方法只写动作不写返回值 | 调用方拿不到结果 | 需要结果时写 return 和返回类型 |
| 参数名和变量名混乱 | 不知道数据从哪里来 | 参数表示传入数据,局部变量表示方法内部数据 |
| 方法里直接读写所有输入 | 方法难复用 | 把输入放在外面,方法只处理参数 |
| 忘记处理非法参数 | 方法被错误数据调用 | 在方法开头校验参数 |
| 方法太长 | 一段代码做了太多事 | 按读取、校验、计算、输出拆成多个方法 |
每日小测
先独立回答,再看参考答案。
1. 找调用和定义
下面哪一行是调用,哪一段是定义?
int Add(int a, int b)
{
return a + b;
}
int result = Add(2, 3);2. 找形参和实参
在上面的代码中,a、b 是什么?2、3 又是什么?
3. 预测返回值
Add(2, 3) 执行后,result 保存什么?
4. 找编译错误
下面代码为什么不能把结果放进 total?
void PrintTotal(decimal price, int count)
{
Console.WriteLine(price * count);
}
decimal total = PrintTotal(10m, 2);5. 判断职责
“读取商品名称、读取单价、计算总价、打印小票”全部放进一个 CalculateTotal 方法,好不好?为什么?
参考答案
Add(2, 3)是调用;带返回类型、方法名、参数和{ }的部分是定义。a、b是形参;2、3是调用时传入的实参。result保存5。PrintTotal的返回类型是void,它只输出,没有交回decimal。要么改成返回decimal,要么不要用变量接结果。- 不好。读取、计算、输出是不同职责。
CalculateTotal最好只接收单价和数量并返回总价。
如果第 1、3、4 题答错,先回看“方法结构拆开看”“调用和定义是两件事”“返回值是什么”。
上位机专项练习
方法可以把读取、换算、判断和显示拆成清楚的动作。下面继续坚持先定义方法,再调用方法。
下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。
专项例子 1:判断采集值是否报警
bool IsAlarm(double value, double limit)
{
return value >= limit;
}
bool result = IsAlarm(82.5, 80.0);
Console.WriteLine(result);运行结果或界面效果:
True改动任务: 把 82.5 改成 79.9,再运行。
专项例子 2:换算百分比工程值
double ConvertToPercent(int rawValue, int maxRawValue)
{
return rawValue * 100.0 / maxRawValue;
}
double percent = ConvertToPercent(2048, 4095);
Console.WriteLine($"{percent:F2}%");运行结果或界面效果:
50.01%改动任务: 用 0 和 4095 分别调用一次。
专项例子 3:打印设备状态
void PrintDeviceStatus(string name, bool isOnline)
{
string status = isOnline ? "在线" : "离线";
Console.WriteLine($"{name}: {status}");
}
PrintDeviceStatus("PLC-01", true);
PrintDeviceStatus("仪表-02", false);运行结果或界面效果:
PLC-01: 在线
仪表-02: 离线改动任务: 增加第三台设备并传入 true。
第四部分:作业完整答案
这一部分给出当天作业的完整答案。先按第三部分做一遍,再用这里检查。
作业要求
把商品小票程序拆成方法,要求:
- 主流程继续负责读取和校验输入,这是 Day 4 已经学过的内容。
CalculateTotal接收单价和数量,返回总价。PrintReceipt接收商品数据,只负责打印小票。- 不要求自己编写带
out的方法。
作业拆解
按这个顺序写:
- 先写
CalculateTotal的定义,用固定数字测试它的返回值。 - 再写
PrintReceipt的定义,确认它只输出、不返回结果。 - 然后写主流程,完成名称、单价和数量的读取与校验。
- 在主流程里调用两个已经写好的方法。
- 最后测试正常输入和错误输入。
完整代码
Console.Write("商品名称: ");
string? nameText = Console.ReadLine();
if (string.IsNullOrWhiteSpace(nameText))
{
Console.WriteLine("商品名称不能为空");
return;
}
Console.Write("单价: ");
string? priceText = Console.ReadLine();
if (!decimal.TryParse(priceText, out decimal price))
{
Console.WriteLine("单价必须是数字");
return;
}
Console.Write("数量: ");
string? countText = Console.ReadLine();
if (!int.TryParse(countText, out int count))
{
Console.WriteLine("数量必须是整数");
return;
}
if (price < 0 || count <= 0)
{
Console.WriteLine("单价或数量范围不正确");
return;
}
string name = nameText.Trim();
decimal CalculateTotal(decimal price, int count)
{
return price * count;
}
void PrintReceipt(string name, decimal price, int count, decimal total)
{
Console.WriteLine("----- 小票 -----");
Console.WriteLine($"商品: {name}");
Console.WriteLine($"单价: {price:F2}");
Console.WriteLine($"数量: {count}");
Console.WriteLine($"总价: {total:F2}");
}
decimal total = CalculateTotal(price, count);
PrintReceipt(name, price, count, total);正确运行结果
正常输入:
商品名称: Keyboard
单价: 199
数量: 2
----- 小票 -----
商品: Keyboard
单价: 199.00
数量: 2
总价: 398.00错误输入要分别测试:
商品名称为空:提示 商品名称不能为空
单价输入 abc:提示 单价必须是数字
数量输入 2.5:提示 数量必须是整数
数量输入 0:提示 单价或数量范围不正确你要能说清楚这 7 件事
- 方法名说明这段逻辑做什么。
- 参数说明方法需要外部提供什么数据。
- 返回值说明方法会交回什么结果。
void方法只做动作,不返回结果。return可以返回结果,也可以提前结束。- 方法内部变量通常不能在方法外直接使用。
- 重载是同名方法配不同参数,不是随便复制代码。