不使用临时变量交换两个变量的值

121

我想在C#中交换两个变量而不使用临时变量,这可能吗?

decimal startAngle = Convert.ToDecimal(159.9);
decimal stopAngle = Convert.ToDecimal(355.87);

// Swap each:
//   startAngle becomes: 355.87
//   stopAngle becomes: 159.9

1
十进制数 stopAngle = Convert.ToDecimal(159.9); 十进制数 startAngle = Convert.ToDecimal(355.87); - user933161
永远不要将浮点数转换为十进制数。应该是字符串,而不是浮点数,例如Convert.ToDecimal("159.9")。 - JoelFan
1
从技术上讲不行。你只能让你的代码看起来具备这种能力,但实际上,在底层,必须始终存在第三个变量: 原因是所有变量都是类型声明:(类型能够容纳的位数)和值(该类型的实际值) 如果你有一个内存块,其中有类似于4位类型 - > 1000(十进制中的1)(变量A) 另一个内存块有类似于4位类型 -> 0100(十进制中的2)(变量B) 当你想要在B中保存A时,B的值将被A覆盖。你无法保留B的值。 - Morten Bork
1
唯一保留B值的方法是将B存储在其他变量中(无论是临时的还是非临时的),直到先前保存在第三个变量内存块中的B值已被存储在A中。然后,您可以释放第三个值的内存以供其他用途,并回到两个变量,但必须按此方式交换值。然后有人会说:“如果通过引用而不是值来完成呢?”这是同样的问题,对A和B的引用必须保持,同时一个引用指向两个值,否则就无法重新连接。 - Morten Bork
30个回答

292

17
很遗憾这个功能之前不存在,否则他可能会学习到一种有力的新语言结构,为他提供更多解决问题的工具,而不是上面所有无用的抱怨。 - Jeffrey Vest
2
这个程序的性能如何?我总是担心会在堆上内部创建一些“元组”实例,以及所有那些花哨的事情。 - Ray
13
@Ray https://www.reddit.com/r/ProgrammerTIL/comments/8ssiqb/cyou_can_swap_values_of_two_variable_with_the_new/e12301f/ - prime23
3
@TheBeardedQuack,您的链接显示了正确的结果。为什么您说它不能与原始类型一起使用? - AlexWei
9
这段代码适用于 .Net 5.0。当我查看了它的 IL 后,发现它实际上通过引入一个额外的变量来实现交换。 - Raj Rao
显示剩余5条评论

243

在这个问题被提出的时候(1),交换两个变量的正确方法是使用一个临时变量:

decimal tempDecimal = startAngle;
startAngle = stopAngle;
stopAngle = tempDecimal;

就是这样。没有聪明的技巧,没有你的代码维护者在未来几十年里咒骂你,也不会花太多时间试图弄清楚为什么你需要在一个操作中使用它,因为即使是最复杂的语言特性,在最低层次上也是一系列简单的操作。

只需要一个非常简单、易读、易理解的 temp = a; a = b; b = temp; 解决方案。

我认为,那些试图使用技巧来“交换变量而不使用临时变量”或“达夫设备”的开发人员只是想展示他们有多聪明(但却失败了)。

我把他们比作那些只为了在派对上显得更有趣而阅读高深书籍的人(而不是拓展自己的视野)。

加减或基于XOR的解决方案不如一个简单的“临时变量”解决方案可读性好,而且很可能比汇编级别的普通移动更慢。

通过编写高质量易读的代码,为自己和他人提供服务。

这就是我的发泄。谢谢倾听 :-)

顺便说一下,我非常清楚这并没有回答你的具体问题(我为此道歉),但在SO上有很多先例,人们问如何做某事,而正确的答案是“不要这样做”。

(1)自那时起,语言和/或.NET Core的改进采用了“Pythonic”的方式,使用元组。现在您只需执行以下操作:

(startAngle, stopAngle) = (stopAngle, startAngle);

交换值。这几乎不会改变底层操作,但至少有一个小优点,即不必在代码中引入临时命名变量。实际上,您可以看到它仍然使用一个临时变量(通过将值推送/弹出堆栈)在幕后进行,使用 (a, b) = (b, a) 语句:

IL_0005: ldloc.1  ; push b
IL_0006: ldloc.0  ; push a
IL_0007: stloc.2  ; t = pop (a)
IL_0008: stloc.0  ; a = pop (b)
IL_0009: ldloc.2  ; push t
IL_000a: stloc.1  ; b = pop (t)

8
+1; 并且还有更多的原因:使用+/-(等等)技巧进行不必要的算术运算。对于整数,这可能勉强可以接受(四舍五入/溢出不是问题,CPU成本几乎为零),但对于小数,加减法是非平凡的操作。甚至不能使用FPU,因为它们不是浮点/双精度。所以请使用临时变量! - Marc Gravell
7
当然,这是最好的方法,但明确要求不使用临时变量。 - Willem Van Onsem
1
+1 我同意你的观点。当你开始把简单的事情复杂化时,你最终会面临各种问题需要在接下来的几年里解决... - Nelson Reis
2
也许有一个真正的理由不使用临时变量。如果这两个变量非常大,你不想创建一个新的变量,因此即使不是很长的时间内也会有3个非常大的变量。 - koumides
@CommuSoft 临时对象不同于临时变量。无论对错,这个额外的临时对象会降低可读性,而采用Pythonic的方式则非常清晰干净。 - tbone
显示剩余8条评论

139

首先,在C#这样的语言中不使用临时变量进行交换是一个非常糟糕的想法。

但是为了回答问题,您可以使用以下代码:

startAngle = startAngle + stopAngle;
stopAngle = startAngle - stopAngle;
startAngle = startAngle - stopAngle;

如果两个数字相差较大,那么在四舍五入时可能会出现问题。这是由于浮点数的性质所致。

如果您想隐藏临时变量,可以使用一个实用方法:

public static class Foo {

    public static void Swap<T> (ref T lhs, ref T rhs) {
        T temp = lhs;
        lhs = rhs;
        rhs = temp;
    }
}

43
对于整数或定点数来说没有问题。但使用浮点数时,你最终会得到微小的舍入误差。这些误差可能会很大,也可能不够显著,这取决于你如何使用这些数字。 - Kennet Belenky
7
只要不遇到溢出问题,那就可以完全正常地运行。 - patjbs
155
解决这个问题唯一“好”的方法是使用一个临时变量。像这样的“聪明”代码(用“聪明”来形容实际上是“愚蠢”的)比起使用临时变量的解决方案来说,可读性和显而易见性要差得多。如果我看到我的下属写出这种代码,他们将受到惩罚并被要求重新编写。我并不是在针对你,@CommuSoft(因为你回答了这个问题),但是这个问题本身就是垃圾。 - paxdiablo
4
@Janusz Lenar: 在具有指针操作的语言中,您可以使用相同的技巧交换指针。在C#中,您可以在不安全的环境下这样做。 :D 但是无论如何,我承认在没有第三个变量的情况下交换对象等是一个不好的想法(对此作出反应的是__curious_geek)。但问题明确要求在没有额外变量的情况下进行操作。 - Willem Van Onsem
16
使用这种技术比使用临时变量计算代价更高。 - Mike
显示剩余5条评论

76

是的,使用以下代码:

stopAngle = Convert.ToDecimal(159.9);
startAngle = Convert.ToDecimal(355.87);

对于任意值,这个问题更加困难。 :-)


42
int a = 4, b = 6;
a ^= b ^= a ^= b;

适用于所有类型,包括字符串和浮点数。


29
我希望异或交换法有一天会被遗忘。 - helpermethod
11
XOR 交换是极客精神的巅峰之一。在学校学会它后,我有几天的涅槃体验。 - Gabriel Magana
11
似乎这完全不起作用 https://dev59.com/BW035IYBdhLWcg3wMM4G - Andrew Savinykh
8
这是C#。上面的代码不会交换,正如@zespri所说。关于你最后一句话:在C#中,你不能使用^=运算符与stringfloat一起使用,因此它们无法编译。 - Jeppe Stig Nielsen
5
如果我曾经乘坐过美国政府制造的战斗机,或者必须植入起搏器等设备,我真心希望不会因为某个程序员“希望异或交换算法有一天被遗忘”而导致堆栈溢出而死亡。 - Chris Beck
显示剩余5条评论

27

BenAlabaster展示了一种实用的变量切换方法,但不需要try-catch子句。这段代码已经足够。

static void Swap<T>(ref T x, ref T y)
{
     T t = y;
     y = x;
     x = t;
}

使用方法与所示相同:

float startAngle = 159.9F
float stopAngle = 355.87F
Swap(ref startAngle, ref stopAngle);

你也可以使用扩展方法:

static class SwapExtension
{
    public static T Swap<T>(this T x, ref T y)
    {
        T t = y;
        y = x;
        return t;
    }
}

像这样使用:

float startAngle = 159.9F;
float stopAngle = 355.87F;
startAngle = startAngle.Swap(ref stopAngle);

这两种方法都在函数中使用了一个临时变量,但是在进行交换的地方是不需要这个临时变量的。


2
是的,但只能在方法中使用,而不能在执行switch语句的地方使用。 - Marcus
6
使用抽象化是解决问题的好方法。它提供了通用的解决方案,使调用代码更易于阅读。当然,它会使用一些额外的内存和处理器周期,但除非您需要调用此代码数百万次,否则您不会注意到任何差异。 - Olivier Jacot-Descombes
1
@OlivierJacot-Descombes,我希望如果你调用它一百万次,JIT会对其进行优化。 - Sebastian
3
所以问题是,为什么这个便捷的功能没有被包括在.NET库中?它是通用方法文档中给出的第一个示例。 - Mark Ransom

18

一个带有详细示例的二进制异或交换:

XOR 真值表

a b a^b
0 0  0
0 1  1
1 0  1
1 1  0

输入:

a = 4;
b = 6;

Step 1: a = a ^ b

a  : 0100
b  : 0110
a^b: 0010 = 2 = a

Step 2: b = a ^ b

a  : 0010
b  : 0110
a^b: 0100 = 4 = b

Step 3: a = a ^ b

a  : 0010
b  : 0100
a^b: 0110 = 6 = a

输出:

a = 6;
b = 4;

16

在C# 7中:

(startAngle, stopAngle) = (stopAngle, startAngle);

15

在 C# 中不行。在本地代码中,你可能能够使用三重异或交换技巧,但在高级类型安全语言中不行。(无论如何,我听说在许多常见的 CPU 架构中,实际上使用异或技巧会比使用临时变量更慢。)

你应该只是使用一个临时变量。没有理由你不能使用它;这并不像有限制供应一样。


大多数XOR操作适用的类型都可以放在寄存器中,因此编译器不应为其分配堆栈空间。 - BCS
真的,但情况比那更为复杂。在汇编语言级别上,你很少需要交换值。交换通常可以作为其他算术运算的副作用来完成。大多数情况下,交换只是为了在高级语言中表达事物而已。编译后,交换就不存在了,因此根本不需要时间成本 :-) - Nils Pipenbrinck
如果内存供应不足,例如在嵌入式设备上,则临时变量有时会短缺。 - AsherMaximum
@AsherMaximum:但如果C#提供了一种交换两个变量的方法,那就更好了。这样可以实现有或没有临时变量。通过显式地实现它,代码变得难以阅读和维护。 - Willem Van Onsem

14

为了未来的学习者和人类,我提供此更正意见以修正当前所选答案。

如果你想避免使用临时变量,有只有两个明智的选择,分别考虑了性能和可读性。

  • 在通用的Swap方法中使用临时变量。(与内联临时变量相比,性能最佳)
  • 使用Interlocked.Exchange。(在我的机器上慢5.9倍,但这是您唯一的选择,如果多个线程将同时交换这些变量。)

永远不应该做的事情:

  • 永远不要使用浮点数算术。(速度慢,四舍五入和溢出错误,难以理解)
  • 永远不要使用非原始算术。(速度慢,溢出错误,难以理解)Decimal 不是 CPU 原语,并且会产生比您意识到的更多的代码。
  • 永远不要使用算术运算符。或者位运算技巧。 (速度慢,难以理解)这是编译器的工作。它可以优化多种不同的平台。

因为每个人都喜欢硬性数字,这里有一个比较选项的程序。在 Visual Studio 外部的发布模式下运行,以使得 Swap 能够内联。我的机器上 (Windows 7 64-bit i5-3470),结果如下:

Inline:      00:00:00.7351931
Call:        00:00:00.7483503
Interlocked: 00:00:04.4076651

代码:

class Program
{
    static void Swap<T>(ref T obj1, ref T obj2)
    {
        var temp = obj1;
        obj1 = obj2;
        obj2 = temp;
    }

    static void Main(string[] args)
    {
        var a = new object();
        var b = new object();

        var s = new Stopwatch();

        Swap(ref a, ref b); // JIT the swap method outside the stopwatch

        s.Restart();
        for (var i = 0; i < 500000000; i++)
        {
            var temp = a;
            a = b;
            b = temp;
        }
        s.Stop();
        Console.WriteLine("Inline temp: " + s.Elapsed);


        s.Restart();
        for (var i = 0; i < 500000000; i++)
        {
            Swap(ref a, ref b);
        }
        s.Stop();
        Console.WriteLine("Call:        " + s.Elapsed);

        s.Restart();
        for (var i = 0; i < 500000000; i++)
        {
            b = Interlocked.Exchange(ref a, b);
        }
        s.Stop();
        Console.WriteLine("Interlocked: " + s.Elapsed);

        Console.ReadKey();
    }
}

1
位操作实际上并不慢。例如,为了将 0 加载到寄存器中,编译器会使用 异或 操作将变量与自身进行运算,因为这条指令更短。在某些罕见的情况下,通过使用较少的变量,可以避免寄存器溢出。这是编译器的工作,但不幸的是,目前没有语言支持以高效的方式执行此操作。 - Willem Van Onsem
你说得对。在这种需要速度的热点情况下,你需要选择不受管理的方式。位操作在托管语言中经常会适得其反。这实际上是Jitter的责任。 - jnm2

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