【问题标题】:Optional Parameters, Good or Bad?可选参数,好还是坏?
【发布时间】:2013-05-15 13:29:52
【问题描述】:

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

现在我听说在方法中使用这些参数可能会带来讨厌的副作用。

这些副作用是什么,是否值得冒险使用这些参数来保持代码干净???

【问题讨论】:

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


【解决方案1】:

我将首先说任何语言功能都可以很好地使用,也可以用不好来作为我的答案的开头。可选参数有一些缺点,就像像 var 那样声明局部变量或泛型。

这些副作用是什么

想到了两个。

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

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

这在 AssemblyB 中:

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

真正最终产生的是这个,在汇编B中:

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

创建默认提供所有可能排列的方法需要几行代码,甚至几十行代码。

结论:可选参数是好的,也可能是坏的。就像其他任何东西一样。

【讨论】:

    【解决方案2】:

    死灵术。
    带有可选参数的事情是,它们是BAD,因为它们不直观 - 这意味着它们不会按照您的预期行事

    原因如下:
    他们打破了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 中断更改...
    示例:

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

    现在,无论您在哪里使用

    System.Activator.CreateInstance<SomeClass>();
    

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

    不要使用可选参数!

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


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

    【讨论】:

      【解决方案3】:

      我会说这取决于包含或省略该参数时方法的不同程度。

      如果一个方法的行为和内部功能在没有参数的情况下非常不同,那么就让它成为一个重载。如果您使用可选参数来改变行为,请不要。与其让一个方法用一个参数做一件事,当你传入第二个参数时做一些不同的事情,不如让一个方法做一件事,另一种方法做另一件事。如果它们的行为差异很大,那么它们可能应该是完全独立的,而不是同名的重载。

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

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

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

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

      【讨论】:

        【解决方案4】:

        你没有编写更易读、更高效的代码。

        首先,您的方法签名将无缘无故地变长。

        其次,重载的存在并非仅出于使用默认值的目的——快速浏览一下 Convert 类应该会告诉您这一点。很多时候重载的方法有不同的执行路径,这会在你的单个非重载方法中变成意大利面条代码。

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

        【讨论】:

        • 您的观点似乎是在很多情况下使用可选参数没有意义。我会同意的。另一方面,如果采用少量参数的方法重载的唯一目的是调用一个需要更多参数的重载,同时传递不易更改的额外参数默认值(例如false, 0 , null 等),简单地指定默认参数可能比额外的重载更高效、简洁和可读。
        • 我同意你的代码会更有效率,如果你足够迂腐的话——你最终会在代码执行上每天节省几毫秒,并且你的执行堆栈会更浅一层。但是我花了数年时间为我的同行做代码审查,我完全不同意它会让你的代码更具可读性。我强烈推荐你一本名为Clean Code的书。
        • 我喜欢编写代码来表达一个人实际上想要做什么。如果一个人的目标是为某些参数设置默认值,那么方法具有带有这些默认值的可选参数这一事实本身就可以准确地表达这一点。具有不同数量参数的重载会起作用,但必须实际检查每个重载的代码以确定它是否可能用于其他目的。此外,接口方法可以具有可选参数,而不会增加实现的负担。相比之下,每个重载都需要在每个实现中添加额外的代码。
        • 顺便说一句,关于后一个问题,我真正希望看到的是接口与静态类相关联的机制,然后可以包含成员或重载,它们将被视为接口的一部分(有点像扩展方法,但有更好的范围规则)。如果运行时类加载器可以尝试通过在关联的静态类中搜索合适的方法来填充“缺失的”接口方法,并且——如果它们存在——自动生成方法实现来调用它们,那就太好了。这可以节省很多样板文件。
        【解决方案5】:

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

        就像 vcsjones 所说,您可以正确使用这些语言功能,但我认为可选参数只应在某些极端情况下使用。

        我的意见。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2014-07-23
          • 2012-04-23
          • 2011-10-08
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-05-04
          相关资源
          最近更新 更多