95

我正在观看Anders 关于 C# 4.0 和 C# 5.0 预览的谈话,这让我想到了当可选参数在 C# 中可用时,推荐的方式来声明不需要指定所有参数的方法?

例如,类之FileStream类的东西有大约 15 个不同的构造函数,它们可以分为逻辑“家族”,例如来自字符串的下面的那些,来自 anIntPtr的那些和来自 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 开始,将密切相关的构造函数和方法组制作成具有可选参数的单个方法会更有意义,还是有充分的理由坚持传统的多重载机制?

4

13 回答 13

126

我会考虑以下几点:

  • 您是否需要从不支持可选参数的语言中使用您的代码?如果是这样,请考虑包括重载。
  • 您的团队中是否有任何成员强烈反对可选参数?(有时,接受一个你不喜欢的决定比争论这个案子更容易。)
  • 您是否确信您的默认设置不会在代码构建之间发生变化,或者如果可能,您的调用者会同意吗?

我还没有检查默认值是如何工作的,但我假设默认值将被烘焙到调用代码中,与对const字段的引用非常相似。这通常没问题 - 对默认值的更改无论如何都非常重要 - 但这些都是要考虑的事情。

于 2008-10-30T21:55:12.820 回答
19

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

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

我在我的 VB6 时代使用 optional 并且后来错过了它,它将减少 C# 中的大量 XML 注释重复。

于 2008-10-30T21:47:37.377 回答
11

我一直在使用带有可选参数的 Delphi。我已经改用重载。

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

而且我喜欢这样的概念,即通常有一种超级方法,其余的是围绕该方法的更简单的包装器。

于 2009-06-24T16:55:56.270 回答
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 编译器,但得知编译器只是为您发出重载,我不会感到震惊。

于 2009-06-24T16:53:38.533 回答
6

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

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

于 2013-01-21T01:41:51.757 回答
5

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

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

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

于 2010-05-21T10:16:13.147 回答
4

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

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

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

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

于 2016-03-08T08:12:44.893 回答
3

我期待可选参数,因为它使默认值更接近方法。因此,无需为仅调用“扩展”方法的重载提供数十行代码,您只需定义一次方法,您就可以在方法签名中看到可选参数的默认值。我宁愿看:

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 个重载的情况下,事情会很快变得拥挤。

于 2008-10-30T21:51:09.600 回答
2

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

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;

}

通过这种方式,我们甚至保护了班级免于接到零折扣的电话。该调用意味着调用者认为有折扣,但实际上根本没有折扣。这样的误解很容易导致bug。

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

这种情况与可以为空的参数非常相似。当实现归结为if (x == null).

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

于 2015-06-10T08:58:34.410 回答
2

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

初始代码

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 中使用可选参数(或者甚至是公共方法,如果它可能被广泛使用)。

于 2018-03-07T23:00:35.147 回答
1

虽然它们(假设是?)两种概念上等效的方式可供您从头开始对 API 进行建模,但不幸的是,当您需要考虑旧客户端的运行时向后兼容性时,它们存在一些细微的差异。我的同事(感谢 Brent!)向我指出了这篇精彩的帖子:可选参数的版本控制问题。引用其中的一些话:

将可选参数引入 C# 4 的原因首先是为了支持 COM 互操作。就是这样。现在,我们正在了解这一事实的全部含义。如果您有一个带有可选参数的方法,则永远不能添加带有其他可选参数的重载,以免导致编译时中断更改。而且您永远无法删除现有的重载,因为这一直是运行时的重大更改。你几乎需要把它当作一个界面来对待。在这种情况下,您唯一的办法是编写一个具有新名称的新方法。因此,如果您打算在 API 中使用可选参数,请注意这一点。

于 2016-09-14T19:58:34.620 回答
1

要在使用重载而不是可选项时添加一个不费脑筋的问题:

每当您有许多只能一起有意义的参数时,不要在它们上引入可选参数。

或者更一般地说,每当您的方法签名启用没有意义的使用模式时,请限制可能调用的排列数量。例如,通过使用重载而不是可选项(顺便说一句,当您有多个相同数据类型的参数时,此规则也适用;在这里,工厂方法或自定义数据类型等设备会有所帮助)。

例子:

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);
于 2017-12-08T13:52:11.067 回答
0

可选参数,方法重载都有自己的优点或缺点。这取决于您在它们之间进行选择的偏好。

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

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

于 2012-12-31T07:27:37.957 回答