当同一段计算第三次出现在项目里,复制粘贴通常已经不是答案。方法(method)把行为命名,把输入、输出与副作用写成明确契约;调用方只需要理解契约,不必每次钻进实现细节。
本章沿着一条可运行的订单计算示例逐步前进:先声明和调用方法,再处理重载与参数,接着讨论返回值、作用域和递归,最后把 Lambda 当作数据传入高阶函数。你将看到完整代码、确定输出、常见失败方式,以及每节紧跟的自测题。
学习路线:方法契约 → 调用与重载 → 参数传递 → 返回组合结果 → 局部逻辑与递归 → Lambda、高阶函数和闭包。

下面的方法把“根据单价和数量计算小计”命名为 Subtotal:
static decimal Subtotal(decimal unitPrice, int quantity)
{
if (unitPrice < 0)
throw new ArgumentOutOfRangeException(nameof(unitPrice));
if (quantity < 0)
throw new ArgumentOutOfRangeException(nameof(quantity));
return unitPrice * quantity;
}
decimal amount = Subtotal(19.90m, 3);
Console.WriteLine($"subtotal={amount:F2}");subtotal=59.70声明里的 unitPrice、quantity 是形参,调用时的 19.90m、3 是实参。返回类型 decimal 告诉调用方成功后会得到什么;return 结束本次调用并交回结果。没有结果的方法使用 void。
表达式体适合只有一个表达式的短方法:
static decimal ApplyRate(decimal amount, decimal rate) => amount * rate;方法名应使用 PascalCase,并优先使用动作或动作短语,例如 CalculateTotal、TryParseOrder。如果方法名需要用 “And” 串起多个不相干动作,它很可能承担了过多职责。
同一作用域可以有多个同名方法,只要参数列表能够区分。这叫方法重载:
static decimal Shipping(decimal weight) => 8m + weight * 1.5m;
static decimal Shipping(decimal weight, bool express) =>
express ? 15m + weight * 2.5m : Shipping(weight);
Console.WriteLine($"normal={Shipping(2m
normal=11.00
express=20.00用于区分重载的方法签名关注方法名、类型参数个数,以及每个参数的类型、顺序和传递方式。返回类型不参与重载区分,所以下面两行不能共存:
// 编译错误:不能只靠返回类型区分
static int Convert(string text) => int.Parse(text);
static double Convert(string text) => double.Parse(text);调用发生时,编译器从可用候选中寻找最佳匹配。完全匹配通常优于需要隐式转换的版本;如果两个候选都同样合适,调用会因歧义而编译失败。不要依靠微妙转换设计一组“猜谜式”重载。
当多个参数类型相同,具名参数能直接表达意图;可选参数则为常用情况提供默认值:
static decimal CalculateTotal(
decimal subtotal,
decimal discountRate = 0m,
decimal shipping = 0m)
{
return subtotal * (1 - discountRate) + shipping;
}
decimal total = CalculateTotal(
subtotal: 200m,
shipping: 12m,
total=192.00可选参数通常放在必选参数之后,默认值必须是编译期可确定的常量、default 等合法形式。默认值会写入调用方编译结果:公共库若修改默认值,旧调用方通常需要重新编译才会采用新值。
params 让调用方传入零个或多个同类型实参:
static decimal Sum(params decimal[] values)
{
decimal total = 0m;
foreach (decimal value in values) total += value;
return total;
}
Console.WriteLine(Sum(10m, 20m, 5.5m));
Console.WriteLine(Sum());35.5
0一个参数列表只能有一个 params 参数,并且它必须位于最后。方法内部把它当作集合/数组使用;高频性能路径要留意调用时可能产生的分配。

C# 默认按值传参:方法得到实参值的一份副本。引用类型的“值”是对象引用,因此方法能通过副本修改同一个对象的内容,却不能在默认传参下替换调用方变量本身。
ref、out、in 让形参成为调用方存储位置的别名:
static void ApplyDiscount(ref decimal total, decimal rate)
{
total *= 1 - rate;
}
static bool TrySplit(decimal total, int people, out decimal each)
{
if (people <= 0)
{
each = 0m
bill=270.00, ok=True, each=90.00声明点与调用点都要写 ref、out 或 in,这让副作用在调用处可见。普通业务结果优先用返回值;不要为了“一次返回多个值”就堆叠多个 out。
少量、紧密相关的临时结果可以装进命名元组:
static (decimal min, decimal max, decimal average) Analyze(decimal[] values)
{
if (values.Length == 0)
throw new ArgumentException("values 不能为空", nameof(values));
decimal min = values[0], max = values[0], sum
min=10, max=20, avg=14.00min、max、average 让调用点比 Item1、Item2 清楚;解构又能把三个结果直接放入局部变量。如果结果需要跨 API 边界、长期保存、增加行为或持续演进,应定义 record/class,让业务概念拥有稳定名称。

只服务于一个方法的辅助逻辑,可以放进局部函数,缩小可见范围:
static decimal CalculateCheckout(decimal[] prices)
{
if (prices is null) throw new ArgumentNullException(nameof(prices));
decimal Normalize(decimal price) => Math.Max(0m, price);
decimal total = 0m;
foreach (decimal price in
局部变量通常只在声明它的代码块中可见。局部函数遵循词法作用域,可以读取外层局部变量,这会形成捕获。若辅助函数不需要外部状态,可加 static,让编译器阻止意外捕获:
static decimal CalculateTax(decimal amount)
{
return Round(amount * 0.06m);
static decimal Round(decimal value) => Math.Round(value, 2);
}局部函数可以写在调用语句之后,但可读性仍应优先:让读者容易看出主流程与辅助步骤的关系。
递归包含两个不可缺少的部分:停止展开的基线条件,以及让问题规模缩小的递归步骤。
static int CountItems(Category category)
{
int count = category.ItemCount;
foreach (Category child in category.Children)
count += CountItems(child);
return count;
}上例的叶子分类没有子节点,循环自然结束,这就是基线情形。处理数字时通常写得更显式:
static int SumTo(int n)
{
if (n <= 0) return 0;
return n + SumTo(n - 1);
}
Console.WriteLine(SumTo(4));10每次递归调用都会占用调用栈。C# 代码不应假定运行时一定消除尾递归;深度可能达到成千上万时,循环或显式 Stack<T> 通常更稳。树、目录和表达式等天然嵌套结构则常适合递归。
Lambda 是没有单独名称的函数表达式。表达式 Lambda 适合单步计算,语句 Lambda 适合多步逻辑:
Func<decimal, decimal> addTax = price => price * 1.06m;
Func<decimal, decimal> normalize = price =>
{
decimal safe = Math.Max(0m, price);
return Math.Round(safe, 2);
};接收函数或返回函数的方法常称为高阶函数。下面把遍历规则固定下来,把转换策略交给调用方:
static decimal[] Transform(
decimal[] source,
Func<decimal, decimal> projector)
{
decimal[] result = new decimal[source.Length];
for (int i = 0; i < source.Length; i++)
result[i] = projector(source[i]);
return result;
10.60, 21.20Func<T, TResult> 表示有返回值的函数形状,Action<T> 表示没有返回值的行为。这里先关注“行为可作为值传递”,委托链与事件发布订阅会在专门章节展开。
Lambda 读取外层局部变量时形成闭包。它捕获的是变量对应的存储位置,而不是创建 Lambda 时自动复制一份值:
decimal rate = 0.10m;
Func<decimal, decimal> discount = amount => amount * (1 - rate);
Console.WriteLine(discount(100m));
rate = 0.20m;
Console.WriteLine(discount(100m));90.00
80.00闭包能自然表达配置和累积状态,但状态变化也可能变得隐蔽。若 Lambda 不需要外部变量,可以写 static Lambda:
Func<decimal, decimal> clamp = static value => Math.Max(0m, value);编译器会阻止它捕获外层局部变量或实例状态。这既能表达意图,也可能避免闭包对象和额外分配。需要配置时,把配置作为显式参数传递,通常比依赖可变外部状态更容易测试。

方法设计的目标不是“语法最短”,而是让调用点诚实、可预测、可测试。
实践时优先写小而完整的方法:一个方法负责一个可描述的结果;参数数量过多时考虑参数对象;纯计算尽量做到相同输入产生相同输出。测试时至少覆盖正常值、边界值、非法值和空集合。
方法是一份可执行契约:名称表达意图,参数定义输入,返回类型定义结果,方法体实现规则。重载必须通过参数列表区分;位置、具名、可选与 params 参数解决不同的调用体验;默认按值传递,而 ref、out、in 显式改变变量别名语义。
命名元组适合短期组合结果,局部函数收窄辅助逻辑的作用域,递归适合天然分层的问题。Lambda 让行为成为值,高阶函数负责组合行为,闭包则提醒我们关注被捕获的可变状态;不需要捕获时,static 局部函数与 static Lambda 能把意图写进代码。