本节摘要:重载是编译期概念,决议发生在编译那一刻。本节讲清重载决议的两阶段规则(候选筛选与更优匹配)、可选参数与重载的冲突、扩展方法的编译原理,以及参数传递的值/引用语义——这些规则决定你预判编译结果的能力。
同一方法名多个签名称为重载。决议分两步:先按方法名与参数个数筛出"适用候选",再在候选里找"更优"。"更优"的比较规则核心是类型转换距离:实参类型到形参类型的转换越短越好,int 到 int 优于 int 到 long 优于 int 到 double,目标类型匹配优于装箱优于 object。
用三行代码走一遍:
static void F(object o) { Console.WriteLine("object"); } static void F(int i) { Console.WriteLine("int"); } static void F(double d) { Console.WriteLine("double"); } F(3); // int:完全匹配最短 F(3L); // long 无匹配,扩宽到 double F("s"); // string 到 object:引用转换 F(3, 4); // CS1501:无两个参数的重载,编译错误
第四行值得注意:重载决议失败是编译期错误,这正是"重载是静态多态"的含义——调用哪个版本在你按下编译那一刻就定了,运行时不会再变。与之对照,2.4 节的虚方法分派是运行时多态,依据对象的实际类型动态选择。两者名字里都有"多态",时间维度完全不同,面试与真实排错中都值得分清。
边界案例最能检验理解。猜结果:
static void G(object o) { Console.WriteLine("object"); } static void G(string s) { Console.WriteLine("string"); } G(null); // string:null 到 string 是比到 object 更具体的引用转换
null 字面量没有类型,但"到 string"的转换比"到 object"更具体(转换关系上更靠下),编译器选 string 版本。这类规则不必背诵,但要知道:拿不准时,窄的赢、近的赢、不装箱的赢。
static void Log(string msg, string level = "INFO") { } Log("启动"); // 用默认值 Log("磁盘满", level: "ERROR"); // 命名参数
可选参数与命名参数是一对搭档。但可选参数有个跨程序集的坑,与 1.4 节的 const 帽子同款:默认值在调用方编译时嵌入。库作者把 "INFO" 改成 "info" 重新发库,引用方不重编译,行为不变。所以公共 API 里改默认值不是兼容变更,要么新增重载,要么升主版本。
可选参数与重载还会互相干扰:F(int a) 与 F(int a, int b = 0) 同时存在时,F(1) 两个候选都适用,规则是无默认值的更优——能编过,但可读性立刻下降。团队规范一般约定:二选一,不混用。
默认情况下 C# 传参是值传递——结合 1.3 节:值类型拷数据,引用类型拷地址。所以"引用类型传参就是引用传递"是错的:方法里 customer = new Customer() 换的是副本指向,调用方的变量不受影响;只有通过副本去修改对象成员,效果才外显。
要真正传引用得加修饰符:
static void Bump(ref int n) { n++; } static bool TryParseInt(string s, out int v) { return int.TryParse(s, out v); } int x = 1; Bump(ref x); // x 变 2 TryParseInt("42", out int y); // y = 42,out 参数必须被方法赋值
ref 双向可写、调用前必须有值;out 方法内必须赋值、调用前无需初始化——Try 模式全靠它返回"成功与否加结果"。编译器为 out 做的额外检查(所有路径必赋值)是那种"编译器替你兜底"的好设计。in 修饰符则是"只读引用传参",为的是大结构体免拷贝,属于性能场景的细粒度武器。
public static class StringExt { public static bool IsEmail(this string s) => s is not null && s.Contains('@') && s.Contains('.'); } // 调用 bool ok = "a@b.com".IsEmail();
扩展方法没有侵入被扩展类型。编译器看到 s.IsEmail() 找不到实例方法,就按 using 引入的静态类搜 IsEmail(this string),然后改写回静态调用 StringExt.IsEmail(s)——产物与你自己写静态调用完全一致。三个推论:扩展方法的调用优先级低于任何实例方法(同名时实例版永远赢,这可能造成"你写的扩展悄悄没被调用");null 调扩展方法不抛 NullReferenceException(它就是个静态调用,参数为 null 而已);只能扩展而不能访问私有成员。
LINQ 的流畅 API 全是扩展方法:where、select、ToList 挂在 Enumerable 与 Queryable 静态类上,注入到 IEnumerable 与 IQueryable。理解了注入机制,第三章 LINQ 的链式调用就不再神秘——每一步都是一次静态方法调用。
普通递归每层消耗一个栈帧,深递归会栈溢出。C# 编译器不做自动尾调用优化(CLR 层有 tail 前缀但 JIT 支持有限),深递归的正确替代是显式循环加栈数据结构。这不是缺陷而是取舍:自动尾调用会干扰栈回溯与安全检查。需要"递归的优雅"时,考虑把递归翻译成迭代加显式栈,是性能敏感场景的常规手工优化。
下一节进入运行时多态:虚方法表如何在对象头上撑起动态分派。