可选参数在方法声明中带有默认值,所以调用方可以省略它。命名参数按参数名而不是位置来传值。两者结合,一个方法就能支持多种调用方式,而不必写一大堆重载。
输出:
to ana@mail.com: (no subject), retries 3
to ben@mail.com: Invoice #1042, retries 3
to cy@mail.com: (no subject) [URGENT], retries 3
to dev@mail.com: Build failed, retries 0
第三次调用跳过了 subject,并按名称设置了 urgent;如果没有命名参数,就得再传一次 "(no subject)",才能轮到第三个位置。最后一次调用按名称传递了所有参数,顺序也和声明不同,这是合法的。
默认值的规则
默认值必须是编译器能够算出来的东西:
- 常量(
3、"INFO"、true、1.5m、const字段、枚举成员) - 对引用类型或可空类型使用
null default(T),或者对值类型T使用new T()
任何在运行时才求值的东西都会被拒绝。典型的例子是日期:
static void Schedule(string task, DateTime at = DateTime.Now) { }
// error CS1736: Default parameter value for 'at' must be a compile-time constant
标准的解决办法是使用一个默认值为 null 的可空参数,在方法体中再确定实际的值:
输出:
backup at 2026-01-01 09:00, tags: 0
report at 2026-03-15 18:30, tags: 0
deploy at 2026-01-01 09:00, tags: 2
同样的技巧也适用于集合:默认值 new List<string>() 不是常量,所以把默认值设为 null,再在方法内部创建列表。这也避免了 Python 中那种可变默认值被共享的 bug,C# 只允许常量作为默认值,从根本上排除了这个问题。
顺序规则
必需参数在前,可选参数在后,params 数组(如果有的话)放在最后:
static void Log(string message, string level = "INFO", params string[] tags) { } // OK
static void Log(string level = "INFO", string message) { }
// error CS1737: Optional parameters must appear after all required parameters
ref 和 out 参数不能是可选的。
在调用方,按位置传入的参数从头开始依次填充。命名参数可以跟在它们后面,顺序任意。从 C# 7.2 起,命名参数也可以出现在按位置传入的参数之前,但前提是它就在自己本来的位置上(SendEmail("a@b.c", subject: "Hi", true));在更早的版本中,命名参数必须全部放在最后。漏掉一个必需参数是编译错误,即使你用名称传了其他参数也一样。
用命名参数提高可读性
即使没有任何可选参数,命名参数也很有用。在调用处,字面量 true、false、null 和光秃秃的数字什么信息也没有表达:
ResizeImage(photo, 800, 600, true, false); // which is which?
ResizeImage(photo, width: 800, height: 600, keepAspect: true, upscale: false);
对于使用了参数名的调用方来说,重命名参数就成了破坏性变更,在公共库中要记住这一点。
可选参数与重载的对比
在 C# 4 之前,要实现同样的灵活性,每种组合都需要一个重载。可选参数把它们合并成了一个方法:
// overloads
static void Connect(string host) => Connect(host, 443);
static void Connect(string host, int port) => Connect(host, port, 30);
static void Connect(string host, int port, int timeoutSeconds) { /* ... */ }
// one method with optional parameters
static void Connect(string host, int port = 443, int timeoutSeconds = 30) { /* ... */ }
当两者同时存在时,重载决策会优先选择不需要填充任何默认值的候选方法:
输出:
Greet()
Greet(string) with Lena
Greet() 同时匹配两个方法,编译器选择了没有省略任何可选参数的那个。在同一个名称上混用这两种技术,往往会让人很难预测一次调用到底会调用哪个方法,所以每个方法只选一种。
如果几种调用方式需要不同的代码或不同的参数类型,选择重载;如果它们只在默认值上不同,选择可选参数。
默认值被编译进调用方
默认值不是在运行时查找的。编译器在编译调用代码时,会把它复制到每一个调用处。Connect("api.shop.com") 会被编译成 Connect("api.shop.com", 443, 30)。
这对库来说会产生一个后果。假设某个包的第 1 版发布了 Connect(string host, int timeoutSeconds = 30),第 2 版把默认值改成了 10。一个针对第 1 版编译的应用程序,在换上第 2 版的 DLL 之后,仍然会继续传入 30,直到这个应用程序本身被重新编译。给现有的公共方法新增一个可选参数,同样会破坏已经编译好的调用方,因为方法的签名变了,而它们仍在寻找旧的签名(运行时抛出 MissingMethodException)。
在作为一个整体编译的单个应用程序中,这完全不是问题。对于 NuGet 包中的公共 API,使用重载(让默认值留在库内部)或在方法体中处理的 null 默认值,就能避免这个问题。
默认值与声明的类型
同样是因为这条编译时规则,当接口和类都声明了默认值时,使用哪个默认值取决于你调用时所用变量的类型,而不是对象本身:
输出:
printing "report" x5
printing "report" x1
同一个对象,两个不同的默认值。让接口和它的实现保持相同的默认值,或者只在一个地方声明默认值。
调用方信息特性
可选参数还为 System.Runtime.CompilerServices 中的调用方信息特性提供了支持。编译器会用调用处的详细信息来填充它们:
输出:
[Main:18] starting
[SaveOrder:13] order saved
日志库就是这样在调用方不必手动输入的情况下记录消息来源的,INotifyPropertyChanged 的实现也是这样获得属性名的。[CallerFilePath] 以同样的方式加入源文件路径。
常见问题
C# 中如何让参数变成可选的?
在声明中给它一个默认值:static void Log(string message, string level = "INFO")。这样调用方就可以写 Log("started") 或 Log("failed", "ERROR")。可选参数必须放在所有必需参数之后,而且默认值必须是编译时常量。
C# 中的命名参数是什么?
带着参数名传递的实参:SendEmail(to: "ana@mail.com", urgent: true)。它们让你可以跳过中间的可选参数、以任意顺序传参,还能让带有字面量 true/false 或数字的调用一眼就看得懂。
为什么不能把 DateTime.Now 用作参数的默认值?
默认值必须是编译时常量,而 DateTime.Now 是在运行时计算的,所以编译器会报告 CS1736:Default parameter value for 'at' must be a compile-time constant。改用可空参数:DateTime? at = null,然后在方法体中写 DateTime time = at ?? DateTime.Now;。
C# 中应该用可选参数还是方法重载?
如果几种调用方式只在默认值上不同,可选参数更简单。如果它们需要不同的逻辑或类型,重载更合适;在公共库中也是重载更好,因为默认值会被编译进每一个调用方,之后再修改它,已经编译好的代码是感知不到的。
“Optional parameters must appear after all required parameters”是什么意思?
错误 CS1737:一个带默认值的参数后面跟着一个没有默认值的参数。把必需参数移到前面:写 (string to, bool urgent = false),而不是 (bool urgent = false, string to)。可选参数后面只能再跟可选参数或一个 params 数组。