修改公共项目方法签名后,依赖项目出现调用异常的技术咨询
问题背景
现有50个已分发至客户的项目P1-P50,均依赖公共项目C0提供的通用功能。在未重新编译分发P项目的前提下,修改了C0中方法的签名,引发运行时异常:
P项目中的代码
public partial class LogView: UserControl { private void buttonReadUnit_Click(object sender, EventArgs e) { readUnit(); // ... } } public partial class LogView : IView { public void readUnit() { UnitBusyMode.Begin(Application.OpenForms[0], true); // ... } }
C0原方法签名
public partial class UnitBusyMode : Form{ public static Thread Begin(Form owner, bool cancellable = false){ // ... } }
修改后的C0方法签名
public partial class UnitBusyMode : Form{ public static Thread Begin(Form owner, bool cancellable = false, int setTemplate= TEMPLATE_NORMAL){ // ... } }
异常现象
重新编译C0分发后,多个P项目调用readUnit();时抛出错误,提示找不到签名为Begin(Form owner, bool cancellable = false)的方法。错误指向readUnit()调用行,而非直接调用Begin的代码行;仅重新编译所有P项目可修复问题。
问题原因
默认参数是编译期替换,而非运行时解析
P项目编译时,编译器会将UnitBusyMode.Begin(Application.OpenForms[0], true)的调用绑定到原签名的Begin(Form, bool)方法,同时把默认参数值直接写入IL代码。当你修改C0的方法签名、新增第三个参数后,原签名的方法已不存在,P项目编译后的IL代码依然在尝试调用这个已消失的方法,自然会触发找不到方法的异常。异常抛出位置的误导
.NET的JIT编译器是按需编译方法的:当执行到buttonReadUnit_Click调用readUnit()时,才会触发readUnit方法的JIT编译,此时才会去查找依赖的Begin方法。找不到目标方法时,异常会关联到触发JIT编译的调用点,也就是readUnit();这一行,而非Begin的直接调用行。方法签名修改破坏二进制兼容性
对已对外发布的公共组件来说,修改原有方法的参数列表(即使是新增可选参数)属于二进制不兼容变更。C#的方法调用基于严格的签名匹配,原有P项目的IL代码记录的是原方法签名,修改C0后原签名方法消失,必然导致运行时绑定失败。
避免方案
保留原方法签名,新增重载方法
不要修改原有Begin方法,而是新增一个重载版本,原方法作为转发层保留:public partial class UnitBusyMode : Form{ public static Thread Begin(Form owner, bool cancellable = false){ // 调用新方法,传递默认参数 return Begin(owner, cancellable, TEMPLATE_NORMAL); } public static Thread Begin(Form owner, bool cancellable = false, int setTemplate= TEMPLATE_NORMAL){ // 新实现逻辑 } }这样原有P项目的调用依然能匹配到原签名方法,同时新代码可以使用重载版本,完全兼容且无需重新编译P项目。
禁止修改已有公共方法的签名
对于已对外发布的公共组件,任何修改原有方法签名的操作(包括新增可选参数、修改参数类型/顺序)都会破坏二进制兼容性,必须避免。所有新需求优先通过新增方法、重载或扩展方法实现。若必须修改,强制重新编译所有依赖项目
如果无法保留原方法签名,唯一的解决方式是重新编译所有依赖C0的P项目,让编译器重新绑定到新的方法签名后再分发。
内容的提问来源于stack exchange,提问作者Tara Rulez

