C#
控制流、方法与参数设计
使用早返回、模式匹配、参数契约和返回类型设计边界明确、易测试的方法。
发布于 2026年7月23日
控制流、方法与参数设计
方法是 C# 项目最重要的行为边界。好的方法不仅复用代码,还明确输入、输出、失败、取消和副作用。控制流应让主路径容易阅读,而不是把业务规则埋进多层嵌套。
一、学习目标
- 使用早返回、循环和模式匹配表达规则
- 设计参数、返回值、重载和命名参数
- 理解值传递以及
in、ref、out - 使用局部函数隔离小型算法
- 区分纯计算与有副作用操作
- 把任务操作拆成可测试的方法
二、分支与早返回
static string NormalizeTitle(string? title)
{
if (string.IsNullOrWhiteSpace(title))
{
throw new ArgumentException("任务标题不能为空", nameof(title));
}
string clean = title.Trim();
if (clean.Length > 120)
{
throw new ArgumentException("任务标题不能超过 120 个字符", nameof(title));
}
return clean;
}
先拒绝非法输入,可以让后续代码在更强的不变量下运行。不要把所有条件写成一个巨大布尔表达式;分别报告失败原因更容易测试和诊断。
条件表达式适合短小的值选择:
string marker = task.Status == TaskStatus.Done ? "[x]" : "[ ]";
当分支包含副作用或多步逻辑时,普通 if 更清楚。
三、switch 与模式匹配
static string FormatStatus(TaskStatus status) => status switch
{
TaskStatus.Todo => "todo",
TaskStatus.Done => "done",
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null)
};
即使枚举当前只有两个成员,也保留无法识别值的处理,因为外部强制转换仍可能产生其他数值。
属性模式可以表达领域条件:
static string Describe(StudyTask task) => task switch
{
{ Status: TaskStatus.Done, CompletedAt: not null } =>
$"已完成:{task.Title}",
{ Status: TaskStatus.Todo, Title.Length: > 80 } =>
$"长标题待办:{task.Title}",
_ => $"待办:{task.Title}"
};
模式匹配应帮助读者看到规则。若分支需要访问数据库或发送网络请求,改用明确的用例方法,不要制造带隐藏副作用的表达式。
四、循环选择
仅遍历元素时使用 foreach:
foreach (StudyTask task in tasks)
{
Console.WriteLine(task.Title);
}
需要索引时:
for (int index = 0; index < tasks.Count; index++)
{
Console.WriteLine($"{index + 1}. {tasks[index].Title}");
}
直到条件改变才停止时使用 while。所有循环都应能回答三个问题:什么时候结束、每次推进什么状态、失败时是否留下部分副作用。
break 与 continue 可以减少嵌套,但不要让多个跳转把状态机切碎。复杂处理流程应提取为方法或显式状态类型。
五、参数与返回值
static IReadOnlyList<StudyTask> FilterTasks(
IReadOnlyList<StudyTask> tasks,
TaskStatus? status = null,
int limit = 100)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(limit);
return tasks
.Where(task => status is null || task.Status == status)
.Take(limit)
.ToArray();
}
可选参数的默认值会编译进调用方。公共库修改默认值后,未重新编译的调用方仍可能使用旧值;跨版本 API 可以考虑重载或选项对象。
命名参数提高调用可读性:
var visible = FilterTasks(tasks, status: TaskStatus.Todo, limit: 20);
参数过多通常说明方法承担太多职责,或这些值本来属于一个配置对象。
六、值传递与参数修饰符
C# 参数默认按值传递。引用类型按值传递时,复制的是引用:
static void RenameLocal(List<string> items)
{
items.Add("新任务"); // 修改调用方看到的对象
items = []; // 只替换本地参数变量
}
out 适合 Try 模式:
static bool TryParseStatus(string text, out TaskStatus status)
{
if (text.Equals("todo", StringComparison.OrdinalIgnoreCase))
{
status = TaskStatus.Todo;
return true;
}
if (text.Equals("done", StringComparison.OrdinalIgnoreCase))
{
status = TaskStatus.Done;
return true;
}
status = default;
return false;
}
ref 允许替换调用方变量,耦合较强;普通业务方法优先返回新值。in 以只读引用传递较大的结构体,是否有收益应通过测量决定。
七、重载与 params
重载应表达同一个概念的不同输入形状:
static StudyTask CreateTask(string title) =>
CreateTask(title, DateTimeOffset.UtcNow);
static StudyTask CreateTask(string title, DateTimeOffset createdAt) =>
new(0, NormalizeTitle(title), TaskStatus.Todo, createdAt, null);
不要让两个重载产生不同业务语义。调用点如果容易歧义,改用不同方法名。
params 适合少量便利参数:
static bool HasAnyTag(StudyTask task, params ReadOnlySpan<string> tags)
{
foreach (string tag in tags)
{
if (task.Tags.Contains(tag, StringComparer.Ordinal))
{
return true;
}
}
return false;
}
高频路径要关注调用是否产生临时数组;C# 允许面向 span 的 params,但公开 API 仍以清晰性优先。
八、局部函数
局部函数适合只服务于一个方法的小算法:
static string RenderTable(IReadOnlyList<StudyTask> tasks)
{
static string Escape(string value) =>
value.Replace("|", "\\|", StringComparison.Ordinal);
var lines = new List<string> { "| ID | 标题 |", "|---:|---|" };
foreach (StudyTask task in tasks)
{
lines.Add($"| {task.Id} | {Escape(task.Title)} |");
}
return string.Join(Environment.NewLine, lines);
}
标记为 static 的局部函数不能意外捕获外层变量。若算法需要独立测试或被多个方法复用,应提升为类型成员或独立服务。
九、失败返回还是异常
选择依据是调用契约:
- 普通、预期且高频的解析失败:
TryParse+bool - 找不到可选对象:返回
StudyTask? - 违反领域前置条件:抛出明确异常
- 一次操作可能产生多种业务结果:定义结果类型
public readonly record struct CompleteResult(
bool Success,
string? Error);
不要用 null 同时表达“找不到”“权限不足”“数据库失败”。失败类型必须让调用方知道下一步能做什么。
十、任务服务
public sealed class TaskService
{
private readonly ITaskRepository _repository;
private readonly TimeProvider _timeProvider;
public TaskService(
ITaskRepository repository,
TimeProvider timeProvider)
{
_repository = repository;
_timeProvider = timeProvider;
}
public StudyTask Add(string title)
{
var task = StudyTask.Create(
NormalizeTitle(title),
_timeProvider.GetUtcNow());
return _repository.Add(task);
}
}
服务描述用例,仓储负责持久化,时间通过依赖注入便于测试。服务不读取控制台,也不拼接 SQL。
十一、常见错误
用 ref 模拟多个返回值
优先返回记录、元组或结果类型,让数据流清楚且便于组合。
捕获异常后返回 false
这会把基础设施故障伪装成普通业务失败。只转换自己真正理解的异常。
一个方法同时解析、写库和打印
它无法独立测试,也无法在 GUI 或 API 中复用。入口解析,服务执行业务,展示层格式化输出。
默认参数承载环境配置
数据库路径、超时等运行配置不应编译进调用点,应由配置系统提供。
十二、练习与自测
练习:
- 编写标题规范化方法,分别测试空白、超长和合法输入。
- 用 switch 表达式完成状态文本双向映射。
- 实现
Complete(int id),区分未知 ID 和已经完成。 - 将一个“读取参数、修改集合、打印结果”的大方法拆成三层。
自测:
- 引用类型参数“按值传递”复制了什么?
- 可选参数为何可能产生版本兼容问题?
- Try 模式与异常分别适合哪些失败?
- 静态局部函数能避免哪类隐藏依赖?
十三、官方资料
上一篇:集合、泛型与常用数据结构 | 下一篇:项目、程序集、命名空间与依赖