简化具有重复结果的嵌套 if/else?

Simplify nested if/else with repeated results?

我正在尝试简化以下内容:

function handleDirection(src) {
  if (src === 'left') {
    if (inverse) {
      tracker--;
    } else {
      tracker++;
    }
  } else {
    if (inverse) {
      tracker++;
    } else {
      tracker--;
    }
  }
}

减少条件语句的数量。 src 将始终是 'left''right'

您可以与第一次检查的结果进行核对。

这是异或检查。

// typeof inverse === 'boolean'

function handleDirection(src) {
    if (src === 'left' === inverse) {
        tracker--;
    } else {
        tracker++;
    }
}

检查按此顺序计算表达式 (src === 'left') === inverse:

src === 'left' === inverse
---- first ---             returns a boolean value
--------- second --------- take result of former check & compairs it with another boolean

这可以简化为 returns 1-1 的三元表达式,具体取决于状态。然后您可以将其添加到 tracker.

function handleDirection(src) {
  var delta = (src === 'left' && inverse) || (src !== 'left' && !inverse) ? -1 : 1;
  tracker += delta;
}

这可以使用@NinaScholz 在她的回答中指出的逻辑进一步简化:

function handleDirection(src) {
  var delta = (src === 'left') === inverse ? -1 : 1;
  tracker += delta;
}
function handleDirection(src) {
   var movement = 1;
   if(src === 'left')
     movement = -1;

   if(inverse)
     tracker += movement;
   else
     tracker -= movement;
}

现在你正在比较字符串,我不建议这样做。例如,如果您使用 'Left' 而不是 'left',它将使第一个 if 语句失败。也许布尔值可以在这里使用,因为您可以保证它只有两种状态。

里面的if语句可以通过conditional operators压缩。

也许您正在寻找这样的东西:

function handleDirection(src) {
  if (src) {
    inverse ? tracker-- : tracker++;
  } else {
    inverse ? tracker++ : tracker--;
  }
}

参见:https://jsfiddle.net/9zr4f3nv/

您可以使用短路语法或三元运算符

// by using short circuiting
    function handleDirection(src) {
       if (src == 'left') tracker = inverse && tracker-1 || tracker +1
       else  tracker = inverse && tracker+1 || tracker -1
    }
// by using ternary operator
 function handleDirection(src) {
       if (src == 'left') tracker = inverse ? tracker-1 : tracker +1
       else  tracker = inverse ? tracker+1 : tracker -1
    }

假设inverse是你设置一次的标志,那么你不需要每次都考虑它,你可以计算它的影响一次并按原样使用它,这将减少您的代码分支和逻辑。如果您想在进行过程中更改它,那么您可能需要分离计算逻辑,以便重新使用它。

然后您也可以将移动方向提取到一个独立的函数中,您的 handleDirection 变得非常简单 - 您可以根据 srcinvert.

let tracker = 0;

//extract logic for the movement offset based on direction
function getDirectionOffset(src) {
  return src === 'left' ? 1 : -1;
}

//have a setter for the invert property
function setInverse(isInverse) {
  movementModifier = isInverse ? -1 : 1
}

//declare the variable dependent on the inverse property
let movementModifier;

//initialise movementModifier variable
setInverse(false);

function handleDirection(src) {
  const offset = getDirectionOffset(src) * movementModifier;
  
  tracker += offset;
}


// usage
setInverse(true);

handleDirection("left");
handleDirection("left");
handleDirection("right");

console.log(tracker);

话虽如此,所有这些都表明您不应该使用某个函数,或者您应该以不同的方式使用它。您可以在 class 中收集所有这些功能,或者让所有信息围绕函数传递,因此您没有全局变量。这是该概念的面向对象实现示例:

class TrackerMover {
  constructor(inverse) {
    this.tracker = 0;
    this.movementModifier = inverse ? 1 : -1
  }
  
  handleDirection(src) {
   const offset = this.getDirectionOffset(src) * this.movementModifier;

    this.tracker += offset;
  }
  
  getDirectionOffset(src) {
    return src === 'left' ? -1 : 1;
  }
  
  getPosition() {
    return this.tracker;
  }
}


//usage
const mover = new TrackerMover(true);

mover.handleDirection("left");
mover.handleDirection("left");
mover.handleDirection("right");

console.log(mover.getPosition())

顺便说一下,另一种选择是不要每次都计算移动。您实际上知道每次发生的事情 - 实际上,您有一个事实 table,其中您的输入是 src === leftinverse,而输出是您修改跟踪的方式。

+--------+------------+--------+
| isLeft | isInverted | Offset |
+--------+------------+--------+
| true   | true       |     -1 |
| true   | false      |      1 |
| false  | true       |      1 |
| false  | false      |     -1 |
+--------+------------+--------+

因此,您可以将 table 放入。

let tracker = 0;
let invert = false;

const movementLookupTable = {
  "true": { },
  "false": { },
}

//it can be initialised as part of the above expression but this is more readable
movementLookupTable[true ][true ] = -1;
movementLookupTable[true ][false] = 1;
movementLookupTable[false][true ] = 1;
movementLookupTable[false][false] = -1;

function handleDirection(src) {
  const offset = movementLookupTable[src === "left"][invert];

  tracker += offset;
}


// usage
invert = true;

handleDirection("left");
handleDirection("left");
handleDirection("right");

console.log(tracker);

在这种情况下,这可能有点矫枉过正,但如果有更多标志(包括更多 标志)and/or 结束状态,这种方法可能会有用。例如,您可能想引入四个方向,但如果 updown,则您不修改 tracker 值。

+-----------+------------+--------+
| direction | isInverted | Offset |
+-----------+------------+--------+
| left      | true       |     -1 |
| left      | false      |      1 |
| right     | true       |      1 |
| right     | false      |     -1 |
| up        | false      |      0 |
| up        | true       |      0 |
| down      | false      |      0 |
| down      | true       |      0 |
+-----------+------------+--------+

如您所见,现在不只是布尔值,您可以处理任何值。使用 table,然后您还可以将 invert 更改为 windDirection,因此如果移动是 leftwindDirectionright ,结果就像现在这样,但是你可以有 left 的方向和 left 的风向,所以你可以 进一步移动 。或者你可以移动 up 并且风向是 left 所以 tracker (此时 X 坐标)实际上会被修改。

+-----------+---------------+---------+
| direction | windDirection | OffsetX |
+-----------+---------------+---------+
| left      | right         |      -1 |
| left      | up            |       1 |
| left      | down          |       1 |
| left      | left          |       2 |
| right     | up            |      -1 |
| right     | down          |      -1 |
| right     | right         |      -2 |
| right     | left          |       1 |
| up        | up            |       0 |
| up        | down          |       0 |
| up        | left          |       1 |
| up        | right         |      -1 |
| down      | up            |       0 |
| down      | down          |       0 |
| down      | left          |       1 |
| down      | right         |      -1 |
+-----------+---------------+---------+

考虑到四个方向和四个风向的逻辑在以后的阅读和维护中可能会很烦人,而如果你只有一个查找table,这很容易,你可以很容易地将其扩展到甚至可以处理对角线(假设它们将值更改为 0.5 而不是 1)并且只要您只是从 table 中获取值,您的算法就不会真正关心。

你甚至可以只用一行代码就可以做到:

function getDirectionOffset(src) {
  tracker += (src === 'left' ? 1 : -1) * (inverse ? -1 : 1);
}

这个只有一个条件,我觉得它比其他答案读起来更直观:

function handleDirection(src) {
    if (
        ((src === 'left') && !inverse) ||
        ((src === 'right') && inverse)
    ) {
        tracker++;
    }
    else {
        tracker--;
    }
}

如果 src == leftinverse 中的一个为真但另一个不为真,你想增加跟踪器,否则减少它,这就是 "XOR" ^ 运营商确实 :

function handleDirection(src) {
    if (src === 'left' ^ inverse) {
        tracker++;
    } else {
        tracker--;
    }
}

您可以使用三元表达式进一步减少它:

function handleDirection(src) {
    tracker += src === 'left' ^ inverse ? 1 : -1;
}

或者,如果您想避免任何类型的条件,使用隐式转换和 "clever" 算术:

function handleDirection(src) {
    tracker += 1 - 2 * (src === 'right' ^ inverse); // either 1-0=1 or 1-2=-1
}

我不喜欢 else,尽可能避免嵌套。我认为这以更自然的方式传达了 inverse 的想法:

function handleDirection(src) 
{
    let change = 1;

    if ('right' == src)
        change = -1;

    if (inverse)
        change = -change;

    tracker += change;
}

您可以使用 js 中的二维数组类型数据结构,并将所需的结果存储在索引 sec 和 inverse 中。或者 JSON.

您根本不需要任何 if 句子。可以执行相同的操作 根据 srcinverse 计算正增量或负增量 仅需三元运算符的帮助。

function handleDirection(src) {
    tracker += (src == "left" ? 1 : -1) * (inverse ? -1 : 1);
};

顺便说一句。为了效率,我建议直接使用numeric 递增/递减而不是需要额外处理的字符串 解码。您可以使用常量来实现相同的可读性:

另外inverse可以优化为数值在1(不是 倒置)和 -1(倒置)。

const left = 1;
const right = -1;
var direction = 1;

function handleDirection(src) {
    tracker += src * direction;
}

function reverse() { // (Example)
    direction = direction * -1;
}

...即使 "right" 和 "left" 关键字来自某种文本用户 输入,您可以简单地从字典中翻译它们:

const steps = {
    left = 1;
    right = -1;
};

function handleDirection(src) {
    tracker += steps[src] * direction;
}

我知道这不是直接解决问题 "simplification" 但我想为您提供一个答案,解决几个代码质量问题,同时使代码更具可读性。

关于副作用

首先,这个给定的函数改变了外部值。这就引入了side effects的问题:

  • 该函数正在改变可能无法在外部环境中处理的外部状态,因此可能导致未定义的行为。
  • 然后函数本身会绑定到外部状态,因此很难更改和重构代码。

测试这样的函数也更难,因为您必须先 "create the state environment" 才能 运行 测试。

第一个简单的调整是通过参数接受所有外部值,并且只是 return 分配给任何东西的 1-1 值(在你的情况下tracker).

带字符串的异或条件

其次,在具有排他性的字符串值上使用 if/else 或会导致未定义状态,其中 src 可能不是 'right',但函数的行为就像它会是 'right'。相反,它应该抛出异常。在这里使用开关是一个很好的帮助。

将这些点应用于函数

如果考虑到以上几点,整体功能将如下所示:

function handleDirection (src, inverse) {
  switch (src) {
    case 'left':
      return inverse ? -1 :  1
    case 'right':
      return inverse ?  1 : -1
    default:
      throw new Error(`Unknown src: ${src}`)
  }
}

您可以轻松测试此功能:

handleDirection('left' , true)  // -1
handleDirection('left' , false) //  1
handleDirection('right', true)  //  1
handleDirection('right', false) // -1
handleDirection('middle',true)  // Error: Unknown src: middle

现在函数明显与 tracker 分离(想一想重构时的宝贵时间),而且函数的作用完全清楚。

备注

总结我想强调的是,这并不总是关于用最少的行编写最简单的代码,而是清晰易读/理解和维护的代码。它不像许多提供的解决方案那么简短,但每个人都应该立即理解它的作用。

VLAZ 的回答实际上是一种非常优雅的重构布尔逻辑的方法。从他给出的第一个 table 可以看出,逻辑显然遵循 XOR 的模式,这使得选择显而易见。但是,我发现它更有用,特别是对于更复杂的多变量逻辑,获取您的条件并绘制出逻辑的 Karnaugh Map ,就像介绍 CS 的好日子一样。在 OP 的示例中,K-map 非常简单:

A = src === 'left'
B = inverse
DEC = tracker--
INC = tracker++

       A=0   A=1
     +-----+-----+
 B=0 | DEC | INC |
     +-----+-----+
 B=1 | INC | DEC |
     +-----+-----+ 

然后对最小项求和:

INC = A!B + !AB
DEC = !A!B + AB

那些记住布尔逻辑基础的人会认识到 INC 是值的 XOR,而 DEC 是相反的,即 XNOR。这使我们可以将函数归结为以下内容:

    if (src == 'left' != inverse) {
        tracker++;
    } else {
        tracker--;
    }

逻辑A != B本质上是异或函数,在JavaScript中不存在。注意 Nina 上面的答案与此相反,逻辑上 A == B,这是 XNOR 函数,这就是 INCDEC 被交换的原因。