params 可变参数、可选参数与命名参数
本教程共 100 篇 · 第 51 篇 · 更新于 2026-07-31 · 约 8 分钟阅读
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 管「传几个都行」,可选参数管「不传就用默认」,命名参数管「调用处自解释」。组合起来能写出既灵活又清晰的接口,但灵活过了头反而难读——克制使用、以可读为先。