在C# 4.0中,你应该使用重载还是可选参数来声明方法?

98
我正在观看Anders关于C# 4.0和C# 5.0的预览的演讲,这让我想到了一个问题:在C#中可选参数可用时,声明不需要指定所有参数的方法的推荐方式是什么?
例如,像FileStream类有大约15个不同的构造函数,可以分成逻辑“家族”,例如下面从字符串、从IntPtr和从SafeFileHandle
FileStream(string,FileMode);
FileStream(string,FileMode,FileAccess);
FileStream(string,FileMode,FileAccess,FileShare);
FileStream(string,FileMode,FileAccess,FileShare,int);
FileStream(string,FileMode,FileAccess,FileShare,int,bool);

我认为这种类型的模式可以通过使用三个构造函数来简化,对于可以默认的构造函数,可以使用可选参数,这将使不同系列的构造函数更加明显 [注:我知道BCL不会做出这种改变,我在假设这种情况]。
你认为呢?从C# 4.0开始,将相关紧密的构造函数和方法作为单个带有可选参数的方法是否更有意义,还是坚持传统的多重载机制有充分的理由?
13个回答

130

我建议你考虑以下几点:

  • 你需要让代码在不支持可选参数的语言中使用吗?如果是,可以考虑包括重载函数。
  • 你的团队中有成员强烈反对可选参数吗?(有时候接受一个你不喜欢的决定可能比较容易,而无需争论。)
  • 你确定你的默认值不会在构建代码的过程中发生改变吗?如果可能会发生改变,你的调用者是否能够接受?

我没有检查默认值的工作方式,但我认为默认值会像对const字段的引用一样被编写到调用代码中。这通常没问题——因为更改默认值通常是相当重要的——但这些是需要考虑的事情。


26
对实用主义的智慧点赞:有时候,接受一个你不喜欢的决定会比争论一番更容易。 - legends2k
16
不,重载效果与第3点不是同一件事情。使用重载提供默认值时,这些默认值在库代码中 - 因此,如果更改了默认值并提供了库的新版本,则调用者将看到新的默认值而无需重新编译。但是,对于可选参数,您需要重新编译才能“看到”新的默认值。很多时候,这不是一个重要的区别,但它确实是一个区别。 - Jon Skeet
1
嗨@JonSkeet,我想知道如果我们同时使用带有可选参数的函数和其他重载方法,哪个方法会被调用?例如Add(int a, int b)和Add(int a, int b, int c = 0),并且函数调用为:Add(5,10);是调用重载函数还是可选参数函数?谢谢 :) - SHEKHAR SHETE
@Shekshar:你试过了吗?阅读规范以获取详细信息,但基本上在决胜局中,编译器没有填充任何可选参数的方法获胜。 - Jon Skeet
1
@JonSkeet 刚刚我尝试了上面的代码...函数重载胜过可选参数 :) - SHEKHAR SHETE

19

当一个方法重载通常执行相同的操作但参数数量不同时,将使用默认值。

当一个方法重载根据其参数以不同的方式执行函数时,则仍然使用重载。

我在VB6时代使用了可选项,自那以后一直很想念它,它可以减少C#中XML注释的重复。


12

我一直使用带可选参数的Delphi, 但现在我改用重载。

因为当你创建更多的重载时,你会不可避免地与一个可选参数形式产生冲突,然后你就必须将它们转换为非可选的。

而且我喜欢这个概念:通常只有一个超级方法,其他方法都是该方法周围的简单包装。


2
我非常同意这一点,但是有一个警告:当您有一个需要多个(3个或以上)参数的方法时,这些参数本质上都是“可选的”(可以用默认值替换),您可能会因此得到许多方法签名的排列组合,而没有更多的好处。考虑Foo(A, B, C)需要Foo(A),Foo(B),Foo(C),Foo(A, B),Foo(A, C),Foo(B, C) - Dan Lugg

7

我一定会使用4.0版本的可选参数功能。它消除了荒谬的...

public void M1( string foo, string bar )
{
   // do that thang
}

public void M1( string foo )
{
  M1( foo, "bar default" ); // I have always hated this line of code specifically
}

...并将值放在调用者可以看到的位置...

public void M1( string foo, string bar = "bar default" )
{
   // do that thang
}

更简单,更少出错。实际上在重载的情况下我曾经看到过这种bug…
public void M1( string foo )
{
   M2( foo, "bar default" );  // oops!  I meant M1!
}

我还没有使用过4.0编译器,但是如果得知编译器只是为您发出重载,我也不会感到惊讶。


6
可选参数本质上是元数据,它指导编译器在处理方法调用时在调用站点插入适当的默认值。相比之下,重载提供了一种编译器可以选择多个方法中的一种的方式,其中一些可能会自己提供默认值。请注意,如果尝试从不支持可选参数的语言编写的代码调用指定可选参数的方法,则编译器将要求指定“可选”参数,但由于调用未指定可选参数的方法等效于使用等于默认值的参数调用它,因此没有障碍阻止这些语言调用这些方法。
绑定可选参数的一个重要后果是,它们将根据编译器可用的目标代码版本被分配值。如果程序集Foo有一个具有默认值5的方法Boo(int),并且程序集Bar包含对Foo.Boo()的调用,则编译器将处理该调用为Foo.Boo(5)。如果将默认值更改为6并重新编译程序集Foo,则除非或直到使用那个新版本的Foo重新编译Bar,否则Bar将继续调用Foo.Boo(5)。因此,应避免将可选参数用于可能会发生变化的事物。

回复:“因此,应避免使用可能会更改的可选参数。”我同意,如果更改未被客户端代码注意到,这可能会有问题。但是,当默认值隐藏在方法重载中时,存在相同的问题:void Foo(int value) … void Foo() { Foo(42); }。从外部来看,调用者不知道使用哪个默认值,也不知道何时可能更改;必须监视编写的文档。可选参数的默认值可以被视为代码中的文档,说明默认值是什么。 - stakx - no longer contributing
@stakx:如果一个无参数的重载链接到一个带参数的重载,改变该参数的“默认”值并重新编译重载的定义将改变它使用的值,即使调用代码没有重新编译。 - supercat
真的,但这并不比另一种选择更具问题性。在一种情况下(方法重载),调用代码对默认值没有发言权。如果调用代码确实完全不关心可选参数及其含义,则可以使用此选项。在另一种情况下(带有默认值的可选参数),先前编译的调用代码不会受到默认值更改的影响。当调用代码实际上关心参数时,这也是适当的;在源中省略它就像说:“当前建议的默认值对我来说是可以的。” - stakx - no longer contributing
我想表达的观点是,虽然两种方法都有后果(就像你指出的那样),但它们并没有固有的优势或劣势。这也取决于调用代码的需求和目标。从这个角度来看,我认为你回答中最后一句话的结论有些过于死板。 - stakx - no longer contributing
@stakx:我说的是“避免使用”,而不是“永远不要使用”。如果更改X将意味着下一次重新编译Y将改变Y的行为,那么这将要求配置构建系统,以便每次重新编译X也会重新编译Y(减慢速度),或者创建程序员将以某种方式更改X的风险,从而在下一次编译Y时破坏Y,并且只有在Y因某些完全不相关的原因而更改时才会发现此类破坏。仅当默认参数的优点超过这些成本时,才应使用它们。 - supercat

5
可以争论是否应该使用可选参数或重载,但最重要的是,它们各自有其不可替代的领域。
当与命名参数结合使用时,可选参数在一些具有所有可选项的长参数列表的COM调用中非常有用。
重载在方法能够操作许多不同的参数类型(只是其中一个例子)并且进行内部转换时非常有用;例如,您只需提供任何有意义的数据类型(由某个现有重载接受的数据类型)。可选参数无法超越这一点。

3

可选参数的一个注意点是版本控制,即重构可能会产生意想不到的后果。 例如:

初始代码

public string HandleError(string message, bool silent=true, bool isCritical=true)
{
  ...
}

假设这是上述方法的众多调用者之一:

HandleError("Disk is full", false);

在这里,事件不是静默的,并被视为关键。

现在假设我们经过重构后发现所有错误都会提示用户,因此我们不再需要静默标志。所以我们将其删除。

重构后

原来的调用仍然可以编译,假设它在重构过程中没有改变:

public string HandleError(string message, /*bool silent=true,*/ bool isCritical=true)
{
  ...
}

...

// Some other distant code file:
HandleError("Disk is full", false);

现在,false会产生一个意外的效果,事件将不再被视为关键。这可能会导致一个微妙的缺陷,因为没有编译或运行时错误(与可选项的其他注意事项不同,比如这个这个)。请注意,这种问题有很多形式。另一种形式在这里概述。此外,请注意,在调用方法时严格使用命名参数将避免这个问题,例如:HandleError("磁盘已满", silent:false)。然而,假设所有其他开发人员(或公共API的用户)都会这样做可能是不切实际的。出于这些原因,除非有其他强制性考虑,否则我会避免在公共API中使用可选参数(甚至在广泛使用的公共方法中也是如此)。

现在false将产生意外的影响,事件将不再被视为关键。--我不确定这是否正确。在重构之前,它也永远不会被视为关键,因为在HandleError("磁盘已满", false);中,false是用于silent - CodingYoshi

3

在许多情况下,可选参数用于切换执行。例如:

decimal GetPrice(string productName, decimal discountPercentage = 0)
{

    decimal basePrice = CalculateBasePrice(productName);

    if (discountPercentage > 0)
        return basePrice * (1 - discountPercentage / 100);
    else
        return basePrice;
}

折扣参数在此处用于if-then-else语句。存在未被识别的多态性,然后被实现为if-then-else语句。在这种情况下,最好将这两个控制流分成两个独立的方法:

decimal GetPrice(string productName)
{
    decimal basePrice = CalculateBasePrice(productName);
    return basePrice;
}

decimal GetPrice(string productName, decimal discountPercentage)
{

    if (discountPercentage <= 0)
        throw new ArgumentException();

    decimal basePrice = GetPrice(productName);

    decimal discountedPrice = basePrice * (1 - discountPercentage / 100);

    return discountedPrice;

}

以这种方式,我们甚至保护了类不会接收到零折扣的调用。这样的调用意味着调用者认为有折扣,但实际上根本没有折扣。这种误解很容易导致错误。
在这种情况下,我更喜欢没有可选参数,而是强制调用者明确选择适合其当前情况的执行方案。
这种情况与具有可能为空的参数相似。当实现变成像 if (x == null) 这样的语句时,同样是一个糟糕的想法。
您可以在以下链接中找到详细分析:避免使用可选参数避免使用空参数

3
我最喜欢的可选参数方面之一是,即使不去查看方法定义,你也可以看到如果没有提供参数会发生什么。当你输入方法名时,Visual Studio将简单地显示参数的默认值。使用重载方法时,你要么阅读文档(如果有的话),要么直接导航到方法的定义(如果有的话)和包装重载的方法。

特别是:随着重载数量的增加,文档工作可能会迅速增加,并且你可能会最终复制现有重载的注释。这非常烦人,因为它不产生任何价值,并且违反了DRY-principle。另一方面,使用可选参数只有一个地方记录所有参数,当你输入时可以看到它们的含义以及它们的默认值

最后,如果你是API的使用者,可能没有查看实现细节的选项(如果你没有源代码),因此无法知道重载方法包装哪个超级方法。因此,你只能阅读文档并希望所有默认值都被列出,但这并不总是情况。当然,这不是一个处理所有方面的答案,但我认为它增加了一个迄今未涉及的方面。

2

我期待可选参数,因为它使默认值更接近方法。所以,不需要为那些只调用“扩展”方法的重载写上几十行代码,你只需定义一次方法,就可以在方法签名中看到可选参数的默认值。我宁愿看到:

public Rectangle (Point start = Point.Zero, int width, int height)
{
    Start = start;
    Width = width;
    Height = height;
}

不要这样做:

public Rectangle (Point start, int width, int height)
{
    Start = start;
    Width = width;
    Height = height;
}

public Rectangle (int width, int height) :
    this (Point.Zero, width, height)
{
}

显然,这个例子非常简单,但在OP的情况下有5个重载,事情很快就会变得拥挤起来。

7
我听说可选参数应该放在最后面,是这样吗? - Ilya Ryzhenkov
取决于你的设计。也许“start”参数通常很重要,除非它不是。也许你在其他地方有相同的签名,意思不同。举个人为的例子,public Rectangle(int width, int height, Point innerSquareStart, Point innerSquareEnd) {} - Robert P
13
根据他们在讲话中的说法,可选参数必须位于必需参数之后。 - Greg Beech

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