是否应该在C#4.0中使用重载或可选参数声明方法?


93

我正在看Anders谈论C#4.0和C#5.0的预览,这让我开始思考,何时可选参数在C#中可用,这是声明不需要指定所有参数的方法的推荐方式?

例如,类似FileStream该类的东西有大约十五个不同的构造函数,这些构造函数可以划分为逻辑“族”,例如,字符串中的下面的那些,an中的一个IntPtr和a中的一个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开始,将密切相关的构造函数和方法组作为具有可选参数的单个方法会更有意义,还是有充分的理由坚持传统的多次重载机制?

Answers:


120

我会考虑以下几点:

  • 您是否需要从不支持可选参数的语言中使用代码?如果是这样,请考虑包括重载。
  • 您的团队中有没有成员强烈反对可选参数?(有时候,做出您不喜欢的决定要比对案件辩护容易。)
  • 您是否确信在代码构建之间不会更改默认设置,或者如果可能,您的调用方会同意吗?

我没有检查默认值的工作方式,但我假设默认值将被烘焙到调用代码中,与对const字段的引用几乎相同。通常没关系-更改默认值非常重要-但这是要考虑的事情。


20
+1实用主义智慧:有时候,做出自己不喜欢的决定要比辩护案件容易。
legends2k

13
@romkyns:不,重载的影响与第3点不同。通过提供默认值的重载,默认值在库代码中 -因此,如果您更改默认值并提供新版本的库,则调用者将请参阅新的默认值而无需重新编译。而使用可选参数,则需要重新编译以“查看”新的默认值。很多时候,这不是重要的区别,但这一个区别。
乔恩·斯基特

嗨@JonSkeet,我想知道我们是否同时使用带有可选参数的ie函数和其他带有重载的函数,将调用哪种方法?例如Add(int a,int b)和Add(int a,int b,int c = 0)和函数调用说:Add(5,10); 哪种方法将被称为重载函数或可选参数函数?谢谢:)
SHEKHAR SHETE

@Shekshar:你尝试过吗?阅读规范以了解详细信息,但基本上是抢七决胜的方法,其中的编译器无需填写任何可选参数wins。
乔恩·斯基特

@JonSkeet刚才我在上面尝试过...函数重载胜过可选参数:)
SHEKHAR SHETE

19

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

当方法重载根据其参数以不同的方式执行功能时,将继续使用重载。

在VB6的日子里,我使用了可选的方法,但是由于错过了它,它将减少C#中许多XML注释的重复。


11

我一直在使用带有可选参数的Delphi。我改用了过载。

因为当您创建更多重载时,您总是会与可选参数形式发生冲突,然后无论如何都必须将它们转换为非可选参数。

我喜欢这样一种观念,即通常存在一种超级方法,而其余的则是围绕该方法的更简单的包装。


1
我非常同意这一点,但是需要注意的是,当您的方法具有多个(3+)个参数时,这些参数本质上都是“可选”的(可以用默认值替换),最终可能会有许多排列方法签名无效。考虑Foo(A, B, C)需要Foo(A)Foo(B)Foo(C)Foo(A, B)Foo(A, C)Foo(B, C)
丹·拉格

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
}

更简单,更少出错。我实际上已经将其视为过载情况下的错误...

public void M1( string foo )
{
   M2( foo, "bar default" );  // oops!  I meant M1!
}

我还没有玩过4.0编译器,但是得知编译器只是为您发出了重载,我不会感到震惊。


6

可选参数本质上是一段元数据,它指示正在处理方法调用的编译器在调用位置插入适当的默认值。相比之下,重载提供了一种方法,编译器可以通过该方法选择许多方法之一,其中一些方法本身可能会提供默认值。请注意,如果尝试从不支持该语言的语言编写的代码中调用指定可选参数的方法,则编译器将要求指定“可选”参数,但是由于调用未指定可选参数的方法是等效于使用等于默认值的参数调用它,此类语言调用此类方法没有障碍。

在调用站点绑定可选参数的一个重要结果是,将根据编译器可用的目标代码的版本为它们分配值。如果程序集Foo具有Boo(int)默认值为5的方法,而程序集Bar包含对的调用Foo.Boo(),则编译器会将其作为进行处理Foo.Boo(5)。如果默认值更改为6并Foo重新编译了程序集,Bar则将继续调用,Foo.Boo(5)除非或直到使用新版本的进行重新编译为止Foo。因此,应该避免对可能发生变化的事物使用可选参数。


回复:“因此,应该避免对可能发生变化的事物使用可选参数。” 我同意,如果客户端代码未注意到该更改,则可能会出现问题。但是,当默认值隐藏在方法重载内部时,会出现相同的问题:void Foo(int value) … void Foo() { Foo(42); }。从外部,调用者不知道使用了什么默认值,也不知道何时会更改。为此,必须监视书面文档。可选参数的默认值可以看作是:代码文档中的默认值。
stakx-不再贡献

@stakx:如果无参数重载链接到带有参数的重载,则更改该参数的“默认”值并重新编译该重载的定义将更改其使用的值,即使未重新编译调用代码也是如此
超级猫

的确如此,但这并不比替代方案带来更多的问题。在一种情况下(方法重载),调用代码在默认值中没有发言权。如果调用代码根本不关心可选参数及其含义,则这可能是适当的。在其他情况下(具有默认值的可选参数),当默认值更改时,先前编译的调用代码不会受到影响。当调用代码实际上关心参数时,这也可能是适当的;在源代码中省略它就像说,“当前建议的默认值对我来说是可以的”。
stakx-不再贡献

我在这里要说明的一点是,尽管两种方法都有其后果(如您所指出的那样),但它们并不是天生的利弊。这也取决于调用代码的需求和目标。从该观点来看,我发现您回答的最后一句话中的判决有些过于刻板。
stakx-不再贡献

@stakx:我说的是“避免使用”而不是“从不使用”。如果更改X意味着下次Y的重新编译将改变Y的行为,则要么需要配置构建系统,以便每次X的重新编译也重新编译Y(使速度变慢),否则就有程序员改变的风险。 X的方式将在下一次编译时破坏Y,并且仅在以后由于完全不相关的原因而更改Y时才发现这种破坏。仅当默认参数的优点胜过此类成本时,才应使用默认参数。
超级猫

4

可以争论是否应该使用可选参数或重载,但是最重要的是,每个参数都有其自己的区域,这些区域是不可替代的。

当与命名参数结合使用时,可选参数与COM调用的一些带有所有可选参数的长参数列表结合使用时非常有用。

当方法能够对许多不同的参数类型进行操作(仅是示例之一)并且在内部进行强制转换时,重载非常有用。您只需提供有意义的任何数据类型(某些现有的重载即可接受)。不能用可选参数击败它。


3

我期待可选参数,因为它使默认值更接近该方法。因此,您只需定义一次方法即可,而不必为调用“ expanded”方法的重载使用数十行代码,而只需在方法签名中看到可选参数的默认值即可。我宁愿看:

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
我听说可选参数应该是最后一个,不是吗?
伊利亚·里真科夫

取决于您的设计。也许“开始”参数通常很重要,除非并非如此。也许您在其他地方具有相同的签名,这意味着有所不同。举一个人为的例子,公共Rectangle(int width,int height,Point innerSquareStart,Point innerSquareEnd){}
Robert P

13
根据他们在谈话中所说的,可选参数必须位于必需参数之后。
格雷格·比奇

3

我最喜欢的可选参数方面之一是,即使不进行方法定义,也可以查看如果不提供参数会发生什么情况。当您键入方法名称时,Visual Studio只会向您显示该参数的默认值。对于重载方法,您将不得不阅读文档(如果有的话)或直接导航到方法的定义(如果有的话)以及重载包装的方法。

特别是:随着超载量的增加,文档工作量可能会迅速增加,您可能最终会从现有的超载中复制已经存在的注释。这很烦人,因为它不产生任何值并且破坏了DRY原理。另一方面,使用可选参数,恰好在一个地方记录了所有参数,并且在键入时会看到它们的含义以及默认值

最后但并非最不重要的一点是,如果您是API的使用者,您甚至可能没有选择检查实现细节(如果您没有源代码),因此没有机会查看重载的方法是哪种超级方法。包装。因此,您不得不阅读文档并希望在那里列出所有默认值,但情况并非总是如此。

当然,这不是一个涵盖所有方面的答案,但我认为它增加了一个到目前为止尚未涉及的答案。


1

尽管它们(据说?)有两种在概念上等效的方法可供您从头开始建模API,但不幸的是,当您需要考虑旧客户端的运行时向后兼容性时,它们有一些细微的差别。我的同事(感谢Brent!)向我指出了这一精彩的文章:带有可选参数的版本控制问题。引述一下:

首先将可选参数引入C#4的原因是为了支持COM互操作。而已。现在,我们正在了解这一事实的全部含义。如果您的方法带有可选参数,则由于担心会导致编译时中断更改,因此永远无法添加带有其他可选参数的重载。而且,您永远无法消除现有的重载,因为这一直是运行时的重大更改。您几乎需要将其视为接口。在这种情况下,您唯一的办法是使用新名称编写新方法。因此,如果您打算在API中使用可选参数,请注意这一点。


1

可选参数的一个警告是版本控制,其中重构会产生意想不到的后果。一个例子:

初始码

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将产生意想不到的效果,该事件将不再被视为紧急事件。

这可能会导致细微的缺陷,因为将不会出现编译或运行时错误(与其他可选警告(例如thisthis)不同))不同)。

请注意,同一问题有多种形式。这里概述另一种形式

另请注意,在调用方法时严格使用命名参数可以避免此问题,例如:HandleError("Disk is full", silent:false)。但是,假设所有其他开发人员(或公共API的用户)都将这样做是不切实际的。

由于这些原因,除非有其他令人信服的考虑因素,否则我将避免在公共API中使用可选参数(如果可能被广泛使用,甚至不要使用公共方法)。


0

这两个Optional参数,Method重载都有其优点或缺点。这取决于您的选择偏好。

可选参数:仅在.Net 4.0中可用。可选参数可减少代码大小。您无法定义out和ref参数

重载的方法:您可以定义Out和ref参数。代码大小会增加,但是重载的方法很容易理解。


0

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

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

    decimal basePrice = CalculateBasePrice(productName);

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

这里的Discount参数用于提供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;

}

这样,我们甚至保护了该类,使其免于接听零折扣的电话。该呼叫意味着呼叫者认为有折扣,但实际上根本没有折扣。这种误解很容易导致错误。

在这种情况下,我不希望没有可选参数,而是要强制调用者显式选择适合其当前情况的执行方案。

这种情况与具有可以为null的参数非常相似。当实现变成类似的语句时,这同样是一个坏主意if (x == null)

您可以在以下链接上找到详细的分析:避免使用可选参数避免使用空参数


0

要在使用重载而不是可选内容时添加一个简单的方法:

只要您有多个只能一起使用的参数,就不要在其上引入可选参数。

或更笼统地说,只要方法签名启用了没有意义的用法模式,就限制可能的调用的排列数量。例如,通过使用重载代替可选的重载(顺便说一句,当您具有相同数据类型的多个参数时,此规则也适用;这里,工厂方法或自定义数据类型之类的设备可以提供帮助)。

例:

enum Match {
    Regex,
    Wildcard,
    ContainsString,
}

// Don't: This way, Enumerate() can be called in a way
//         which does not make sense:
IEnumerable<string> Enumerate(string searchPattern = null,
                              Match match = Match.Regex,
                              SearchOption searchOption = SearchOption.TopDirectoryOnly);

// Better: Provide only overloads which cannot be mis-used:
IEnumerable<string> Enumerate(SearchOption searchOption = SearchOption.TopDirectoryOnly);
IEnumerable<string> Enumerate(string searchPattern, Match match,
                              SearchOption searchOption = SearchOption.TopDirectoryOnly);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.