10-函数(方法)
函数(又称方法)是将一段具有特定功能的代码封装在一起,便于重复调用。函数是 C# 程序组织代码的基本单元,也是代码复用的核心手段。
一、函数的核心概念
为什么使用函数?
| 不使用函数 | 使用函数 |
|---|---|
| 重复代码到处复制 | 一处定义,多处调用 |
| 修改需要改所有复制点 | 只改函数定义即可 |
| 代码难以阅读和理解 | 通过函数名即可理解功能 |
| 逻辑分散,测试困难 | 可单独测试每个函数 |
函数定义语法
csharp
访问修饰符 返回值类型 方法名(参数列表)
{
// 方法体
return 返回值; // 如果返回值类型为 void,则不需要
}组成部分详解
| 组成部分 | 说明 | 示例值 |
|---|---|---|
| 访问修饰符 | 控制方法的访问范围 | public(公开)、private(私有)、internal(程序集内)、protected(派生类) |
| 返回值类型 | 方法返回的数据类型,无返回值用 void | int、string、bool、void、自定义类型 |
| 方法名 | 方法的标识符,使用 PascalCase(首字母大写) | CalculateTotal、GetUserName |
| 参数列表 | 方法接收的输入值(可以为空) | (int x, int y)、(string name) |
| 方法体 | 方法执行的代码块 | { return x + y; } |
二、基本示例
csharp
// 1. 无参数,无返回值
static void SayHello()
{
Console.WriteLine("你好!欢迎使用本系统。");
}
// 2. 有参数,无返回值
static void Greet(string name)
{
Console.WriteLine($"你好,{name}!");
}
// 3. 有参数,有返回值
static int Add(int a, int b)
{
return a + b;
}
// 4. 无参数,有返回值
static string GetVersion()
{
return "v1.0.0";
}
// 调用
SayHello(); // 输出:你好!欢迎使用本系统。
Greet("张三"); // 输出:你好,张三!
int result = Add(3, 5); // 8
string version = GetVersion(); // "v1.0.0"三、参数传递方式详细对比
C# 提供了多种参数传递方式,理解它们的区别至关重要。
1. 值参数(默认)
传递的是值的副本,方法内修改不影响原变量。
csharp
static void ChangeValue(int x)
{
x = 100; // 只修改副本
Console.WriteLine($"方法内:{x}"); // 输出:100
}
int num = 10;
ChangeValue(num);
Console.WriteLine($"方法外:{num}"); // 输出:10(未改变)内存原理:
num和x在栈上有两个独立的内存空间,方法内修改的是x的空间。
2. ref 参数(引用传递)
传递的是变量的引用,方法内修改会影响原变量。
csharp
static void ChangeRef(ref int x)
{
x = 100; // 直接修改原变量
}
int num = 10;
ChangeRef(ref num); // 调用时必须加 ref
Console.WriteLine(num); // 输出:100(已改变)特点: 调用前变量必须初始化,
ref参数在方法内可以读也可以写。
3. out 参数(输出参数)
用于从方法返回多个值,调用前无需初始化,但方法内必须赋值。
csharp
static void GetMinMax(int[] numbers, out int min, out int max)
{
min = numbers[0];
max = numbers[0];
foreach (int n in numbers)
{
if (n < min) min = n;
if (n > max) max = n;
}
// 方法结束前 min 和 max 必须被赋值
}
int[] arr = { 3, 7, 1, 9, 4 };
GetMinMax(arr, out int min, out int max);
Console.WriteLine($"最小值:{min},最大值:{max}"); // 输出:最小值:1,最大值:9
// 使用下划线忽略不需要的参数(C# 7.0+)
GetMinMax(arr, out _, out int onlyMax);
Console.WriteLine($"最大值:{onlyMax}");4. in 参数(只读引用)
传递引用但方法内不能修改,适合传递大型结构体以避免复制开销。
csharp
static void PrintValue(in int x)
{
// x = 100; // 编译错误:in 参数不可修改
Console.WriteLine(x);
}
int num = 42;
PrintValue(in num); // in 可以省略5. params 参数(可变参数)
允许传入任意数量的参数,编译器自动打包为数组。
csharp
static int Sum(params int[] numbers)
{
int total = 0;
foreach (int n in numbers)
total += n;
return total;
}
Console.WriteLine(Sum(1, 2, 3)); // 输出:6
Console.WriteLine(Sum(1, 2, 3, 4, 5)); // 输出:15
Console.WriteLine(Sum()); // 输出:0(不传参数也合法)
Console.WriteLine(Sum(new int[] { 10, 20 })); // 也可以直接传数组规则:
params必须是方法参数列表中的最后一个参数。
参数传递方式对比
| 方式 | 关键字 | 是否必需初始化 | 方法内能否修改 | 是否能影响原变量 |
|---|---|---|---|---|
| 值传递 | (无) | 是 | 可以修改副本 | 否 |
| 引用传递 | ref | 是(调用前) | 可以修改 | 是 |
| 输出参数 | out | 否(调用前) | 必须赋值 | 是 |
| 只读引用 | in | 是 | 不能修改 | 否 |
| 可变参数 | params | — | — | — |
四、可选参数和命名参数
可选参数(默认值)
给参数指定默认值,调用时可省略该参数。
csharp
static void CreateUser(string name, int age = 18, string city = "北京")
{
Console.WriteLine($"创建用户:{name},{age}岁,来自{city}");
}
CreateUser("张三"); // 使用全部默认值
CreateUser("李四", 25); // 指定 age,city 使用默认
CreateUser("王五", 30, "上海"); // 全部指定规则: 可选参数必须放在必需参数之后。
命名参数
调用时指定参数名称,可以不按顺序传递,提高可读性。
csharp
static void Register(string account, string password, string email, string phone)
{
Console.WriteLine($"注册账户:{account},邮箱:{email}");
}
// 普通调用
Register("zhangsan", "123456", "zhang@test.com", "13800000000");
// 命名参数调用(顺序可以打乱)
Register(
account: "zhangsan",
phone: "13800000000",
email: "zhang@test.com",
password: "123456"
);
// 混合使用(命名参数在最后)
Register("zhangsan", "123456", email: "zhang@test.com", phone: "13800000000");五、方法重载(Overload)
同一个类中允许定义多个同名方法,但参数列表必须不同。编译器根据传入的参数决定调用哪个版本。
csharp
public class Calculator
{
// 两个整数相加
public int Add(int a, int b)
{
return a + b;
}
// 三个整数相加(参数数量不同)
public int Add(int a, int b, int c)
{
return a + b + c;
}
// 浮点数相加(参数类型不同)
public double Add(double a, double b)
{
return a + b;
}
// 数组求和(参数类型不同)
public int Add(int[] numbers)
{
int sum = 0;
foreach (int n in numbers) sum += n;
return sum;
}
}
Calculator calc = new Calculator();
Console.WriteLine(calc.Add(1, 2)); // 3
Console.WriteLine(calc.Add(1, 2, 3)); // 6
Console.WriteLine(calc.Add(1.5, 2.5)); // 4.0
Console.WriteLine(calc.Add(new[] { 1,2,3,4,5 })); // 15重载规则
| 规则 | 说明 | 示例 |
|---|---|---|
| 参数数量不同 | 参数个数不同 | Add(int, int) vs Add(int, int, int) |
| 参数类型不同 | 参数类型不同 | Add(int, int) vs Add(double, double) |
| 参数顺序不同 | 参数顺序不同 | Method(int, string) vs Method(string, int) |
| 返回值不同 | 不能仅靠返回值区分重载 | 编译错误 |
六、局部函数(C# 7.0+)
在方法内部定义方法,作用域仅限于包含它的方法。
csharp
static void ProcessOrder(int orderId)
{
// 验证订单
bool Validate()
{
return orderId > 0;
}
// 计算折扣
decimal CalculateDiscount(decimal amount)
{
return amount * 0.9m;
}
// 发送通知
void Notify(string message)
{
Console.WriteLine($"[订单 {orderId}] {message}");
}
if (Validate())
{
decimal total = CalculateDiscount(100);
Notify($"订单处理完成,金额:{total}");
}
}局部函数的特性
- 可以访问包含它的方法中的变量(闭包)
- 包含
static关键字的局部函数不能访问外部变量(C# 8.0+) - 可以在迭代器方法和异步方法中使用
csharp
// 静态局部函数(C# 8.0+)
void Calculate()
{
int multiplier = 10;
// 静态局部函数不能访问外部变量
static int Multiply(int x, int y) => x * y;
// 普通局部函数可以访问外部变量
int MultiplyBy(int x) => x * multiplier;
Console.WriteLine(Multiply(3, 5)); // 15
Console.WriteLine(MultiplyBy(3)); // 30
}七、递归函数
函数调用自身称为递归。递归必须有终止条件,否则会栈溢出。
经典案例:阶乘
csharp
// n! = n × (n-1) × (n-2) × ... × 1
static int Factorial(int n)
{
if (n <= 1)
return 1; // 终止条件:0! = 1! = 1
return n * Factorial(n - 1); // 递归调用
}
Console.WriteLine(Factorial(5)); // 输出:120执行过程:
Factorial(5) = 5 * Factorial(4)
= 5 * 4 * Factorial(3)
= 5 * 4 * 3 * Factorial(2)
= 5 * 4 * 3 * 2 * Factorial(1)
= 5 * 4 * 3 * 2 * 1
= 120案例:斐波那契数列
csharp
// Fibonacci: 1, 1, 2, 3, 5, 8, 13, 21...
static int Fibonacci(int n)
{
if (n <= 2)
return 1;
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
// 打印前 10 项
for (int i = 1; i <= 10; i++)
Console.Write($"{Fibonacci(i)} ");
// 输出:1 1 2 3 5 8 13 21 34 55注意: 递归虽然代码简洁,但性能较差(重复计算)。可用循环或记忆化优化。
递归 vs 循环
| 对比 | 递归 | 循环 |
|---|---|---|
| 代码简洁性 | 好(自然表达分治思想) | 一般 |
| 性能 | 较差(函数调用开销) | 好 |
| 栈溢出风险 | 有(层级过深时) | 无 |
| 适用场景 | 树遍历、分治算法 | 一般迭代 |
八、表达式主体方法(箭头方法,C# 6.0+)
对于单行方法,可以使用 => 简化语法。
csharp
// 传统写法
static int Add(int a, int b)
{
return a + b;
}
// 箭头方法
static int Add(int a, int b) => a + b;
// 无返回值
static void Print(string msg) => Console.WriteLine(msg);
// 只读属性
public string FullName => $"{FirstName} {LastName}";
// 构造函数
public Person(string name) => Name = name;九、综合案例
学生成绩管理系统(函数版)
csharp
class Program
{
static List<int> scores = new List<int>();
static void Main()
{
while (true)
{
ShowMenu();
string choice = Console.ReadLine();
switch (choice)
{
case "1": AddScore(); break;
case "2": ShowScores(); break;
case "3": ShowStatistics(); break;
case "4": SearchScore(); break;
case "0":
Console.WriteLine("再见!");
return;
default:
Console.WriteLine("无效选项");
break;
}
}
}
static void ShowMenu()
{
Console.WriteLine("\n=== 成绩管理系统 ===");
Console.WriteLine("1. 添加成绩");
Console.WriteLine("2. 查看成绩");
Console.WriteLine("3. 统计信息");
Console.WriteLine("4. 查找成绩");
Console.WriteLine("0. 退出");
Console.Write("请选择:");
}
static void AddScore()
{
Console.Write("请输入成绩(0-100):");
if (int.TryParse(Console.ReadLine(), out int score) && score >= 0 && score <= 100)
{
scores.Add(score);
Console.WriteLine($"添加成功!当前共 {scores.Count} 个成绩。");
}
else
{
Console.WriteLine("输入无效,请输入 0-100 之间的整数。");
}
}
static void ShowScores()
{
if (scores.Count == 0)
{
Console.WriteLine("暂无成绩数据。");
return;
}
Console.WriteLine($"共 {scores.Count} 个成绩:");
for (int i = 0; i < scores.Count; i++)
Console.WriteLine($" [{i}] {scores[i]}");
}
static void ShowStatistics()
{
if (scores.Count == 0)
{
Console.WriteLine("暂无成绩数据。");
return;
}
int sum = 0, max = scores[0], min = scores[0], passCount = 0;
foreach (int s in scores)
{
sum += s;
if (s > max) max = s;
if (s < min) min = s;
if (s >= 60) passCount++;
}
double avg = (double)sum / scores.Count;
double passRate = (double)passCount / scores.Count * 100;
Console.WriteLine($"总人数:{scores.Count}");
Console.WriteLine($"总 分:{sum}");
Console.WriteLine($"平均分:{avg:F1}");
Console.WriteLine($"最高分:{max}");
Console.WriteLine($"最低分:{min}");
Console.WriteLine($"及格率:{passRate:F1}%");
}
static void SearchScore()
{
Console.Write("请输入要查找的分数:");
if (int.TryParse(Console.ReadLine(), out int target))
{
var indices = new List<int>();
for (int i = 0; i < scores.Count; i++)
if (scores[i] == target)
indices.Add(i);
if (indices.Count > 0)
Console.WriteLine($"找到 {indices.Count} 个匹配,索引位置:{string.Join(", ", indices)}");
else
Console.WriteLine("未找到该成绩。");
}
}
}核心知识点总结
- 函数定义:
访问修饰符 + 返回值类型 + 方法名 + 参数列表 + 方法体 - 参数传递:
- 值参数:传递副本,不影响原变量
ref:传递引用,必须初始化,可读可写out:输出参数,无需初始化,必须赋值in:只读引用,适合大型结构体params:可变数量参数
- 方法重载:同名不同参,编译器根据参数自动匹配
- 递归:函数调用自身,必须有终止条件
- 局部函数:方法内定义方法,C# 7.0+
- 箭头方法:单行方法用
=>简化,C# 6.0+
常见错误
csharp
// 错误1:返回值类型不匹配
static int GetNumber() { return "string"; } // 编译错误
// 错误2:方法名未使用 PascalCase
// ✅ public int CalculateTotal()
// ❌ public int calculateTotal()
// 错误3:out 参数未在方法内赋值
static void Test(out int x) { } // 编译错误:out 参数必须赋值

