c#4.0-应该使用c#4.0中的重载或可选参数来声明方法吗

本文关键字:c#4 参数 方法 声明 重载 | 更新日期: 2023-09-27 17:47:47

我观看了Anders关于C#4.0和C#5.0预览的演讲,这让我思考当C#中有可选参数时,声明不需要指定所有参数的方法的推荐方法是什么?

例如,像FileStream类这样的类有大约十五个不同的构造函数,它们可以分为逻辑"族",例如下面来自字符串的构造函数、来自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开始,将密切相关的构造函数和方法组作为一个具有可选参数的单一方法会更有意义吗?还是有充分的理由坚持传统的多过载机制?

c#4.0-应该使用c#4.0中的重载或可选参数来声明方法吗

我会考虑以下内容:

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

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

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

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

我在VB6的日子里使用了optional,但后来错过了它,它将减少C#中大量的XML注释重复。

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

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

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

我肯定会使用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编译器,但如果知道编译器只是为您发出过载,我不会感到震惊。

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

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

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

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

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

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

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)这样的语句时,这同样是个坏主意。

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

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

初始代码

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将产生意想不到的影响,该事件将不再被视为关键事件。

这可能会导致一个微妙的缺陷,因为不会出现编译或运行时错误(不像其他一些选项的注意事项,如This或This)。

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

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

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

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

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

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

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

我很期待可选参数,因为它保持了更接近方法的默认值。因此,您只需定义一次方法,就可以看到方法签名中可选参数的默认值,而不是只调用"扩展"方法的重载的几十行。我宁愿看:

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)
{
}

显然,这个例子非常简单,但在有5个过载的OP中,情况可能会很快变得拥挤。

虽然它们(应该是?)是两种概念上等效的方法,可供您从头开始为API建模,但不幸的是,当您需要考虑野外旧客户端的运行时向后兼容性时,它们有一些细微的差异。我的同事(谢谢Brent!)向我介绍了这篇精彩的帖子:使用可选参数的版本控制问题。引用其中的一句话:

在首先是支持COM互操作。就是这样。现在,我们了解这一事实的全部含义。如果你有方法,则永远不能使用添加重载额外的可选参数,因为担心会导致编译时间突破性变化。而且您永远无法删除现有的过载,因为这一直是一个破坏运行时的变化。你非常需要把它当作一个接口。在这种情况下,你唯一的追索权是用新名称编写一个新方法。因此,如果你计划在API中使用可选参数。

在使用重载而不是选项时添加一个无需思考的问题:

只要有多个参数只有在一起才有意义,就不要在它们上引入选项。

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

示例:

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);

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

可选参数:仅在中提供。Net 4.0。可选参数可减小代码大小。您无法定义和引用参数

重载方法:可以定义输出参数和引用参数。代码大小会增加,但重载方法很容易理解。