用重载替换可选参数是否算是破坏性变更?

34

我知道在库方法中添加可选参数是一种破坏性的更改,

void Foo(int x)             // OLD
void Foo(int x, int y = 5)  // NEW

因为在编译后的代码中,新版本被视为Foo(int, int)。每次调用Foo(0)(源代码)都会被编译器翻译为Foo(0, 5)(已编译的代码)。因此,使用已编译调用Foo(0)的旧客户端将找不到合适的方法。

其他方向呢?

void Foo(int x, int y = 5) { ... }    // OLD

void Foo(int x)        { Foo(x, 5); } // NEW
void Foo(int x, int y) { ... }        // NEW

Foo(0)(源代码)仍然可以编译,Foo(0, 5)(已编译的代码)仍然可以找到合适的重载,因此理论上应该可以工作。

在实践中,它是否有效,即这种情况是否被.NET运行时和C#/VB编译器“官方支持”?或者调用具有可选参数的方法是否会被“标记”,导致当可选参数被重载替换时失败?


编辑:澄清一下,我正在询问二进制兼容性:是否可能在不重新编译projectUsingLibrary.exe的情况下,用library.dll(new)替换library.dll(old)


7
我会期望这没问题;但是:你测试过吗?用两个项目只需要几分钟。 - Marc Gravell
1
@UweKeim:这正是我的问题:客户端是否需要重新编译? - Heinzi
4
即使测试运行良好,我仍然想知道它是否能够正常工作是因为它被规定为这样并且应该能够正常工作还是只是“偶然”地正常工作(因为我使用的特定CLR决定要“慷慨”)。这就是我所说的“官方支持”的意思。 - Heinzi
如果对于所有的 xFoo(x, 5) 等价于 Foo(x) ,则可以接受。 - CodesInChaos
3
如果您没有使用带有版本号的强名称程序集,那么这应该是没问题的,因为运行时将会找到方法Foo(int,int)。所以不需要重新编译您的exe文件。 - Jehof
显示剩余6条评论
2个回答

12

我认为那是一个好问题,下面是我的看法。

使用一个快速的客户端来完成这个操作:

        c1.Foo(1);
        c1.Foo(1, 2);

当使用可选参数时,客户端IL看起来像:

    IL_0000: nop
IL_0001: newobj instance void [ClassLibrary1]ClassLibrary1.Class1::.ctor()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: ldc.i4.1
IL_0009: ldc.i4.5
IL_000a: callvirt instance void [ClassLibrary1]ClassLibrary1.Class1::Foo(int32, int32)
IL_000f: nop
IL_0010: ldloc.0
IL_0011: ldc.i4.1
IL_0012: ldc.i4.2
IL_0013: callvirt instance void [ClassLibrary1]ClassLibrary1.Class1::Foo(int32, int32)
IL_0018: nop
IL_0019: ret

当使用重载时,它看起来像这样:

    IL_0000: nop
IL_0001: newobj instance void [ClassLibrary2]ClassLibrary2.Class2::.ctor()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: ldc.i4.1
IL_0009: callvirt instance void [ClassLibrary2]ClassLibrary2.Class2::Foo(int32)
IL_000e: nop
IL_000f: ldloc.0
IL_0010: ldc.i4.1
IL_0011: ldc.i4.2
IL_0012: callvirt instance void [ClassLibrary2]ClassLibrary2.Class2::Foo(int32, int32)
IL_0017: nop
IL_0018: ret

因此,如果你将实现从可选项更改为重载,但保留客户端与原始状态相同,则实际上为您添加默认参数,并始终调用具有两个参数的函数,这可能是所需的行为,也可能不是。


谢谢,这非常有帮助,因为它显示在IL级别上调用Foo(int, opt int)Foo(int, int)是相同的。关于调用“错误”的函数:这不是问题,我已经在我的问题中澄清了(Foo(int)只是调用Foo(int, 5))。 - Heinzi

4

我不确定我的测试方法是否最佳,但以下是我发现的内容(对于类和命名空间名称表示抱歉):

namespace ClassLibrary1
{
    public class Class1
    {
        private int x;
        private int y;

        public void Foo(int x)
        {
            Foo(x, 0);
        }
        public void Foo(int x, int y = 5)
        {
            this.x = x;
            this.y = y;
        }
    }
}

我建立了这个并将dll添加到不同解决方案中的控制台应用程序,通过浏览到它来引用dll:

using ClassLibrary1;

  namespace ConsoleApplication1  
  {
        class Program
        {
            static void Main(string[] args)
            {
                var c = new Class1();

                c.Foo(1);
                c.Foo(2, 3);
                c.Foo(3, 5);
            }
        }
    }

我随后修改了类库的方法签名为:

namespace ClassLibrary1
{
    public class Class1
    {
        private int x;
        private int y;

        public void Foo(int x)
        {
            Foo(x, 0);
        }
        public void Foo(int x, int y)
        {
            this.x = x;
            this.y = y;
        }
    }
}

我随后编译了类库,将dll文件复制到控制台应用程序文件夹中并运行控制台应用程序;更改签名时没有遇到任何问题,但我不确定我的测试方法是否足够。
因此,回答您的问题,您可以按照指定的方式更改库而无需重新编译可执行文件。

我知道这已经过时了,但我不知道编译器如何编译您应用程序的第一个版本。您有两个版本的Foo方法,它们仅因可选参数而不同,我认为这是不允许的。根据您的示例,基于传递单个参数时决定执行哪个Foo版本,结果会有所不同,这是不应该被允许的。 - Shaggie

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接