关于c#4.0:你应该在C#4.0中使用重载或可选参数声明方法吗?

Should you declare methods using overloads or optional parameters in C# 4.0?

我在看安德斯关于C 4.0的谈话和C 5.0的预告,这让我想到当C中有可选参数时,推荐的方法是什么,来声明不需要指定所有参数的方法?

例如,类似于FileStream类的类有大约十五个不同的构造器,这些构造器可以分为逻辑"族",例如,下面的构造器来自字符串,来自IntPtr的构造器和来自SafeFileHandle的构造器。

1
2
3
4
5
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中,将密切相关的构造函数和方法组设置为具有可选参数的单个方法是否更有意义,或者是否有充分的理由坚持传统的多过载机制?


我会考虑以下几点:

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

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


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

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

我在我的vb6中使用了可选的back,并且从那以后就错过了它,它将减少C中大量XML注释的重复。


我一直在使用Delphi,带有可选参数。我改为使用重载。

因为当您要创建更多的重载时,您将始终使用可选的参数形式进行配置;然后无论如何,您都必须将它们转换为非可选的。

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


我肯定会使用4.0的可选参数功能。它摆脱了荒谬…

1
2
3
4
5
6
7
8
9
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
}

…并将值放在调用方可以看到的位置…

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

更简单,更不容易出错。我真的把这看作是超负荷情况下的一个错误…

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

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


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

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


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

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

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

当然,这不是一个处理所有方面的答案,但我认为它添加了一个迄今为止还没有涉及的答案。


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

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

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


我期待可选参数,因为它使默认值更接近方法。因此,对于只调用"expanded"方法的重载,您只需定义一次该方法,就可以看到方法签名中的可选参数默认为什么。我宁愿看看:

1
2
3
4
5
6
public Rectangle (Point start = Point.Zero, int width, int height)
{
    Start = start;
    Width = width;
    Height = height;
}

而不是这个:

1
2
3
4
5
6
7
8
9
10
11
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)
{
}

显然,这个例子很简单,但是在带有5个重载的OP中,情况会很快变得拥挤。


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

初始代码

1
2
3
4
public string HandleError(string message, bool silent=true, bool isCritical=true)
{
  ...
}

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

1
HandleError("Disk is full", false);

在这里,事件不是无声的,被视为关键事件。

现在让我们假设在重构之后,我们发现所有错误都会提示用户,因此我们不再需要静默标志。所以我们移除它。

重构后

前一个调用仍在编译,假设它在重构过程中保持不变:

1
2
3
4
5
6
7
8
9
public string HandleError(string message, /*bool silent=true,*/ bool isCritical=true)
{
  ...
}

...

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

现在,false将产生意想不到的影响,事件将不再被视为关键事件。

这可能会导致一个细微的缺陷,因为不会有编译或运行时错误(不像其他选项的警告,比如这个或这个)。

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

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

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


要在使用重载而不是选项时添加一个无需大脑的程序,请执行以下操作:

无论何时,只要您有一些只在一起有意义的参数,就不要在它们上引入选项。

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

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);

当他们(据说?)对于从头开始建模API,有两种在概念上等效的方法可用,不幸的是,当您需要考虑对旧客户机的运行时向后兼容性时,它们有一些细微的区别。我的同事(谢谢布伦特!)向我指出了这篇精彩的文章:带有可选参数的版本控制问题。一些引用:

The reason that optional parameters were introduced to C# 4 in the
first place was to support COM interop. That’s it. And now, we’re
learning about the full implications of this fact. If you have a
method with optional parameters, you can never add an overload with
additional optional parameters out of fear of causing a compile-time
breaking change. And you can never remove an existing overload, as
this has always been a runtime breaking change. You pretty much need
to treat it like an interface. Your only recourse in this case is to
write a new method with a new name. So be aware of this if you plan to
use optional arguments in your APIs.


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

1
2
3
4
5
6
7
8
9
10
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语句。在这种情况下,最好将两个控制流分成两个独立的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)这样的语句时,这同样是一个坏主意。

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


无论是可选参数,方法重载都有其优点或缺点,这取决于您选择它们的偏好。

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

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