浏览知识库目录

C#

控制流、方法与参数设计

使用早返回、模式匹配、参数契约和返回类型设计边界明确、易测试的方法。

控制流、方法与参数设计

方法是 C# 项目最重要的行为边界。好的方法不仅复用代码,还明确输入、输出、失败、取消和副作用。控制流应让主路径容易阅读,而不是把业务规则埋进多层嵌套。


一、学习目标

  • 使用早返回、循环和模式匹配表达规则
  • 设计参数、返回值、重载和命名参数
  • 理解值传递以及 inrefout
  • 使用局部函数隔离小型算法
  • 区分纯计算与有副作用操作
  • 把任务操作拆成可测试的方法

二、分支与早返回

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。所有循环都应能回答三个问题:什么时候结束、每次推进什么状态、失败时是否留下部分副作用。

breakcontinue 可以减少嵌套,但不要让多个跳转把状态机切碎。复杂处理流程应提取为方法或显式状态类型。


五、参数与返回值

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 中复用。入口解析,服务执行业务,展示层格式化输出。

默认参数承载环境配置

数据库路径、超时等运行配置不应编译进调用点,应由配置系统提供。


十二、练习与自测

练习:

  1. 编写标题规范化方法,分别测试空白、超长和合法输入。
  2. 用 switch 表达式完成状态文本双向映射。
  3. 实现 Complete(int id),区分未知 ID 和已经完成。
  4. 将一个“读取参数、修改集合、打印结果”的大方法拆成三层。

自测:

  • 引用类型参数“按值传递”复制了什么?
  • 可选参数为何可能产生版本兼容问题?
  • Try 模式与异常分别适合哪些失败?
  • 静态局部函数能避免哪类隐藏依赖?

十三、官方资料

上一篇:集合、泛型与常用数据结构 | 下一篇:项目、程序集、命名空间与依赖