在 C# 4.0 中是不是应该使用重载或可选参数声明方法?
Posted
技术标签:
【中文标题】在 C# 4.0 中是不是应该使用重载或可选参数声明方法?【英文标题】:Should you declare methods using overloads or optional parameters in C# 4.0?在 C# 4.0 中是否应该使用重载或可选参数声明方法? 【发布时间】:2010-09-20 02:15:35 【问题描述】:我在看Anders' talk about C# 4.0 and sneak preview of 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 开始,将密切相关的构造函数组和方法组变成具有可选参数的单个方法会更有意义,还是有充分的理由坚持传统的多次重载机制?
【问题讨论】:
【参考方案1】:我会考虑以下几点:
您是否需要在不支持可选参数的语言中使用您的代码?如果是这样,请考虑包含重载。 您的团队中是否有成员强烈反对可选参数? (有时候,接受一个你不喜欢的决定比争论这个案子更容易。) 您是否确信您的默认值不会在代码构建之间发生变化,或者如果可能,您的调用者会接受吗?我还没有检查默认值是如何工作的,但我假设默认值将被烘焙到调用代码中,与对const
字段的引用非常相似。这通常没问题 - 对默认值的更改无论如何都非常重要 - 但这些都是要考虑的事情。
【讨论】:
实用主义智慧+1:有时候,接受一个你不喜欢的决定比争论这个案子更容易。 @romkyns:不,重载的效果与第 3 点不同。通过提供默认值的重载,默认值在 在库代码中 - 所以如果您更改默认值并提供新版本的库,调用者将看到新的默认值而无需重新编译。而对于可选参数,您需要重新编译以“查看”新的默认值。很多时候这不是一个重要的区别,但它是一个区别。 嗨@JonSkeet,我想知道我们是否同时使用带有可选参数的函数和其他带有重载的方法将被调用?例如 Add(int a, int b) 和 Add(int a,int b,int c=0) 和函数调用说:Add(5,10);哪个方法将被称为重载函数或可选参数函数?谢谢:) @Shekshar:你试过了吗?阅读规范以获取详细信息,但基本上在决胜局中,编译器不必填写任何可选参数的方法获胜。 @JonSkeet 刚才我在上面尝试过...函数重载胜过可选参数:)【参考方案2】:当方法重载通常使用不同数量的参数执行相同的操作时,将使用默认值。
当方法重载根据其参数执行不同的功能时,将继续使用重载。
我在 VB6 的日子里使用 optional 并且后来错过了它,它将减少 C# 中的大量 XML 注释重复。
【讨论】:
【参考方案3】:我一直在使用带有可选参数的 Delphi。我已改用重载。
因为当你去创建更多的重载时,你总是会与可选参数形式发生冲突,然后你必须将它们转换为非可选的。
我喜欢这样的概念,即通常有一个 super 方法,其余的都是围绕该方法的更简单的包装器。
【讨论】:
我非常同意这一点,但是需要注意的是,当您有一个采用多个 (3+) 参数的方法时,这些参数本质上都是“可选的”(可以替换为默认值) ) 你最终可能会得到方法签名的许多排列而没有更多的好处。考虑Foo(A, B, C)
需要Foo(A)
、Foo(B)
、Foo(C)
、Foo(A, B)
、Foo(A, C)
、Foo(B, C)
。【参考方案4】:
我肯定会使用 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 编译器,但得知编译器只是为您发出重载,我不会感到震惊。
【讨论】:
【参考方案5】:可选参数本质上是一段元数据,它指导正在处理方法调用的编译器在调用站点插入适当的默认值。相比之下,重载提供了一种方法,编译器可以通过该方法选择多种方法中的一种,其中一些方法本身可能提供默认值。请注意,如果尝试从用不支持可选参数的语言编写的代码中调用指定可选参数的方法,编译器将要求指定“可选”参数,但由于在不指定可选参数的情况下调用方法是相当于使用等于默认值的参数调用它,这样的语言调用这样的方法是没有障碍的。
在调用站点绑定可选参数的一个重要后果是,它们将根据编译器可用的目标代码版本分配值。如果程序集Foo
具有默认值为5 的方法Boo(int)
,并且程序集Bar
包含对Foo.Boo()
的调用,则编译器会将其处理为Foo.Boo(5)
。如果默认值更改为 6 并重新编译程序集 Foo
,则 Bar
将继续调用 Foo.Boo(5)
,除非或直到使用新版本的 Foo
重新编译它。因此,应该避免对可能发生变化的事物使用可选参数。
【讨论】:
回复:“因此,应该避免对可能发生变化的事物使用可选参数。” 我同意如果客户端代码没有注意到更改,这可能会出现问题。但是,当默认值隐藏在方法重载中时,同样的问题存在:void Foo(int value) … void Foo() Foo(42);
。从外部看,调用者不知道使用什么默认值,也不知道它何时可能更改;必须为此监控书面文件。可选参数的默认值可以这么看:documentation-in-code 默认值是什么。
@stakx:如果无参数重载链接到带有参数的重载,则更改该参数的“默认”值并重新编译重载的定义将更改它使用的值即使调用代码没有重新编译.
是的,但这并不比替代方案更成问题。在一种情况下(方法重载),调用代码对默认值没有发言权。如果调用代码真的完全不关心可选参数及其含义,这可能是合适的。另一种情况(带默认值的可选参数),在默认值改变时,之前编译的调用代码不会受到影响。当调用代码实际上关心参数时,这也是合适的;在源代码中省略它就像在说“当前建议的默认值对我来说是可以的。”
我在这里要强调的一点是,虽然这两种方法都会产生后果(就像您指出的那样),但它们本质上并不是有利或不利的。这也取决于调用代码的需求和目标。从那个 POV 来看,我发现你回答的最后一句话中的判断有些过于僵化了。
@stakx:我说的是“避免使用”而不是“从不使用”。如果更改 X 将意味着 Y 的下一次重新编译将更改 Y 的行为,那将需要配置构建系统以便每次重新编译 X 也重新编译 Y(减慢速度),或者产生程序员更改的风险X 以一种在下次编译时会破坏 Y 的方式,并且只有在以后由于某些完全不相关的原因而更改 Y 时才会发现这种破坏。只有在其优势超过此类成本时才应使用默认参数。【参考方案6】:
是否应该使用可选参数或重载可以争论,但最重要的是,每个都有自己不可替代的领域。
可选参数与命名参数结合使用时,与 COM 调用的一些长参数列表和所有可选参数结合使用时非常有用。
例如,当方法能够对许多不同的参数类型(只是示例之一)进行操作并在内部进行强制转换时,重载非常有用;您只需使用任何有意义的数据类型(被一些现有的重载接受)提供它。不能用可选参数打败它。
【讨论】:
【参考方案7】:我最喜欢可选参数的一个方面是,如果不提供参数,即使不转到方法定义,您也可以看到参数会发生什么。 Visual Studio 将在您键入方法名称时简单地显示参数的默认值。使用重载方法时,您要么阅读文档(如果可用),要么直接导航到方法的定义(如果可用)和重载包装的方法。
特别是:文档工作量可能会随着重载数量的增加而迅速增加,您最终可能会从现有重载中复制已经存在的 cmets。这很烦人,因为它不会产生任何价值并且会破坏DRY-principle)。另一方面,使用可选参数时,恰好处记录了所有参数,您可以在键入时看到它们的含义以及它们的默认值。
最后但同样重要的是,如果您是 API 的使用者,您甚至可能没有检查实现细节的选项(如果您没有源代码),因此没有机会查看哪个超级方法重载的正在包装。因此,您只能阅读文档并希望在其中列出所有默认值,但情况并非总是如此。
当然,这不是一个涵盖所有方面的答案,但我认为它增加了一个迄今为止尚未涵盖的答案。
【讨论】:
【参考方案8】:我期待可选参数,因为它使默认值更接近方法。因此,无需为仅调用“扩展”方法的重载提供数十行代码,您只需定义一次方法,即可在方法签名中看到可选参数的默认值。我宁愿看:
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 个重载的情况下,事情会变得非常拥挤。
【讨论】:
我听说可选参数应该放在最后,不是吗? 取决于您的设计。也许“开始”参数通常很重要,除非它不重要。也许您在其他地方有相同的签名,意味着不同的东西。举一个人为的例子, public Rectangle(int width, int height, Point innerSquareStart, Point innerSquareEnd) 从他们的谈话中说,可选参数必须在必填参数之后。【参考方案9】:在许多情况下,可选参数用于切换执行。例如:
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)
之类的语句时,这同样是个坏主意。
你可以在这些链接上找到详细的分析:Avoiding Optional Parameters和Avoiding Null Parameters
【讨论】:
【参考方案10】:可选参数的一个警告是版本控制,其中重构会产生意想不到的后果。一个例子:
初始代码
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)。
请注意,同样的问题有多种形式。另一种形式是here。
另请注意,在调用方法时严格使用命名参数将避免该问题,例如:HandleError("Disk is full", silent:false)
。但是,假设所有其他开发人员(或公共 API 的用户)都会这样做可能不切实际。
出于这些原因,除非有其他令人信服的考虑因素,否则我会避免在公共 API 中使用可选参数(或者甚至是公共方法,如果它可能被广泛使用)。
【讨论】:
【参考方案11】:虽然它们是(假设是?)两种概念上等效的方法,可供您从头开始建模 API,但不幸的是,当您需要考虑旧客户端的运行时向后兼容性时,它们存在一些细微的差异。我的同事(感谢布伦特!)向我指出了这个wonderful post: Versioning issues with optional arguments。部分引述:
在 C# 4 中引入可选参数的原因 首先是支持 COM 互操作。而已。而现在,我们 了解这一事实的全部含义。如果你有一个 带有可选参数的方法,你永远不能添加一个重载 额外的可选参数,因为担心导致编译时 突破性的变化。而且您永远无法删除现有的重载,因为 这一直是运行时的重大变化。你非常需要 把它当作一个接口来对待。在这种情况下,您唯一的办法是 用新名称编写一个新方法。因此,如果您打算这样做,请注意这一点 在您的 API 中使用可选参数。
【讨论】:
【参考方案12】:在使用重载而不是可选项时添加一个不费吹灰之力:
当您有许多参数只能一起有意义时,不要在它们上引入可选参数。
或者更一般地说,每当您的方法签名启用没有意义的使用模式时,请限制可能调用的排列数量。例如,通过使用重载而不是可选项(顺便说一句,当您有多个相同数据类型的参数时,此规则也适用;在这里,工厂方法或自定义数据类型等设备会有所帮助)。
例子:
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);
【讨论】:
【参考方案13】:Optional parameter , Method 重载各有优劣,看你的喜好了。
可选参数: 仅在 .Net 4.0 中可用。 可选参数减少您的代码大小。 你不能定义 out 和 ref 参数
重载方法: 您可以定义 Out 和 ref 参数。 代码量会增加,但重载的方法很容易理解。
【讨论】:
以上是关于在 C# 4.0 中是不是应该使用重载或可选参数声明方法?的主要内容,如果未能解决你的问题,请参考以下文章