可选参数,好还是坏?

时间:2013-05-15 13:29:53

标签: vb.net c#-4.0 optional-parameters

我正在编写并浏览我正在使用的项目中的许多方法,并且我认为overloads很有用我认为使用具有默认值的简单optional parameter可以绕过帮助写作更具可读性的问题,我认为有效的代码。

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

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

5 个答案:

答案 0 :(得分:9)

我首先要说明任何语言功能都可以使用得很好或者使用效果不佳。可选参数有一些缺点,就像声明本地作为var或泛型一样。

  

这些副作用是什么

两个想到的。

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

public class Foo
{
    public void Bar(string baz = "cat")
    {
        //Omitted
    }
}

这在AssemblyB中:

public void CallBar()
{
    new Foo().Bar();
}

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

public void CallBar()
{
    new Foo().Bar("cat");
}

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

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

public interface IBar
{
     void Foo(string baz = "cat");
}

和这堂课:

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

这些行会打印不同的内容:

IBar bar1 = new Bar();
bar1.Foo(); //Prints "cat"
var bar2 = new Bar();
bar2.Foo(); //Prints "dog"

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

void Foo(string bar = "bar", string baz = "baz", string yat = "yat")
{
}

创建提供所有可能排列的方法作为默认值,即使不是几十行代码也是如此。

结论:可选参数很好,而且可能很糟糕。就像其他任何东西一样。

答案 1 :(得分:5)

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

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

如果您需要知道参数是用户指定的还是留空,请考虑使其成为过载。有时你可以使用可空值,如果他们传入的地方不允许空值,但通常你不能排除用户通过null的可能性,所以如果你需要知道值来自以及值是什么,不要使用可选参数。

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

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

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

答案 2 :(得分:3)

不断创新。
带有可选参数的是,它们是不良,因为它们不直观-意味着它们不会像您期望的那样运行

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

例如:

您有一个DLL,其中包含诸如此类的代码

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

现在会发生什么,您希望编译器在后台生成此代码:

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

或更现实的是,您希望它传递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);
}

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

但这不是C#编译器的工作,因为那样便无法做:

Foo(a:"dog", c:"dogfood");

因此,C#编译器执行此操作:

例如您在任何地方书写的

Foo(a:"dog", c:"mouse");
or Foo(a:"dog");
or Foo(a:"dog", b:"bla");

它将替换为

Foo(your_value_for_a_or_default, your_value_for_b_or_default, your_value_for_c_or_default);

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

这意味着,如果仅从构成一个应用程序的所有文件中替换DLL,就会破坏使用该DLL的所有应用程序。那真是太糟糕了。因为如果您的DLL包含一个错误,并且必须替换它,则必须使用最新的DLL重新编译整个应用程序。那可能包含很多更改,所以我不能很快做。我可能也没有旧的源代码,并且该应用程序可能进行了重大修改,不知道在旧版本的应用程序上提交了什么提交。因此,我此时可能无法重新编译。太糟糕了。

并且仅在PUBLIC方法中使用它,而不是私有的,受保护的或内部的。
是的,很好的尝试,但是仍然可以使用带有反射的私有,受保护或内部方法。不是因为一个人想要,而是因为有时候它是必要的,因为没有其他方法。 (Example)。

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

但是真正令人不快的是,除此之外,您现在还可以在构造函数中引入API-breaking-changes ...
示例:

public class SomeClass
{
    public SomeClass(bool aTinyLittleBitOfNewSomething = true)
    {
    }
}

现在,无论您在哪里使用

System.Activator.CreateInstance<SomeClass>();

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

请勿使用可选参数!

尤其是在类构造函数中。

从理论上讲,我认为它们适合快速原型制作,但仅此而已。
但是由于原型具有很强的生产力倾向(至少在我目前工作的公司中),因此也不要将其用于生产。

答案 3 :(得分:2)

你没有提高代码的可读性和效率。

首先,您的方法签名将无偿延长。

其次,仅仅为了使用默认值而不存在重载 - 快速查看Convert类应该向您显示。很多时候重载方法都有不同的执行路径,这将成为单个非重载方法中的意大利面条代码。

第三,有时你需要知道一个值是否被用作输入。如果他碰巧使用与您使用的默认值相同的值,那么您如何知道用户是否传递了这些值?

答案 4 :(得分:0)

我经常在C#中看到可选参数,例如IMyInterface parameter = null。 特别是当我在构造函数中看到它时,我什至会说这是一种代码味道。 我知道这是一个艰难的决定-但在这种情况下,它掩盖了您的依赖关系,这很糟糕。

就像vcsjones所说的那样,您可以正确使用这些语言功能,但我相信可选参数仅应在某些情况下使用。

我的看法。