在代码中高效表达 2x2 逻辑网格

发布于 2024-09-16 10:43:23 字数 1506 浏览 4 评论 0原文

在事件处理程序中,我响应值的更改。我可以访问旧值和新值,并希望根据变化进行某些操作。

每个不同的结果都会执行一些操作/功能 X、Y 或 Z 的组合。Z 接受 -1 和 1 之间的参数。执行这些操作的顺序并不重要。

看下面的逻辑网格。旧值是标签的最左边一列,新值是标签的顶行:

          New:
          0          !=0
          --------   -------
Old:  0 | nothing    Y, Z(1)
    !=0 | X, Z(-1)   X, Y    -- Z(0) is okay but not required for this quadrant

表示这一点的好方法是什么?

我正在使用 C# 工作,但会接受任何语言的答案,因为这并不是真正的语言问题 — 我可以翻译任何语言。

示例:

if (oldvalue == 0 && newvalue == 0) return;
if (oldvalue != 0) X();
if (newvalue != 0) Y();
Z(oldvalue != 0 ? -1 : 0 + newvalue != 0 ? 1 : 0);

我认为这看起来不错,但还有其他方法可以完成。

int which = (oldvalue == 0 ? 0 : 1) + (newvalue == 0 ? 0 : 2)

switch (which) {
   case 1:
      X(); Z(-1);
      break;
   case 2:
      Y(); Z(1);
      break;
   case 3:
      X(); Y();
      break;
}

这实际上比我正在处理的情况稍微简单一些。如果旧值和新值非零且彼此相等,则将新值视为 0。

请随意按给定的方式或使用此附加约束来回答。还有一点,但我认为一开始就太多了。如果之后事情看起来很有趣,我将在此处或在新问题中介绍其余内容。

我想我问这个问题是因为我经常遇到这些逻辑网格的东西,而且它们并不总是 2x2,有时它们会更大一些。很高兴注意到我可以用整个“条纹”处理一些响应,例如注意到每次 oldvalue != 0 时都会完成 X,但似乎我开始遇到一种模式,需要一些表达逻辑来处理它更普遍,而不是费力地把它变成 if then else 语句。我的意思是,如果我能够提供一种逻辑网格并让编译器找出处理它的最佳方法,那就太酷了。

只是进行一些疯狂的头脑风暴:

Conditions:
oldvalue == 0 ? 0 : 1
newvalue == 0 ? 0 : 2

Actions:
X = {false, true, false, true}
Y = {false, false, true, true}
Z(-1) = true where condition = 1
Z(1) = true where condition = 2

你有什么想法?我会奖励任何物质上的参与。

In an event handler I'm responding to the change of a value. I have access to the old value and the new value and want to do certain things depending on what the change is.

Each different outcome will do some combination of actions/functions X, Y, or Z. Z accepts a parameter between -1 and 1. Order of performing these is not important.

Look at the following logic grid. The old value is the leftmost column of labels, and the new value is the top row of labels:

          New:
          0          !=0
          --------   -------
Old:  0 | nothing    Y, Z(1)
    !=0 | X, Z(-1)   X, Y    -- Z(0) is okay but not required for this quadrant

What would be a good way to represent this?

I'm working in C# but will accept answers in any language since it's not really a language question—I can translate whatever.

Example:

if (oldvalue == 0 && newvalue == 0) return;
if (oldvalue != 0) X();
if (newvalue != 0) Y();
Z(oldvalue != 0 ? -1 : 0 + newvalue != 0 ? 1 : 0);

I suppose that looks pretty good, but there are other ways it could be done.

int which = (oldvalue == 0 ? 0 : 1) + (newvalue == 0 ? 0 : 2)

switch (which) {
   case 1:
      X(); Z(-1);
      break;
   case 2:
      Y(); Z(1);
      break;
   case 3:
      X(); Y();
      break;
}

This is actually a slightly simpler case than what I'm dealing with. In the case that oldvalue and newvalue are nonzero and equal to each other, treat newvalue as if it was 0.

Feel free to answer as given or with this additional constraint. There's still a little bit more but I think it's too much to start off with. If things seem interesting afterward, I'll present the rest either here or in a new question.

I guess I'm asking the question because I end up with these logic grid things often, and they aren't always 2x2, sometimes they're a bit bigger. It's nice to notice that I can handle some responses with entire "stripes" like noticing that X is done every time the oldvalue != 0, but it seems like I'm starting to run into a pattern that begs for some expressive logic to handle it more generally instead of laboriously turning it into if then else statements. I mean, it would be really cool if I could provide a sort of grid of logic and let the compiler figure out the best way to handle it.

Just doing some wild brainstorming:

Conditions:
oldvalue == 0 ? 0 : 1
newvalue == 0 ? 0 : 2

Actions:
X = {false, true, false, true}
Y = {false, false, true, true}
Z(-1) = true where condition = 1
Z(1) = true where condition = 2

What are your ideas? I'll reward any material involvement at all.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

微暖i 2024-09-23 10:43:23

让我们从另一个角度来看您的问题。在设计代码体时,我尝试应用以下原则:

  1. 使其正确。
  2. 说清楚。
  3. 使其简洁。
  4. 让它快点。
    ...按这个顺序。

所有这些,在某种程度上,都是主观的。然而,理性的人往往会找到共同点,而且对于他们的对立面往往也能达成更多共识。但除此之外...

这里的首要任务是确保您的代码能够正常工作。显然,有多种实现可以实现这一目标 - 但我还要补充一点重要的是它很容易证明实现是正确的。实现此目的的一种方法是使代码读起来更像规范(稍后会详细介绍)。

第二个优先事项是确保将来当开发人员(包括原作者!)查看此代码时,他们将能够立即理解它在做什么。实现越复杂(读作:花哨),开发人员就越难立即理解代码的作用。

第三个优先事项 - 简短、简洁的代码,与前两个优先事项部分相反。希望使代码更简洁,可能会导致您使用比实际需要更复杂的结构。解决问题。虽然保持代码简短很重要,但我们不应该通过使其变得难以理解的密集来做到这一点。

最后一个优先事项 - 性能 - 仅在重要时才重要。 我的意思是,从性能的角度来看,您不应该使实现复杂化,除非您已执行分析并将其识别为瓶颈在你的系统中。

既然我们已经了解了推动我们决策的原则,那么让我们将它们应用于当前的问题。您已经提供了关于代码应该如何运行的非常清晰的规范。让我们尝试遵守它们:

void YourMethod( int oldValue, int newValue )
{
    bool oldValueNonZero = oldValue != 0;
    bool newValueNonZero = newValue != 0;

    if( oldValueNonZero ) { X(); }
    if( newValueNonZero ) { Y(); }
    if( oldValueNonZero && newValueNonZero ) { Z(); }
}

那么为什么我喜欢这个特定的实现。让我们分解一下。

首先,请注意,我选择创建临时布尔值来捕获测试旧值/新值是否非零的结果。通过捕获这些值,我可以避免多次执行计算,并且还可以使代码更具可读性(见下文)。

第二,通过选择描述性名称 oldValueNonZeronewValueNonZero,我使实现清楚地表明了我的期望。这既提高了代码的可读性,又清楚地向未来必须阅读代码的开发人员传达了我的意图。

第三,请注意 if() 测试的主体包含在 {} 括号中 - 这有助于减少未来对实现的更改破坏行为的可能性 - 例如,意外地包含一个新案例。使用单行 ifs 是解决未来问题的良方。

最后,我不会尝试缩短比较并提前退出函数。如果性能极其重要,那么提前退出可能会很有用。但除此之外,如果只有一个退出点(1),则更容易理解方法的行为。

这段代码是否按照规范所述进行操作?我相信是的。

它容易阅读和理解吗?至少在我看来,我会说是的。

这段代码是短路逻辑的最紧凑或最复杂的方式吗?几乎肯定不是……但在我看来,它的其他品质远远弥补了这一点。

在某种程度上,您是否喜欢这种特定的代码结构取决于品味和风格。但我希望我所阐述的关于如何组织它的原则可以帮助您将来做出此类决定。

您指出,您有时会遇到类似的“逻辑网格”问题,但案例数量更多。由于两个不同的原因,此类问题可能会变得复杂:

  1. 参数可以采用的值的数量不断增加 - 它们可以采用一般形式 MxN
  2. 维数增加 - 换句话说,规则中包含更多变量:MxNxOxP...xZ

该问题的一个通用解决方案(如另一个响应所示)是将解决方案编码为多维矩阵 - 并为每种情况定义一组操作。然而,规则完全有可能重叠 - 并且为了简单起见,可能需要将等效案例折叠在一起。

我对处理一般情况的回应是……这取决于情况。如果条件可以减少到极少数情况,那么命令式 if/else 逻辑可能仍然是解决问题的最佳方法。如果条件的数量非常大,那么使用声明性方法可能是有意义的,在这种方法中,您使用某种查找表或矩阵来对情况进行编码。

1> - 方法仅具有单个退出点这一原则的一个常见例外是先决条件。首先检查所有/任何先决条件,如果违反则使方法失败(退出),这样可以更干净地避免嵌套和反转逻辑。

Let's look at your problem from another point of view. When designing a body of code, I try to apply the following principle:

  1. Make it correct.
  2. Make it clear.
  3. Make it concise.
  4. Make it fast.
    ... in that order.

All of these, to one extent or another, are subjective. However, reasonable people tend to find common ground - and there's often more agreement about their opposites. But that aside...

The first priority here is making sure that your code will work correctly. Clearly, there are multiple implementation that achieve this - but I would also add that it's important that it be easy to demonstrate that the implementation is correct. One way of achieving this is to make the code read more like the spec (more on this later).

The second priority is to make sure that in the future, when a developer (including the original author!) looks at this code they'll be able to understand what it's doing right away. The more sophisticated (read: fancy) the implementation, the harder it is for a developer to immediately understand what the code is doing.

The third priority - short, concise code, is one that partially stands in opposition to the first two. A desire to make the code more concise, may lead you to use more complex constructs than are actually necessary to solve the problem. While it's important to keep the code short, we shouldn't do this by making it unintelligibly dense.

The last priority - performance - only matters when it matters. By this, I mean that you shouldn't complicate the implementation from the point of view of performance unless you've performed profiling and identified it as bottleneck in your system.

So now that we've looked at the principles that should drive our decisions, let's apply them to the problem at hand. You've provided a very clear specification of how the code is supposed to behave. Let's try to adhere to them:

void YourMethod( int oldValue, int newValue )
{
    bool oldValueNonZero = oldValue != 0;
    bool newValueNonZero = newValue != 0;

    if( oldValueNonZero ) { X(); }
    if( newValueNonZero ) { Y(); }
    if( oldValueNonZero && newValueNonZero ) { Z(); }
}

So why do I like this particular implementation. Let's break it down.

First, note that I've chosen to create temporary boolean to capture the result of testing the old/new value for whether they are nonzero. By capturing these values, I avoid performing the calculation more than once, and I also make the code more readable (see below).

Second, by choosing the descriptive names oldValueNonZero and newValueNonZero I'm making the implementation clearly indicate my expectations. This both improves the readability of the code and clearly conveys my intent to future developers who have to read it.

Third, note that the body of the if() test is wrapped in { and } brackets - this helps reduce that chance that future changes to the implementation will break the behavior - by accidentally including a new case, for instance. Using single-line ifs is a recipe for future problems.

Finally, I don't try to short-circuit the comparison and exit the function early. If performance were extremely important, an early exit may be useful. But otherwise, it makes it easier to understand the behavior of a method if there's only one exit point(1).

Does this code do what the spec says? I believe so.

Is it easy to read and understand. At least to my eyes, I would say yes.

Is this code the most compact or sophisticated way of short-circuiting the logic? Almost certainly not ... but it's other qualities more than make up for that, in my opinion.

Whether you like this particular structure of code or not is, to some extent, a matter of taste and style. But I hope that the principles I've laid out about how I chose to organize it may help you make such decisions in the future.

You've indicated that you sometimes run into similar "logic-grid" problems, but ones where the number of cases are more numerous. These kinds of problems can become complicated for two separate reasons:

  1. The number of values that the parameters can take on increase - they can take on the general form MxN.
  2. The number of dimensions increase - in other words, there are more variables to include in the rules: MxNxOxP...xZ.

One generalized solution to the problem (as another response indicates), is to encode the solution as multidimensional matrix - and define a set of actions for each case. However, it's entirely possible for the rules to overlap - and it's probably desirable to collapse equivalent cases together, for simplicity.

My response to dealing with the general case is ... that it depends. If the conditions can be reduced to some very small number of cases, than imperative if/else logic may still be the best way to solve the problem. If the number of conditions are very large, than it may make sense to use a declarative approach, in which you use some kind of lookup table or matrix to encode the cases.

1> - One common exception to the principle of only having a single exit point from a method is for preconditions. It's cleaner to avoid nesting and inverted logic by first checking all/any preconditions, and failing (exiting) the method if they are violated.

悍妇囚夫 2024-09-23 10:43:23

实际上,您可以使用多维数组来缩短测试和分支过程。如果查找的结果不是固定值,而是需要完成的某种工作,则可以将该数组设为委托数组,并用方法或可执行代码 (lambda) 填充它。

像这样:

private void Y() { }
private void X() {}
private void Z(int n) {}

private void example(int oldvalue, int newvalue)
{
    var array = new Action[2, 2];
    array[0, 0] = () => { };
    array[0, 1] = () => { Y(); Z(1); };
    array[1, 0] = () => { X(); Z(-1); };
    array[1, 1] = () => { X(); Y(); };

    // invoke
    array[oldvalue == 0 ? 0 : 1, newvalue == 0 ? 0 : 1]();
}

您还可以使用内联初始化程序初始化状态数组,但我发现在显式语句中分解单元格分配更容易阅读和跟踪内容。

数组的初始化可以提前完成,也许在包含此示例函数的类的构造函数中完成。然后,示例函数中的执行将减少为

  1. 评估 oldvalue == 0
  2. 评估 newvalue == 0
  3. 数组中的索引以检索委托
  4. 执行委托以执行操作

此技术会导致非常非常小的执行开销(快速),可扩展可以很好地适应任意数量的维度,并保留代码和逻辑表之间的相关性,与到处都是混乱的 if 语句相比,这应该使它更容易理解和修改。

You can actually use a multidimentional array to shortcut your test and branch process. If the result of the lookup is not a fixed value, but is instead some sort of work that needs to be done, you can make the array an array of delegate and populate it with methods or executable code (lambdas).

Like this:

private void Y() { }
private void X() {}
private void Z(int n) {}

private void example(int oldvalue, int newvalue)
{
    var array = new Action[2, 2];
    array[0, 0] = () => { };
    array[0, 1] = () => { Y(); Z(1); };
    array[1, 0] = () => { X(); Z(-1); };
    array[1, 1] = () => { X(); Y(); };

    // invoke
    array[oldvalue == 0 ? 0 : 1, newvalue == 0 ? 0 : 1]();
}

You can also initialize the state array using an inline initializer, but I find breaking out the cell assignments in explicit statements much easier to read and keep track of what goes where.

Initialization of the array could be done in advance, perhaps in the constructor of the class that contains this example function. Execution in the example function would then be reduced to

  1. evaluate oldvalue == 0
  2. evaluate newvalue == 0
  3. index into the array to retrieve the delegate
  4. execute the delegate to perform the action

This technique results in very, very little execution overhead (fast), scales well to arbitrary numbers of dimensions, and preserves the correlation between the code and your logic table which should make it much easier to understand and modify later compared to jumbles of if statements all over the place.

£冰雨忧蓝° 2024-09-23 10:43:23

额外积分解决方案

这是一个非常有趣的主题,贡献者提供了一些深刻的见解。我认为你的问题已经得到解答,所以我将重点关注额外的学分。最简单的事情是更新您的表并将其应用到您最喜欢的解决方案。

让我们以完整描述每列条件的方式来表述问题。如果新值等于零​​或旧值,我们将从第一列开始执行操作。如果新值非零且不等于旧值,我们将从第二列执行操作。

          New == 0 || New == Old    New != 0 && New != Old
          ----------------------   ------------------------
Old == 0 | nothing                  Y, Z(1)
Old != 0 | X, Z(-1)                 X, Y    -- Z(0) is okay but not required for this quadrant

因此,在 dthorpe 的解决方案中,我们将 newValue == 0 替换为 newValue == 0 || newValue == oldValue

private void example(int oldvalue, int newvalue)
{
    var array = new Action[2, 2];
    array[0, 0] = () => { };
    array[0, 1] = () => { Y(); Z(1); };
    array[1, 0] = () => { X(); Z(-1); };
    array[1, 1] = () => { X(); Y(); };

    array[oldvalue == 0 ? 0 : 1, newValue == 0 || newValue == oldValue ? 0 : 1]();
}

在 LBushkin 的解决方案中,我们将 newValue != 0 替换为 newValue != 0 && newValue != oldValue 并更新相应的变量名称以保持可读性。我还要对代码位进行混蛋:

void YourMethod( int oldValue, int newValue )
{
    bool oldValueNonZero = oldValue != 0;
    bool newValueDifferentAndNonZero = newValue != 0 && newValue != oldValue;
    int zCondition = 0;

    if( oldValueNonZero ) { X(); zCondition--;}
    if( newValueDifferentAndNonZero ) { Y(); zCondition++;}
    if( zCondition != 0 ) { Z(zCondition); }
}

Tada!

Extra Credit Solution

This is a very interesting topic with some great insight from the contributors. I think your question's been answered so I'll focus on the extra credit. The easiest thing to do is update your table and apply it to whichever solution you like best.

Let's phrase the problem in a way that the conditions for each column are completely described. We would perform an action from the first column if the new value is equal to either zero or the old value. We would perform an action from the second column if the new value is nonzero and not equal to the old value.

          New == 0 || New == Old    New != 0 && New != Old
          ----------------------   ------------------------
Old == 0 | nothing                  Y, Z(1)
Old != 0 | X, Z(-1)                 X, Y    -- Z(0) is okay but not required for this quadrant

So in dthorpe's solution we would replace newValue == 0 with newValue == 0 || newValue == oldValue :

private void example(int oldvalue, int newvalue)
{
    var array = new Action[2, 2];
    array[0, 0] = () => { };
    array[0, 1] = () => { Y(); Z(1); };
    array[1, 0] = () => { X(); Z(-1); };
    array[1, 1] = () => { X(); Y(); };

    array[oldvalue == 0 ? 0 : 1, newValue == 0 || newValue == oldValue ? 0 : 1]();
}

In LBushkin's solution we would replace newValue != 0 with newValue != 0 && newValue != oldValue and update the respective variable name to maintain readability. I'm also going to bastardize the code bit:

void YourMethod( int oldValue, int newValue )
{
    bool oldValueNonZero = oldValue != 0;
    bool newValueDifferentAndNonZero = newValue != 0 && newValue != oldValue;
    int zCondition = 0;

    if( oldValueNonZero ) { X(); zCondition--;}
    if( newValueDifferentAndNonZero ) { Y(); zCondition++;}
    if( zCondition != 0 ) { Z(zCondition); }
}

Tada!

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文