有哪种编程语言拥有一元布尔切换运算符?

143

所以这更像是一个理论性问题。C++和(间接)基于它的语言(如Java、C#和PHP)都有用于将大多数二元运算符的结果分配给第一个操作数的快捷方式运算符

a += 3;   // for a = a + 3
a *= 3;   // for a = a * 3;
a <<= 3;  // for a = a << 3;

但是当我想要切换一个布尔表达式时,我总是发现自己写的类似于:

a = !a;

a 是一个长表达式时,这很令人烦恼。

this.dataSource.trackedObject.currentValue.booleanFlag =
    !this.dataSource.trackedObject.currentValue.booleanFlag;

(是的,我知道迪米特定律)。

所以我在想,有没有一种语言有一元布尔开关运算符,可以让我缩写a = !a而不需要重复表达式a,例如:

!=a;  
// or
a!!;
假设我们的语言有适当的布尔类型(例如 C++ 中的 bool 类型),并且 a 是该类型(因此不是 C 风格的 int a = TRUE)。
如果您能找到有文献来源的话,我也很想了解例如 C++ 设计者在布尔类型成为内置类型时是否考虑添加这样的运算符,以及他们为什么决定不这样做。
(注:我知道有些人认为赋值不应使用 = ,并且 ++ 和 += 不是有用的运算符而是设计缺陷;让我们假设我对它们感到满意,并专注于为什么它们不能扩展到布尔类型)。

31
这个函数 void Flip(bool& Flag) { Flag=!Flag; } 可以缩短你的长表达式。 - harper
72
this.dataSource.trackedObject.currentValue.booleanFlag ^= 1; - KamilCuk
13
可以将长表达式分配给引用变量,以简化代码。 - Quentin 2
6
这可能有效,但您混淆了类型。您将整数分配给了布尔值。 - harper
7
@user463035818提到了^=true,而我个人更喜欢用*= -1,因为我觉得它更容易理解。 - CompuChip
显示剩余16条评论
12个回答

145

切换布尔位

…这将允许我缩写a = !a而不重复表达式的a

这种方法并不是真正的“变异翻转”运算符,但确实满足您上述的条件;表达式的右边不涉及变量本身。

任何具有布尔XOR赋值(例如^=)的语言都可以通过对true进行XOR赋值来切换变量a的当前值:

// type of a is bool
a ^= true;  // if a was false, it is now true,
            // if a was true, it is now false

正如下面评论中@cmaster所指出的那样,上述假设abool类型,而不是整数或指针。如果a实际上是其他类型(例如某些非bool类型但计算结果为“真”或“假”的值,并且其位表示不是0b10b0),则上述内容不成立。
以具体例子来说,Java是一种定义明确且不受任何隐式转换影响的语言。引用下面@Boann的评论:
在Java中,对于布尔值和整数,^^=有明确定义的行为(参见15.22.2.布尔逻辑运算符&^|),其中操作符的两侧必须都是布尔值或都是整数。这些类型之间没有隐式转换。因此,如果将a声明为整数,则它不会在静默失效,而是会产生编译错误。因此,在Java中,a ^= true;是安全且定义明确的。

Swift: toggle()

自Swift 4.2起,以下演进提案已被接受并实施:

这在Swift中为Bool类型添加了一个本地的toggle()函数。

toggle()

Toggles the Boolean variable’s value.

Declaration

mutating func toggle()

Discussion

Use this method to toggle a Boolean value from true to false or from false to true.

var bools = [true, false]

bools[0].toggle() // bools == [false, false]

这不是一个运算符,而是允许布尔值切换的本地语言方法。


3
评论不适合进行扩展讨论;此对话已被移至聊天室,请前往那里继续讨论。 - Samuel Liew

44
在C++中,可以犯下重新定义运算符含义的基本错误。有了这个想法和一点ADL,我们只需要做的就是这样,就可以对我们的用户群造成混乱:
#include <iostream>

namespace notstd
{
    // define a flag type
    struct invert_flag {    };

    // make it available in all translation units at zero cost
    static constexpr auto invert = invert_flag{};

    // for any T, (T << invert) ~= (T = !T)    
    template<class T>
    constexpr T& operator<<(T& x, invert_flag)
    {
        x = !x;
        return x;
    }
}

int main()
{
    // unleash Hell
    using notstd::invert;

    int a = 6;
    std::cout << a << std::endl;

    // let confusion reign amongst our hapless maintainers    
    a << invert;
    std::cout << a << std::endl;

    a << invert;
    std::cout << a << std::endl;

    auto b = false;
    std::cout << b << std::endl;

    b << invert;
    std::cout << b << std::endl;
}

预期输出:

6
0
1
0
1

10
“重新定义运算符的大罪”——我明白你在夸张,但通常来说,对于已有的运算符重新赋予新的意义并不是一个“大罪”。 - Konrad Rudolph
6
当我说一个操作符应该根据它通常的算术或逻辑含义具有逻辑意义时,我的意思是显然的。对于两个字符串来说,'operator+' 是合理的,因为连接在我们看来类似于加法或累加。'operator<<' 现在的意思是“流出”,因为它最初在STL中被滥用。它自然不会意味着“在RHS上应用变换函数”,这基本上是我在这里重新定位它的目的。 - Richard Hodges
7
抱歉,我不认为那是一个好的论点。首先,字符串拼接完全与加法有不同的语义(甚至不可交换!)。其次,根据你的逻辑,<<是位移操作符,而不是“流输出”操作符。你重新定义的问题不在于它与运算符的现有含义不一致,而在于它并没有比简单的函数调用更具价值。 - Konrad Rudolph
8
看起来是一个糟糕的重载。我会写成!invert= x; ;) - Yakk - Adam Nevraumont
13
@Yakk-AdamNevraumont 哈哈,现在你让我开始了:https://godbolt.org/z/KIkesp - Richard Hodges
显示剩余7条评论

37

只要我们包含汇编语言...

FORTH

INVERT用于按位求补。

0=用于逻辑(真/假)求补。


4
在每个基于堆栈的编程语言中,一元的 not 操作符可以被看作是一个“切换”操作符 :-) - Bergi
2
当然,这是一个有些侧面的答案,因为OP显然在考虑具有传统赋值语句的类C语言。我认为像这样的侧面回答很有价值,即使只是展示读者可能没有想到的编程世界的一部分。(“天堂和地球上的编程语言比我们的哲学中所梦到的还要多。”) - Wayne Conrad

31

在C99中,对bool进行递减操作会产生预期的效果,对一些微型控制器方言支持的bit类型进行递增或递减操作也会产生预期的效果(据我观察,这些方言将位视为单个位宽的位域,因此所有偶数被截断为0,而所有奇数被截断为1)。虽然我不是bool类型语义的铁杆粉丝 [在我看来,该类型应该指定一个存储任何值为0或1之外的bool可能在读取时表现为不确定(不一定一致)的整数值;如果程序尝试存储一个不知道是否为0或1的整数值,应该首先将其用!!处理],但我不会特别推荐这种用法。


2
C++在C++17之前也是用bool来实现同样的功能的。参考链接 - Davis Herring

31

2
我认为这不完全是op所想的,假设这是x86汇编。例如 https://en.wikibooks.org/wiki/X86_Assembly/Logic ; here edx would be 0xFFFFFFFE because a bitwise NOT 0x00000001 = 0xFFFFFFFE - BurnsBA
8
可以使用所有位:NOT 0x00000000 = 0xFFFFFFFF。 - Adrian
5
嗯,是的,虽然我试图区分布尔运算符和位运算符。正如问题中所说,假设语言具有明确定义的布尔类型:“(因此没有C风格的int a = TRUE)”。 - BurnsBA
5
@BurnsBA: 的确,汇编语言没有一套明确定义的真/假值。在 code-golf 上,我们已经讨论了在各种语言中为布尔问题返回哪些值的问题。由于汇编语言没有 if(x) 结构,在布尔问题中允许使用 0/-1 作为假/真是完全合理的,比如对于 SIMD(单指令多数据流)比较(http://felixcloutier.com/x86/PCMPEQB:PCMPEQW:PCMPEQD.html)。但你说得对,如果你在任何其他值上使用它,这种方法就会出错。 - Peter Cordes
4
由于PCMPEQW的结果为0/-1,而SETcc的结果为0/+1,因此很难获得一种单一的布尔表示。 - Eugene Styer
许多架构没有NOT指令。例如,MIPS使用NOR代替。其他一些架构使用不同的名称,如COM(补码)、BIC(位补码)... - phuclv

28

我假设你不会仅仅根据这个来选择一种编程语言 :-) 无论如何,你可以使用类似于以下代码的C++来完成:

inline void makenot(bool &b) { b = !b; }

以下是一个完整的示例程序:

#include <iostream>

inline void makenot(bool &b) { b = !b; }

inline void outBool(bool b) { std::cout << (b ? "true" : "false") << '\n'; }

int main() {
    bool this_dataSource_trackedObject_currentValue_booleanFlag = false;
    outBool(this_dataSource_trackedObject_currentValue_booleanFlag);

    makenot(this_dataSource_trackedObject_currentValue_booleanFlag);
    outBool(this_dataSource_trackedObject_currentValue_booleanFlag);

    makenot(this_dataSource_trackedObject_currentValue_booleanFlag);
    outBool(this_dataSource_trackedObject_currentValue_booleanFlag);
}

这将按预期输出:

false
true
false

2
你想要同时返回 b = !b 吗?读到 foo = makenot(x) || y 或者简单的 foo = makenot(bar) 的人可能会认为 makenot 是一个纯函数,假设没有副作用。在一个更大的表达式中隐藏副作用可能是不好的风格,非 void 返回类型的唯一好处就是使这种情况成为可能。 - Peter Cordes
2
@MatteoItalia建议 使用template<typename T> T& invert(T& t) { t = !t; return t; },它是模板化的,如果用于非布尔对象,则会转换为/从bool。我不知道这是好是坏。大概是好的。 - Peter Cordes

22

21

通过扩展方法,Visual Basic.Net 可以支持此功能。

定义扩展方法如下:

<Extension>
Public Sub Flip(ByRef someBool As Boolean)
    someBool = Not someBool
End Sub

然后像这样调用它:

Dim someVariable As Boolean
someVariable = True
someVariable.Flip

那么,您的原始示例将类似于:

me.DataSource.TrackedObject.CurrentValue.BooleanFlag.Flip

2
虽然这确实可以作为作者所寻求的简写方式,但是你必须使用两个运算符来实现Flip()=Not。有趣的是,在调用SomeInstance.SomeBoolean.Flip的情况下,即使SomeBoolean是一个属性,它也能正常工作,而等效的C#代码则无法编译。 - Lance U. Matthews

15

就从纯理论角度来看,这个问题确实很有趣。暂时不考虑一元、变异的布尔切换运算符是否有用,或者为什么许多语言选择不提供它,我探索了一下是否存在这样的运算符。

简而言之,似乎没有,但是Swift可以让您实现一个。如果您只想看看如何实现,请滚动到本答案底部。


经过(快速)搜索各种语言的特性后,我敢说没有一种语言将其实现为严格的原地变异操作(如果您找到了请纠正我)。 因此下一步就是看看是否有让您构建一个的语言。 这将需要两件事:

  1. 能够使用函数实现(一元)运算符
  2. 允许这些函数具有传递引用参数的功能(以便直接改变它们的参数)

许多语言将因为不支持这些要求而立即被淘汰。例如Java不允许操作符重载(或自定义操作符),此外,所有基本类型都是按值传递的。 Go根本不支持操作符重载(除非通过hack)。Rust仅允许自定义类型的运算符重载。在Scala中,您可以使用非常有创意的命名函数并省略括号,但遗憾的是没有传递引用参数的功能。弗特兰很接近,因为它允许自定义运算符,但明确禁止它们具有inout参数(这在普通函数和子例程中是允许的)。


然而至少有一种语言符合所有必要条件: Swift。虽然一些人已经链接到即将推出的.toggle()成员函数,但您也可以编写自己的操作符,这确实支持inout参数。看:

prefix operator ^

prefix func ^ (b: inout Bool) {
    b = !b
}

var foo = true
print(foo)
// true

^foo

print(foo)
// false

6
这是翻译的文本:该语言将提供此功能:https://github.com/apple/swift-evolution/blob/master/proposals/0199-bool-toggle.md - Cristik
3
好的,但该问题特别要求一个“操作符”,而不是成员函数。 - Lauri Piispanen
4
"Neither does Rust" => Yes it does “Rust也不行” => 是可以的 - Boiethios
3
@Boiethios 谢谢!为 Rust 进行了编辑 - 只允许对自定义类型进行重载运算符,所以不适用。 - Lauri Piispanen
3
规则比那个更加普遍,并且由于孤儿规则,详情请参考链接。 - Boiethios

14
在Rust中,您可以创建自己的trait来扩展实现Not trait的类型:
use std::ops::Not;
use std::mem::replace;

trait Flip {
    fn flip(&mut self);
}

impl<T> Flip for T
where
    T: Not<Output = T> + Default,
{
    fn flip(&mut self) {
        *self = replace(self, Default::default()).not();
    }
}

#[test]
fn it_works() {
    let mut b = true;
    b.flip();

    assert_eq!(b, false);
}

您还可以像建议的那样使用 ^= true,在 Rust 中这样做不会出现任何问题,因为 false 不像在 C 或 C++ 中一样是一个“伪装”的整数:

fn main() {
    let mut b = true;
    b ^= true;
    assert_eq!(b, false);

    let mut b = false;
    b ^= true;
    assert_eq!(b, true);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接