可选参数,好还是坏?

Alt*_*F4_ 6 vb.net optional-parameters c#-4.0

我正在编写和浏览我正在使用的项目中的许多方法,并且尽管我觉得overloads有用,但我认为optional parameter使用带有默认值的简单可以解决问题,帮助编写更具可读性,我认为有效的代码.

现在我听说在这些方法中使用这些参数会产生令人讨厌的副作用.

这些副作用是什么,是否值得使用这些参数来保持代码清洁的风险?

vcs*_*nes 12

我将首先说明我的答案,说任何语言功能都可以很好地使用,或者可以很好地使用它.可选参数有一些缺点,就像声明locals var或泛型一样.

这些副作用是什么?

两个想到的.

第一个是可选参数的默认值是嵌入在方法的使用者中的编译时常量.假设我在AssemblyA中有这个类:

public class Foo
{
    public void Bar(string baz = "cat")
    {
        //Omitted
    }
}
Run Code Online (Sandbox Code Playgroud)

这在AssemblyB中:

public void CallBar()
{
    new Foo().Bar();
}
Run Code Online (Sandbox Code Playgroud)

最终产生的是这个,在assemblyB中:

public void CallBar()
{
    new Foo().Bar("cat");
}
Run Code Online (Sandbox Code Playgroud)

因此,如果您要更改默认值Bar,需要重新编译assemblyA和assemblyB.因此,如果方法使用可选参数,而不是内部或私有,我倾向于不将方法声明为公共方法.如果我需要将其声明为public,我会使用重载.

第二个问题是它们如何与接口和多态互动.拿这个界面:

public interface IBar
{
     void Foo(string baz = "cat");
}
Run Code Online (Sandbox Code Playgroud)

而这堂课:

public class Bar : IBar
{
     public void Foo(string baz = "dog")
     {
         Console.WriteLine(baz);
     }
}
Run Code Online (Sandbox Code Playgroud)

这些行将打印不同的东西:

IBar bar1 = new Bar();
bar1.Foo(); //Prints "cat"
var bar2 = new Bar();
bar2.Foo(); //Prints "dog"
Run Code Online (Sandbox Code Playgroud)

这些是我想到的两个消极因素.但是,也有积极的一面.考虑这种方法:

void Foo(string bar = "bar", string baz = "baz", string yat = "yat")
{
}
Run Code Online (Sandbox Code Playgroud)

创建默认情况下提供所有可能排列的方法将是几行(如果不是几十行代码).

结论:可选参数很好,而且可能很糟糕.就像其他事情一样.


Ste*_*ger 8

死灵法术。
带有可选参数的问题是,它们很糟糕,因为它们不直观——这意味着它们的行为方式与您期望的不同

原因如下:
它们破坏了ABI兼容性!
(严格来说,在构造函数中使用时,它们也会破坏API兼容性)

例如:

您有一个 DLL,其中有这样的代码

public void Foo(string a = "dog", string b = "cat", string c = "mouse")
{
    Console.WriteLine(a);
    Console.WriteLine(b);
    Console.WriteLine(c);
}
Run Code Online (Sandbox Code Playgroud)

现在发生的事情是,您希望编译器在幕后生成以下代码:

public void Foo(string a, string b, string c)
{
    Console.WriteLine(a);
    Console.WriteLine(b);
    Console.WriteLine(c);
}

public void Foo(string a, string b)
{
    Foo(a, b, "mouse");        
}

public void Foo(string a)
{
    Foo(a, "cat", "mouse");
}

public void Foo()
{
    Foo("dog", "cat", "mouse");
}
Run Code Online (Sandbox Code Playgroud)

或者更现实地说,您希望它传递 NULL 并执行

public void Foo(string a, string b, string c)
{
    if(a == null) a = "dog";
    if(b == null) b = "cat";
    if(c == null) c = "mouse";

    Console.WriteLine(a);
    Console.WriteLine(b);
    Console.WriteLine(c);
}
Run Code Online (Sandbox Code Playgroud)

因此您可以在一处更改默认参数。

但这不是 C# 编译器所做的,因为那样你就不能做:

Foo(a:"dog", c:"dogfood");
Run Code Online (Sandbox Code Playgroud)

因此,C# 编译器会这样做:

你写的任何地方,例如

Foo(a:"dog", c:"mouse");
or Foo(a:"dog");
or Foo(a:"dog", b:"bla");
Run Code Online (Sandbox Code Playgroud)

它用它代替

Foo(your_value_for_a_or_default, your_value_for_b_or_default, your_value_for_c_or_default);
Run Code Online (Sandbox Code Playgroud)

这意味着如果您添加另一个默认值、更改默认值、删除一个值,您不会破坏 API 兼容性,但会破坏 ABI 兼容性。

所以这意味着,如果您只是从组成应用程序的所有文件中替换 DLL,您将破坏所有使用您的 DLL 的应用程序。那是相当糟糕的。因为如果你的 DLL 包含一个错误的错误,我必须替换它,我必须用你最新的 DLL 重新编译我的整个应用程序。这可能包含很多更改,所以我不能很快完成。我也可能手头没有旧的源代码,并且应用程序可能正在进行重大修改,不知道旧版本的应用程序是在什么提交上编译的。所以我现在可能无法重新编译。这是非常糟糕的。

至于只在 PUBLIC 方法中使用它,而不是私有的、受保护的或内部的。
是的,不错的尝试,但仍然可以使用私有、受保护或内部方法与反射。不是因为一个人想要,而是因为有时这是必要的,因为没有其他办法。(示例)。

接口已经被 vcsjones 提到了。
问题是代码重复(允许不同的默认值 - 或忽略默认值)。

但真正令人失望的是,除此之外,您现在可以在构造函数中引入 API 破坏性更改...
示例:

public class SomeClass
{
    public SomeClass(bool aTinyLittleBitOfNewSomething = true)
    {
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,无论您在哪里使用

System.Activator.CreateInstance<SomeClass>();
Run Code Online (Sandbox Code Playgroud)

你现在会得到一个 RUNTIME 异常,因为现在没有无参数构造函数......
编译器将无法在编译时捕获它。
晚安,如果你Activator.CreateInstance的代码中有很多s。
你会被搞砸,而且很糟糕。
如果您必须维护的某些代码使用反射来创建类实例,或使用反射来访问私有/受保护/内部方法,则将获得奖励积分...

不要使用可选参数!

尤其不是在类构造函数中。
(免责声明:有时,根本没有其他方法 - 例如,属性上的属性自动将属性的名称作为构造函数参数 - 但尽量将其限制在这几种情况下,特别是如果您可以通过重载来完成)


我理论上猜测它们适用于快速原型设计,但仅限于此。
但是由于原型有很强的生产力(至少在我目前工作的公司中),所以也不要使用它。


ana*_*der 5

我想说,当你包含或省略该参数时,它取决于方法的不同.

如果方法的行为和内部功能在没有参数的情况下非常不同,那么使其成为过载.如果您使用可选参数来改变行为,请不要.不是让一个方法用一个参数做一件事,而是在传递第二个参数时做一些不同的事情,而是让一个方法做一件事,另一个方法做另一件事.如果它们的行为差异很大,那么它们应该完全分开,而不是具有相同名称的重载.

如果您需要知道参数是用户指定的还是留空,那么请考虑使其成为过载.有时您可以使用可空值,如果它们传入的位置不允许空值,但通常您不能排除用户通过的可能性null,因此如果您需要知道值来自何处就像值一样,不要使用可选参数.

最重要的是,请记住,可选参数应该(根据定义有点)用于对方法结果具有小的,微不足道或其他不重要影响的事物.如果更改默认值,则在未指定值的情况下调用方法的任何位置仍应对结果感到满意.如果您更改默认值,然后发现调用带有可选参数的方法的其他一些代码留空,现在无法正常工作,那么它可能不应该是可选参数.

使用可选参数最好的地方是:

  • 如果没有提供值,可以安全地将某些内容设置为默认值的方法.这基本上涵盖了呼叫者可能不知道或关心价值的任何内容.一个很好的例子是加密方法 - 调用者可能只是想"我不知道加密,我不知道应该设置什么值R,我只想加密",在这种情况下你设置默认值理智的价值观.通常这些开始是一个带有内部变量的方法,然后您可以将其移动到用户提供的位置.当唯一的区别在于一个人var foo = bar;在开始的某个地方时,制造两种方法毫无意义.
  • 需要具有一组参数但不是全部参数的方法.这对于构造函数来说非常常见; 你会看到各自设置各种属性的不同组合的重载,但是如果有三个或四个参数可能需要或可能不需要设置,那么可能需要大量的重载来覆盖所有可能的组合(它基本上是握手问题),所有这些重载在内部都有或多或少相同的行为.您可以通过让大多数只设置默认值并调用设置所有参数的那个来解决这个问题,但使用可选参数的代码较少.
  • 编码器调用它们的方法可能需要设置参数,但是您希望它们知道"正常"值是什么.例如,我们前面提到的加密方法可能需要各种参数来进行内部数学运算.编码人员可能会看到他们可以传递workFactor或的值blockSize,但他们可能不知道这些值的"正常"值.评论和文档在这里会有所帮助,但可选参数也是如此 - 编码人员会在签名[workFactor = 24], [blockSize = 256]中看到,这有助于他们判断哪种价值是合理的.(当然,这不是没有正确评论和记录您的代码的借口.)