【问题标题】:Expressing 2x2 Logic Grid in Code Efficiently在代码中高效地表达 2x2 逻辑网格
【发布时间】:2023-03-03 22:58:01
【问题描述】:

在事件处理程序中,我正在响应值的更改。我可以访问旧值和新值,并希望根据更改的内容做某些事情。

每个不同的结果都会执行某种操作/功能 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;
}

这实际上比我正在处理的情况要简单一些。如果 oldvalue 和 newvalue 不为零且彼此相等,则将 newvalue 视为 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

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

【问题讨论】:

    标签: c# language-agnostic grid logic


    【解决方案1】:

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

    1. 改正。
    2. 说清楚。
    3. 简明扼要。
    4. 快点。 ... 按此顺序。

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

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

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

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

    最后的优先级 - 性能 - 仅在重要时才重要。 我的意思是,除非您已执行分析并确定它是您系统中的瓶颈。

    既然我们已经了解了应该推动我们决策的原则,让我们将它们应用到手头的问题上。您已经提供了关于代码应该如何执行的非常清晰的规范表现。让我们尝试遵守它们:

    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>- 只有一个方法退出点的原则的一个常见例外是前置条件。通过首先检查所有/任何先决条件,如果违反(退出)方法,则避免嵌套和反转逻辑更干净。

    【讨论】:

    • 哦!我喜欢您的回复,并且非常感谢您花时间讨论这些想法,而不仅仅是快速回答。我看到了创建变量来命名你正在做的事情的实用性,尽管我对此有复杂的感觉,因为我宁愿尽可能不声明额外的变量。测试布尔值是否为真实际上是否比测试 int != 0 的成本低?您将在哪里多说它“易于证明实现是正确的”?
    • 另一条评论。您建议的代码使这个理论问题更接近我正在处理的实际业务情况。我的意思是,您的代码具有所有正确的变量名和函数,听起来很像英语。这意味着代码是自记录的,易于理解且不易出错。这些都是有价值的,相信我,我确实知道这么多。
    • @Emtucifor:很高兴能帮上忙。回答您的第一个问题-使用布尔值进行测试不一定可以提高性能-主要是为了提高清晰度。阅读声明 if( someClearlyStatedCondition )if( value != 0 && anotherValue != 0 ) 更容易 - 并且比阅读以下内容更容易:if( v == z || v != 0 && (z != 0 || z < 5) || ... )
    • @Emtucifor:顺便说一句,使用布尔值的另一个原因是它避免在多个地方重复逻辑。因此,如果需求发生变化,您只需更新一个计算 - 而不是多个。
    • 好吧,只是让我皱一下,你将如何处理从旧值非零到相同的新值(例如 4 到 4)的变化,就好像它是对 0 的变化一样?我很乐意提出一个新问题。
    【解决方案2】:

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

    像这样:

    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. 评估新值 == 0
    3. 索引到数组以检索委托
    4. 执行委托以执行操作

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

    【讨论】:

    • 你知道,这真的很吸引我。我喜欢一旦你设置它,你就可以一遍又一遍地使用这个数组。我想我已经确信我不能像这样编程,除非每个数组项的定义都在其他地方以清晰的方式完成(例如在 GUI 中)。这种方式很有趣,但它完全隐藏了运行 X() 在逻辑上依赖于 oldvalue 非零。
    • 对 2x2 逻辑表使用数组有点矫枉过正,因为对应的离散逻辑只有几个 if 语句。但是,当您向逻辑表添加行、列或维度时,与等效离散逻辑所需的大量代码相比,这种数组技术仍然合理且易于管理。核心问题是:您希望您的代码看起来像您的逻辑表,还是您希望您的代码看起来像税收规则? ;>
    • 是的,一旦规则变得更加复杂,基于表格的逻辑就更有意义了!
    【解决方案3】:

    额外信用解决方案

    这是一个非常有趣的话题,贡献者有一些深刻的见解。我认为您的问题已得到解答,因此我将专注于额外的功劳。最简单的做法是更新您的表格并将其应用于您最喜欢的任何解决方案。

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

              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); }
    }
    

    多田!

    【讨论】:

      猜你喜欢
      • 2012-01-25
      • 2023-02-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-07-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多