是否有“空”的简明对立面?

时间:2014-02-19 13:57:20

标签: c++ string api coding-style readability

字符串类的接口通常具有名为IsEmptyVCL)或emptySTL)的方法。这是绝对合理的,因为它是一个特例,但使用这些方法的代码通常必须否定这个谓词,这会导致“光学(甚至是心理上的)开销” (感叹号不是很明显,尤其是在开括号后)。例如,参见这个(简化的)代码:

/// format an optional time specification for output
std::string fmtTime(const std::string& start, const std::string& end)
{
    std::string time;
    if (!start.empty() || !end.empty()) {
        if (!start.empty() && !end.empty()) {
            time = "from "+start+" to "+end;
        } else {
            if (end.empty()) {
                time = "since "+start;
            } else {
                time = "until "+end;
            }
        }
    }
    return time;
}

它有四个否定,因为空案例是要跳过的。在设计界面时,我经常会观察到这种否定,而且不是一个大问题,但这很烦人。我只想支持编写易于理解且易于阅读的代码。我希望你能理解我的观点。

也许我只是失明了:你怎么解决上述问题?


编辑:在阅读了一些评论之后,我认为原始代码使用VCL的System::AnsiString类是有必要的。这个类提供了一个非常易读的IsEmpty方法:

 if (text.IsEmpty()) { /* ... */ } // read: if text is empty ...

如果没有否定:

 if (!text.IsEmpty()) { /* ... */} // read: if not text is empty ... 

...而不是如果文字不为空。我认为文字is最好留给读者的幻想,让否定的工作做得好。好吧,也许不是一个普遍的问题...

13 个答案:

答案 0 :(得分:27)

在大多数情况下,您可以颠倒ifelse的顺序来清理代码:

const std::string fmtTime(const std::string& start, const std::string& end)
{
    std::string time;
    if (start.empty() && end.empty()) {
        return time;
    }

    if (start.empty() || end.empty()) {
        if (end.empty()) {
            time = "since "+start;
        } else {
            time = "until "+end;
        }
    } else {
        time = "from "+start+" to "+end;
    }
    return time;
}

或者在进行一些重构后更加清洁:

std::string fmtTime(const std::string& start, const std::string& end)
{
    if (start.empty() && end.empty()) {
        return std::string();
    }

    if (start.empty()) {
        return "until "+end;
    }    

    if (end.empty()) {
        return "since "+start;
    }

    return "from "+start+" to "+end;
}

最终的紧凑性(虽然我更喜欢以前的版本,因为它的可读性):

std::string fmtTime(const std::string& start, const std::string& end)
{
    return start.empty() && end.empty() ? std::string()
         : start.empty()                ? "until "+end
         :                  end.empty() ? "since "+start
                                        : "from "+start+" to "+end;
}

另一种可能性是创建辅助函数:

inline bool non_empty(const std::string &str) {
  return !str.empty();
}

if (non_empty(start) || non_empty(end)) {
...
}

答案 1 :(得分:12)

我认为我已经消除了支持一点数学的条件:

const std::string fmtTime(const std::string& start, const std::string& end) {

    typedef std::string const &s;

    static const std::function<std::string(s, s)> f[] = {
       [](s a, s b) { return "from " + a + " to " + b; }           
       [](s a, s b) { return "since " + a; },
       [](s a, s b) { return "until " + b; },
       [](s a, s b) { return ""; },
    };

   return f[start.empty() * 2 + end.empty()](start, end);
}

编辑:如果您愿意,可以将数学表达为start.empty() * 2 + end.empty()。为了理解发生了什么,也许最好是我阐述一下如何从一开始就考虑事物。我认为事物是一个二维数组:

enter image description here

(可以随意交换“开始空”和“结束空”,具体取决于您是否愿意按行主要或列主要顺序进行思考。)

如果您愿意,start.empty()end.empty()(或它们的逻辑not每个都充当此2D矩阵的一个维度的索引。数学涉及简单地“线性化”该寻址,因此我们得到一个长行而不是两行和两列,如下所示:

enter image description here

在数学术语中,这是“行*列+列”的简单问题(或者,反之亦然,取决于您是否更喜欢行主列或列主键排序)。我最初将* 2部分表示为比特移位,并将加法表示为逐位or(因为先前的左移,因此知道最低有效位为空)。我发现这很容易处理,但我想我可以理解其他人可能没有的地方。

我应该补充一点:虽然我已经提到了row-major与column-major,但是从两个“x.empty”值到数组中的位置的映射基本上是任意的。我们从.empty()获得的值意味着当值不存在时我们得到0,而当它不存在时得到1。因此,从原始值到数组位置的直接映射可能是这样的:

enter image description here

由于我们将值线性化,因此我们对映射的方式有几个选择:

  1. 只需安排数组以适应我们得到的值。
  2. 单独反转每个维度的值(这基本上是导致原始问题的原因 - !x.empty()的持续使用)
  3. 将两个输入组合成一个线性地址,然后通过减去3来“反转”。
  4. 对于那些怀疑这种效率的人来说,它实际上已经归结为(使用VC ++):

    mov eax, ebx
    cmp QWORD PTR [rsi+16], rax
    sete    al
    cmp QWORD PTR [rdi+16], 0
    sete    bl
    lea eax, DWORD PTR [rbx+rax*2]
    movsxd  rcx, eax
    shl rcx, 5
    add rcx, r14
    mov r9, rdi
    mov r8, rsi
    mov rdx, rbp
    call    <ridiculously long name>::operator()
    

    即使f的一次性构造也没有人们想象的那么糟糕。它不涉及动态分配,也不涉及该订单上的任何内容。名字很长,最初看起来有点可怕,但最后,它主要是四次重复:

    lea rax, OFFSET FLAT:??_7?$_Func_impl@U?$_Callable_obj@V<lambda_f466b26476f0b59760fb8bb0cc43dfaf>@@$0A@@std@@V?$allocator@V?$_Func_class@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@AEBV12@AEBV12@@std@@@2@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@2@AEBV42@AEBV42@@std@@6B@
    mov QWORD PTR f$[rsp], rax
    

    离开static const似乎并没有真正影响执行速度。由于表是静态的,我认为它应该存在,但是就执行速度而言,如果表初始化涉及四个单独的动态分配,或者类似的事情,那么我们可能没有那么大的胜利。

答案 2 :(得分:6)

你可以说

if (theString.size()) { .... }

这是否更具可读性是另一回事。在这里,您调用的方法的主要目的不是告诉您事物是否为空,并依赖于隐式转换为bool。我更喜欢!s.empty()版本。我可能会使用not来取笑:

if (not theString.empty()) { .... }

看到!not版本混淆的人之间的相关性可能会很有趣。

答案 3 :(得分:5)

我必须重构这一点,纯粹是出于肛门保留性疾病......

std::string fmtTime( const std::string & start, const std::string & end ) {
    if ( start.empty() ) {
        if ( end.empty() ) return ""; // should diagnose an error here?

        return "until " + end;
    }

    if ( end.empty() ) return "since " + start;

    return "from " + start + " to " + end;
}

那里......干净整洁。如果此处的内容难以阅读,请添加注释,而不是其他if子句。

答案 4 :(得分:4)

通常最好不要使用这种复杂的条件代码。为什么不保持简单?


const std::string fmtTime(const std::string& start, const std::string& end)
{
    if (start.empty() && end.empty())
    {
        return "";
    }

    // either start or end or both are not empty here.

    std::string time;

    if (start.empty())
    {
        time = "until "+end;
    }
    else if (end.empty())
    {
        time = "since "+start;
    }
    else // both are not empty
    {
        time = "from "+start+" to "+end;
    }

    return time;
}

答案 5 :(得分:3)

不使用否定..;)

const std::string fmtTime(const std::string& start, const std::string& end)
{
   std::string ret;
   if (start.empty() == end.empty())
   {
     ret = (start.empty()) ? "" : "from "+start+" to "+end;
   }
   else
   {
     ret = (start.empty()) ? "until "+end : "since "+start;
   }
   return ret;
}
编辑:好的,清理了一点......

答案 6 :(得分:3)

由于没有人愿意用我的评论输入完整的答案,所以这里是:

创建简化表达式读取的局部变量:

std::string fmtTime(const std::string& start, const std::string& end)
{
    std::string time;
    const bool hasStart = !start.empty();
    const bool hasEnd   = !end.empty();
    if (hasStart || hasEnd) {
        if (hasStart && hasEnd) {
            time = "from "+start+" to "+end;
        } else {
            if (hasStart) {
                time = "since "+start;
            } else {
                time = "until "+end;
            }
        }
    }
    return time;
}

编译器足够智能以消除这些变量,即使它没有,它也不会比原始变量效率低(我希望它们都是变量的单个测试)。对于只能读取条件的人来说,代码现在更具有可读性

如果已经开始或结束,那么

当然你也可以做不同的重构来进一步简化嵌套操作的数量,比如在没有开始或结束的时候单挑出去并提前纾困......

答案 7 :(得分:3)

在全球范围内,我对你的写作方式没有任何问题;它的 其他人的选择当然更清洁 提议。如果你担心!消失(哪个 是一种合理的担心),使用更多的空白区域。

if ( ! start.empty() || ! end.empty() ) ...

或者尝试使用关键字not代替:

if ( not start.empty() || not end.empty() ) ...

(对于大多数编辑者,not将突出显示为关键字, 这会引起更多关注。)

否则,有两个辅助函数:

template <typename Container>
bool
isEmpty( Container const& container )
{
    return container.empty();
}

template <typename Container>
bool
isNotEmpty( Container const& container )
{
    return !container.empty();
}

这具有提供功能的附加优势 一个更好的名字。 (函数名称是动词,所以c.empty() 逻辑上意味着“清空容器”,而不是“是容器 空的“。但是,如果你开始包装所有的功能 名字很差的标准库,你的工作已经减少了 出你的。)

答案 8 :(得分:2)

我也在与负面逻辑的心理开销斗争。

对此的一个解决方案(当无法避免时)是检查显式条件,请考虑:

if (!container.empty())

VS

if (container.empty() == false)

第二个版本更容易阅读,因为它会像你大声朗读一样流动。它还清楚地表明您正在检查错误情况。

现在,如果这对你来说仍然不够好,我的建议是创建一个瘦的包装类,继承自你正在使用的任何容器,然后为该特定检查创建自己的方法。

例如使用字符串:

class MyString : public std::string
{
   public:
     bool NotEmpty(void)
     { 
       return (empty() == false); 
     }
};

现在变成了:

if (container.NotEmpty())...

答案 9 :(得分:2)

如果您只关心!可以忽略的容易程度,则可以使用标准C ++替代令牌not代替:

const std::string fmtTime(const std::string& start, const std::string& end)
{
    std::string time;
    if (not start.empty() or not end.empty()) {
        if (not start.empty() and not end.empty()) {
            time = "from "+start+" to "+end;
        } else {
            if (end.empty()) {
                time = "since "+start;
            } else {
                time = "until "+end;
            }
        }
    }
    return time;
}

(请参阅替代令牌标准中的[lex.digraph]

答案 10 :(得分:2)

您是否会认为已分配正好相反?

#include <string>

template <typename CharType>
bool assigned(const std::basic_string<CharType>& s)
{
    return !s.empty();
}

std::string fmtTimeSpec(const std::string& from, const std::string& to)
{
    if (assigned(from)) {
        if (assigned(to)) {
            return "from "+from+" to "+to;
        }
        return "since "+from;
    }
    if (assigned(to)) {
        return "until "+to;
    }
    return std::string();
}

“测试功能”的结构改进来自众多有用的答案。特别感谢:

答案 11 :(得分:0)

为了表达“.isEmpty()”用法的相反形式,我更喜欢这样:

 if (textView.getText().toString().isEmpty()){

            //do the thing if textView has nothing inside as typed.

 }else if (textView.getText().toString() != ""){

            // do the thing if textView has something inside as typed.
        }

此外,您可以使用“.equals("")”代替 Android Studio 推荐的“!=”字体。

textView.getText().toString().equals("")

答案 12 :(得分:0)

回到API 设计方面

(它可能不适用于字符串,但通常适用于容器类)

纯属偶然,我找到了这个老问题的绝佳答案(强调我的问题)

<块引用>

使用 any() 怎么样? [...]

completely unrelated post 中作为问题的答案

<块引用>

How do I know if a generator is empty from the start?

对比 emptyany 在英语中可能很差,但在 API 设计中绝对有意义。