可选函数参数:使用默认参数(NULL)还是重载函数?

Posted

技术标签:

【中文标题】可选函数参数:使用默认参数(NULL)还是重载函数?【英文标题】:Optional function parameters: Use default arguments (NULL) or overload the function? 【发布时间】:2010-10-16 18:00:18 【问题描述】:

我有一个处理给定向量的函数,但如果没有给出,也可以自己创建这样的向量。

对于这种情况,我看到了两种设计选择,其中函数参数是可选的:

将其设为指针并默认设为NULL

void foo(int i, std::vector<int>* optional = NULL) 
  if(optional == NULL)
    optional = new std::vector<int>();
    // fill vector with data
  
  // process vector

或者有两个具有重载名称的函数,其中一个省略了参数:

void foo(int i) 
   std::vector<int> vec;
   // fill vec with data
   foo(i, vec);


void foo(int i, const std::vector<int>& optional) 
  // process vector

是否有理由选择一种解决方案而不是另一种?

我更喜欢第二个,因为我可以将向量设为const 引用,因为它在提供时只能读取,不能写入。此外,界面看起来更干净(NULL 不只是一个 hack 吗?)。并且间接函数调用导致的性能差异可能被优化掉了。

然而,我经常在代码中看到第一个解决方案。除了程序员的懒惰之外,还有令人信服的理由喜欢它吗?

【问题讨论】:

【参考方案1】:

我不会使用任何一种方法。

在这种情况下, foo() 的目的似乎是处理一个向量。也就是说,foo() 的工作是处理向量。

但是在 foo() 的第二个版本中,它隐含地赋予了第二个工作:创建向量。 foo() 版本 1 和 foo() 版本 2 之间的语义不同。

如果你需要这样的东西,我会考虑只使用一个 foo() 函数来处理向量,以及创建向量的另一个函数,而不是这样做。

例如:

void foo(int i, const std::vector<int>& optional) 
  // process vector


std::vector<int>* makeVector() 
   return new std::vector<int>;

显然这些函数是微不足道的,如果所有 makeVector() 需要做的就是完成它的工作只是调用 new,那么使用 makeVector() 函数可能没有意义。但我敢肯定,在您的实际情况下,这些函数的作用远不止此处所示,我上面的代码说明了语义设计的一种基本方法:让一个函数完成一项工作

我上面对 foo() 函数的设计还说明了我个人在代码中使用的另一种基本方法,当涉及到设计接口时——包括函数签名、类等。就是这样:我相信一个好的界面是 1) 简单直观地正确使用,2) 难以或不可能错误地使用。在 foo() 函数的情况下,我们含蓄地说,在我的设计中,向量必须已经存在并且“准备好”。通过将 foo() 设计为获取引用而不是指针,调用者必须已经有一个向量是直观的,并且他们将很难传递一些不是现成向量的东西.

【讨论】:

这是最好的建议。首先,简化。我很惊讶地发现它位于答案堆栈的底部。 完全同意;我认为最好的答案 但是在版本 2 中创建向量在外部是不可见的 - 所以除了接受额外的参数之外,版本 1 和版本 2 的语义相同的,不是他们?【参考方案2】:

我绝对赞成第二种方法的重载方法。

第一种方法(可选参数)模糊了方法的定义,因为它不再有一个明确定义的目的。这反过来又增加了代码的复杂性,使不熟悉它的人更难理解。

使用第二种方法(重载方法),每种方法都有明确的目的。每种方法都结构良好内聚。一些附加说明:

如果需要将代码复制到两个方法中,可以将其提取到单独的方法中,并且每个重载的方法都可以调用此外部方法。 我会更进一步,以不同的方式命名每个方法,以表明这些方法之间的差异。这将使代码更具自我记录性。

【讨论】:

我对这个答案有点意见,但我不同意你的推理。重载的重点是每个方法都已经做了同样的事情,这就是为什么我们不关心通过名称来区分它们的原因。通常重载只会在它们接受的参数的 type 方面有所不同——想到一组print( int )print( double ) 类型的函数。如果有一个可选的返回参数,那应该只是意味着调用者可能关心也可能不关心 rx 该可选结果。我同意这个答案的最后一句话:如果函数做不同的事情,那么是的,每个方法的名称不同【参考方案3】:

虽然我确实理解许多人对默认参数和重载的抱怨,但似乎对这些功能提供的好处缺乏了解。

默认参数值: 首先我想指出,在项目的初始设计中,如果设计得当,默认值应该很少甚至没有用处。然而,默认值的最大优势在于现有项目和完善的 API。我从事的项目包含数百万行现有代码,并且没有机会重新编写所有代码。因此,当您希望添加需要额外参数的新功能时;新参数需要默认值。否则,您将破坏使用您项目的每个人。这对我个人来说没问题,但我怀疑你的公司或你的产品/API 的用户会喜欢在每次更新时重新编码他们的项目。 简单地说,默认值非常适合向后兼容!这通常是您在大型 API 或现有项目中看到默认值的原因。

函数覆盖: 功能覆盖的好处是它们允许共享功能概念,但具有不同的选项/参数。然而,很多时候我看到函数覆盖被懒惰地用来提供截然不同的功能,只是参数略有不同。在这种情况下,它们每个都应该有单独命名的函数,与它们的特定功能有关(与 OP 的示例一样)。

这些,c/c++ 的特性很好,如果使用得当,效果很好。这可以说是大多数编程功能。当它们被滥用/误用时,它们就会引起问题。

免责声明: 我知道这个问题已经有几年的历史了,但由于这些答案出现在我今天(2012 年)的搜索结果中,我觉得这需要为未来的读者进一步解决。

【讨论】:

【参考方案4】:

在 C++ 中引用不能为 NULL,一个非常好的解决方案是使用 Nullable 模板。 这会让你做的事情是 ref.isNull()

你可以在这里使用:

template<class T>
class Nullable 
public:
    Nullable() 
        m_set = false;
    
    explicit
    Nullable(T value) 
        m_value = value;
        m_set = true;
    
    Nullable(const Nullable &src) 
        m_set = src.m_set;
        if(m_set)
            m_value = src.m_value;
    
    Nullable & operator =(const Nullable &RHS) 
        m_set = RHS.m_set;
        if(m_set)
            m_value = RHS.m_value;
        return *this;
    
    bool operator ==(const Nullable &RHS) const 
        if(!m_set && !RHS.m_set)
            return true;
        if(m_set != RHS.m_set)
            return false;
        return m_value == RHS.m_value;
    
    bool operator !=(const Nullable &RHS) const 
        return !operator==(RHS);
    

    bool GetSet() const 
        return m_set;
    

    const T &GetValue() const 
        return m_value;
    

    T GetValueDefault(const T &defaultValue) const 
        if(m_set)
            return m_value;
        return defaultValue;
    
    void SetValue(const T &value) 
        m_value = value;
        m_set = true;
    
    void Clear()
    
        m_set = false;
    

private:
    T m_value;
    bool m_set;
;

现在你可以拥有

void foo(int i, Nullable<AnyClass> &optional = Nullable<AnyClass>()) 
   //you can do 
   if(optional.isNull()) 

   

【讨论】:

也可以看看std::optional【参考方案5】:

我同意,我会使用两个函数。基本上,你有两个不同的用例,所以有两个不同的实现是有意义的。

我发现我编写的 C++ 代码越多,我的参数默认值就越少 - 如果该功能被弃用,我不会真的流泪,尽管我将不得不重新编写大量旧代码!

【讨论】:

完全同意第二段...默认参数(除了模板参数)几乎从来都不是一个好的设计选择。 除非它们真的是构造函数的唯一选择(无论如何在 C++'03 中)。【参考方案6】:

我通常避免第一种情况。请注意,这两个功能的作用不同。其中一个用一些数据填充向量。另一个没有(只接受来自调用者的数据)。我倾向于命名实际上做不同事情的不同功能。实际上,即使您编写它们,它们也是两个函数:

foo_default(或只是foofoo_with_values

至少我发现这种区别在 long therm 中更清晰,对于偶尔的库/函数用户而言。

【讨论】:

【参考方案7】:

我也更喜欢第二个。虽然两者之间没有太大区别,但您基本上使用 foo(int i) 重载中的主要方法的功能,并且主要重载可以完美地工作而无需关心是否存在缺少另一个重载,因此在重载版本中有更多的关注点分离。

【讨论】:

【参考方案8】:

在 C++ 中,您应该尽可能避免允许使用有效的 NULL 参数。原因是它大大减少了调用站点文档。我知道这听起来很极端,但我使用的 API 需要超过 10-20 个参数,其中一半可以有效地为 NULL。生成的代码几乎不可读

SomeFunction(NULL, pName, NULL, pDestination);

如果您将其切换为强制 const 引用,则代码只会被强制变得更具可读性。

SomeFunction(
  Location::Hidden(),
  pName,
  SomeOtherValue::Empty(),
  pDestination);

【讨论】:

你是对的,将默认值命名(也许#defining它为0)比func( arg, 0, 0, 0, 0, 0, 0 );更容易阅读【参考方案9】:

我完全属于“超载”阵营。其他人添加了有关您的实际代码示例的细节,但我想补充一下我认为在一般情况下使用重载与默认值相比的好处。

任何参数都可以“默认” 如果覆盖函数使用不同的默认值,则没有问题。 不必为现有类型添加“hacky”构造函数以使其具有默认值。 可以默认输出参数,而无需使用指针或 hacky 全局对象。

在每个上放一些代码示例:

任何参数都可以默认:

class A ; class B ; class C ;

void foo (A const &, B const &, C const &);

inline void foo (A const & a, C const & c)

  foo (a, B (), c);    // 'B' defaulted

没有覆盖具有不同默认值的函数的危险:

class A 
public:
  virtual void foo (int i = 0);
;

class B : public A 
public:
  virtual void foo (int i = 100);
;


void bar (A & a)

  a.foo ();           // Always uses '0', no matter of dynamic type of 'a'

不必为现有类型添加“hacky”构造函数以允许它们被默认:

struct POD 
  int i;
  int j;
;

void foo (POD p);     // Adding default (other than 0, 0)
                      // would require constructor to be added
inline void foo ()

  POD p =  1, 2 ;
  foo (p);

输出参数可以默认,无需使用指针或hacky全局对象:

void foo (int i, int & j);  // Default requires global "dummy" 
                            // or 'j' should be pointer.
inline void foo (int i)

  int j;
  foo (i, j);

规则重载与默认值的唯一例外是构造函数,目前无法将构造函数转发给另一个构造函数。 (我相信 C++ 0x 会解决这个问题)。

【讨论】:

【参考方案10】:

我倾向于第三种选择: 分成两个函数,但不要重载。

从本质上讲,重载不太可用。它们要求用户了解两个选项并弄清楚它们之间的区别,如果他们愿意,还需要检查文档或代码以确保哪个是哪个。

我会有一个接受参数的函数, 还有一个叫做“createVectorAndFoo”或类似的东西(显然命名变得更容易解决实际问题)。

虽然这违反了“函数的两个职责”规则(并给它一个长名称),但我相信当你的函数确实做了两件事(创建向量和 foo 它)时,这更可取。

【讨论】:

我会反驳说,如果这个函数真的做了两件事,那么这是一个设计缺陷;应该修复的破窗(参见“实用程序员”)。【参考方案11】:

一般来说,我同意其他人使用双功能方法的建议。但是,如果在使用 1 参数形式时创建的向量始终相同,您可以通过将其设为静态并使用默认的 const&amp; 参数来简化事情:

// Either at global scope, or (better) inside a class
static vector<int> default_vector = populate_default_vector();

void foo(int i, std::vector<int> const& optional = default_vector) 
    ...

【讨论】:

【参考方案12】:

第一种方法比较差,因为你无法判断你是不小心传入了 NULL 还是故意这样做的......如果这是一个意外,那么你很可能导致了一个错误。

使用第二个,您可以测试(断言,无论如何)NULL 并适当地处理它。

【讨论】:

以上是关于可选函数参数:使用默认参数(NULL)还是重载函数?的主要内容,如果未能解决你的问题,请参考以下文章

基于可选参数存在而不使用函数重载的打字稿函数返回类型

TS 函数重载

TypeScript系列教程11函数的使用

可选参数的函数还可以这样设计!

Typescript的函数

Hive 窗口分析函数