首页 / C# 入门教程 / params 可变参数、可选参数与命名参数

C# 入门教程

params 可变参数、可选参数与命名参数

本教程共 100 篇 · 第 51 篇 · 更新于 2026-07-31 · 约 8 分钟阅读

C#C# 入门教程编程语言参数params方法

51. params 可变参数、可选参数与命名参数

本节目标:学完你能用 params 接收任意数量的参数、用默认值定义可选参数、用命名参数让调用更清晰,并知道三者各自适合什么场景。

调用方法时,参数个数往往写死在签名里。可现实里,求和可能加两个数,也可能加十个。难道为每个数量都写一个重载?C# 提供了三件省力工具:params 可变参数、带默认值的可选参数、以及命名参数调用。

把方法签名想象成「点餐菜单」:必点的主菜是固定参数;大多数人都接受的默认配料是可选参数;命名参数则像「芝士双份、面包不要」——把每一项说清楚,避免上错菜。三件工具解决的是不同问题,下面分开讲。

一、params 接收不定数量参数

最常见的场景是「参数个数不确定」。关键字 params 让一个形参代表一组同类型实参。

namespace CSharpDemo;

int Sum(params int[] numbers)
{
    int total = 0;
    foreach (var n in numbers) total += n;
    return total;
}

Console.WriteLine(Sum(1, 2));
Console.WriteLine(Sum(1, 2, 3, 4, 5));
Console.WriteLine(Sum()); // 传 0 个也合法,结果是 0

调用方看起来像传了多个独立参数,编译器其实把它们打包成数组。你也可以直接传一个数组:Sum(new[] { 1, 2, 3 }),效果一样。

Note

params 形参必须是参数列表里最后一个,而且一个方法里只能有一个 params。它后面不能再有其他普通参数。

为什么这样设计

没有 params 时,你要么写一堆重载(Sum2、Sum3…),要么让调用方自己先 new 一个数组。前者繁琐,后者把实现细节推给调用方。params 把打包动作交给编译器,调用方只管一个个传值,代码更轻。

适用场景与对比

params 适合「调用方确实不知道要传几个」的场合:求任意个数的最大值、把多个日志片段拼成一条、给一组学生批量加分。反过来,如果数量基本固定(永远就两三个),硬用 params 反而让调用方失去「少传会编译报错」的保护,这时老老实实写固定参数更好。

和「要求传一个 List<T>」比,params 胜在调用时不用先 new 列表,写起来轻。代价是每次调用都会生成一个数组(除非用 C# 13 起的 ReadOnlySpan<T> 写法),极高频调用时要留意这点分配。

C# 13 起的 params 集合

老版本里 params 只能修饰数组。从 C# 13 起,params 还能修饰集合表达式支持的类型,比如 ReadOnlySpan<T>,避免数组分配:

namespace CSharpDemo;

int Total(params ReadOnlySpan<int> values)
{
    int sum = 0;
    foreach (var v in values) sum += v;
    return sum;
}

Console.WriteLine(Total(10, 20, 30));
Tip

只是简单求和,用 int[] 就够了。当你非常在乎性能、想避免数组分配时,再考虑 ReadOnlySpan<T> 这种写法。

Warning

params 和重载(overload)同时存在时,重载解析可能选到你意想不到的那个。比如既有 void F(int x) 又有 void F(params int[] xs),调用 F(1) 会命中第一个,而不是打包成数组。这种「二义性」要提前想清楚,必要时改名避免混淆。

二、可选参数与默认值

有些参数大多数时候取同一个值,比如问候语默认是「你好」。给形参一个默认值,调用方不传就用默认:

namespace CSharpDemo;

void Greet(string name, string greeting = "你好", bool loud = false)
{
    var msg = $"{greeting}{name}!";
    if (loud) msg = msg.ToUpper();
    Console.WriteLine(msg);
}

Greet("小明");                 // 你好,小明!
Greet("小红", "早上好");        // 早上好,小红!
Greet("小刚", loud: true);     // 你好,小刚!(大写)

默认值写在形参后面,等号右边必须是个编译期常量,比如数字、string 字面量、true/false,不能是变量或方法调用结果。

Note

可选参数必须排在必填参数之后;必填参数不能放在可选参数后面,否则编译报错。也就是说,params 在最后,可选参数在它前面。

为什么这样设计 / 何时用

默认值在编译期确定,调用方不传就用默认。这避免了「为了一个常用配置,到处写同一个字面量」的重复。比如读取文件的方法,encoding 默认 UTF-8,大多数调用方根本不用管它。

Tip

可选参数很适合「配置项多、但大多有合理默认」的方法。但如果默认值依赖运行时状态(比如「当前时间」「当前用户」),就别用可选参数——它编译期就定死了。这种情况应该用普通参数,在方法内部取当前值。

三、命名参数让调用更清楚

参数一多,光看调用处很难想起每个位置代表什么。命名参数允许你写出「参数名: 值」,顺序还能随便换:

namespace CSharpDemo;

void Greet(string name, string greeting = "你好", bool loud = false)
{
    var msg = $"{greeting}{name}!";
    if (loud) msg = msg.ToUpper();
    Console.WriteLine(msg);
}

void Send(string to, string subject, bool isHtml = false) =>
    Console.WriteLine($"发给 {to}{subject}(HTML={isHtml})");

Send(to: "a@x.com", subject: "你好");
Send(subject: "会议", to: "b@x.com", isHtml: true);

两个调用结果一致,但读起来一目了然,不用去翻方法签名。

与位置参数混用

命名参数可以跟在位置参数后面,但反过来不行:

namespace CSharpDemo;

void Send(string to, string subject, bool isHtml = false) =>
    Console.WriteLine($"发给 {to}{subject}(HTML={isHtml})");

Send("a@x.com", subject: "你好");          // 合法
// Send(subject: "你好", "a@x.com");        // 编译错误:位置参数不能排在命名参数之后

命名参数适合什么

命名参数最大的价值是「自我说明」。Send(email, subject, body, isHtml: true) 一眼看出最后那个 bool 是「是否 HTML 邮件」,而 Send(email, subject, body, true) 得翻签名才懂。当方法有多个同类型参数(尤其多个 bool),强烈建议命名。

命名参数还能跳过「中间有默认值的参数」直接指定后面的,让调用更聚焦,不必为了改最后一个参数而把前面的都填一遍。

Tip

当方法有多个 bool 或同类型参数时,强烈建议用命名参数,避免 Greet(true, false, true) 这种天书式调用。

四、三者一起用要注意什么

params、可选参数、命名参数可以共存,但重载解析有讲究。看这个组合:

namespace CSharpDemo;

void Log(string message, string level = "INFO") =>
    Console.WriteLine($"[{level}] {message}");

Log("启动完成");
Log("出错了", level: "ERROR");

这里用命名参数跳过位置、明确指定 level,清晰又省事。

Warning

可选参数的默认值在编译期就绑死了。如果方法定义在另一个程序集(类库)里,你后来改了默认值,调用方必须重新编译才能拿到新默认值,否则还在用旧值。跨程序集的 API 改默认值是个隐蔽的破坏性变更,要谨慎对待。

五、新手最容易踩的坑

第一个坑:想给 params 之前的可选参数传值,却忘了用命名参数。比如 Greet("小红", "你好", true) 位置是对的,但一旦顺序记错,编译未必报错,运行结果却不对。用命名参数最稳妥。

第二个坑:默认值用变量。下面这种写法通不过编译:

int step = 2;
// void Walk(int n = step) { }   // 错误:默认值必须是常量

第三个坑:用命名参数「跳着传」想省略中间参数,但中间那个并没有默认值。C# 不允许跳过没有默认值的必填参数。

第四个坑:以为命名参数能扛住「参数改名」。命名参数用的是参数名,如果你把 name 改成 userName,所有写 name: 的调用处会编译报错——好在编译器会抓出来,改起来不隐蔽,比运行时才暴露强。

六、怎么选才合适

params 解决「数量不定」,可选参数解决「多数情况取同一值」,命名参数解决「调用可读性」。三者不是互斥的:数量不定就用 params;数量固定但多数有默认值,就用可选参数;参数一多、类型容易混淆,就用命名参数。

记住一个原则:方法的签名越直观,调用方越不容易出错。语言给了你这些工具,目的就是让接口既灵活又易懂。

七、最佳实践清单

  • 数量真正不定才用 params;固定两三个就老老实实写参数。
  • 可选参数放在必填参数之后;需要「运行时默认值」就改用普通参数。
  • 多个 bool / 同类型参数,调用时一律用命名参数。
  • 公共 API 的参数一旦发布,改默认值要当作破坏性变更对待。
  • 三者混用时,命名参数放最后、顺序清晰,别玩花活。

八、小结

三件工具各管一摊:params 管「传几个都行」,可选参数管「不传就用默认」,命名参数管「调用处自解释」。组合起来能写出既灵活又清晰的接口,但灵活过了头反而难读——克制使用、以可读为先。