你应该申报方法使用载或可选择的参数在C#4.0?
-
05-07-2019 - |
题
我正在看 安德斯*'谈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将使它更有意义的作密切相关的群体的构造和方法的一个方法可选择的参数,或是有一个很好的理由坚持传统的许多过载机制?
解决方案
我会考虑以下几点:
- 你需要你们的代码可以使用的语言,它不支持任择参数?如果是这样,考虑包括过载。
- 你有任何成员在你的团队,他们猛烈地反对任择参数?(有时更容易生活的一个决定你不喜欢比争论的情况。)
- 你相信,你的默认是不会变化之间建立的码,或者如果他们可能,你的呼叫会好吗?
我没有检查如何的违约要工作,但我认为默认的价值观将会被烤成的调用代码,如同作为参考 const
领域。这是通常的好-改变为默认值是相当重要无论如何,但这些是要考虑的事项。
其他提示
当方法重载通常使用不同数量的参数执行相同的操作时,将使用默认值。
当方法重载根据其参数执行不同的函数时,将继续使用重载。
我在VB6的日子里使用了可选的,并且从那时起就错过了它,它将减少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
的方法 Boo(int)
的默认值为5,而程序集 Bar
包含对 Foo的调用.Boo()
,编译器将其处理为 Foo.Boo(5)
。如果默认值更改为6并且汇编 Foo
重新编译, Bar
将继续调用 Foo.Boo(5)
,除非或直到它使用新版本的 Foo
重新编译。因此,应该避免对可能发生变化的事物使用可选参数。
我期待可选参数,因为它保持默认值更接近方法。因此,重新调用“扩展”的重载代替了几十行。方法,您只需定义一次方法,您就可以在方法签名中看到可选参数的默认值。我宁愿看看:
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个重载的情况,事情可以快速拥挤。
可以争论是否应该使用可选参数或重载,但最重要的是,每个参数都有自己不可替代的区域。
与命名参数结合使用时,可选参数与COM调用的一些long-argument-lists-all-optionals结合使用时非常有用。
当方法能够在许多不同的参数类型(仅仅是一个示例)上运行时,重载非常有用,并且例如在内部执行转换;你只需要提供任何有意义的数据类型(一些现有的重载都可以接受)。无法用可选参数击败它。
可选参数的一个最受欢迎的方面是,如果您不提供参数,您会看到参数会发生什么,即使不进行方法定义也是如此。当您键入方法名称时, Visual Studio将只显示参数的默认值。使用重载方法,您可以阅读文档(如果可用)或直接导航到方法的定义(如果可用)以及重载包装的方法。
特别是:文档工作量可能会随着重载量的增加而迅速增加,并且您可能最终会从现有的重载中复制已存在的注释。这非常令人讨厌,因为它没有产生任何价值并打破了 DRY-principle )。另一方面,使用可选参数,恰好有一个,其中所有参数都记录在案,并且您在键入时会看到它们的含义以及默认值。
最后但并非最不重要的是,如果您是API的使用者,您甚至可能无法选择检查实现细节(如果您没有源代码),因此无法查看哪种超级方法超载的是包装。因此,您不得不阅读文档并希望列出所有默认值,但情况并非总是如此。
当然,这不是一个处理所有方面的答案,但我认为它增加了一个目前尚未涉及的问题。
可选参数的一个警告是版本控制,其中重构具有意想不到的后果。一个例子:
初始代码
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 。
请注意,同一问题有很多种形式。另一种形式是此处。
另请注意,在调用方法时严格使用命名参数将避免此问题,例如: HandleError(“Disk is full”,silent:false)
。但是,假设所有其他开发人员(或公共API的用户)都这样做可能是不切实际的。
由于这些原因,我会避免在公共API中使用可选参数(如果可以广泛使用,甚至可以使用公共方法),除非有其他令人信服的考虑因素。
两个可选参数,Method重载都有自己的优点或缺点。这取决于你喜欢在它们之间做出选择。
可选参数: 仅在.Net 4.0中可用。 可选参数减少您的代码大小。 您无法定义out和ref参数
重载方法: 您可以定义Out和ref参数。 代码大小会增加,但重载方法很容易理解。
在许多情况下,可选参数用于切换执行。例如:
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;
}
通过这种方式,我们甚至可以保护班级免受零折扣的呼叫。该呼叫意味着呼叫者认为存在折扣,但事实上根本没有折扣。这种误解很容易造成错误。
在这种情况下,我不希望有可选参数,而是强制调用者明确选择适合其当前情况的执行方案。
情况非常类似于可以为null的参数。当实现沸腾到诸如 if(x == null)
之类的语句时,这同样是个坏主意。
虽然它们(据说是?)有两种概念上等同的方式可供您从头开始建模API,但遗憾的是,当您需要考虑旧的客户端的运行时向后兼容性时,它们会有一些细微差别。我的同事(感谢布伦特!)向我指出了这个精彩帖子:可选参数的版本控制问题。有人引用它:
将可选参数引入C#4的原因 第一个地方是支持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);