【问题标题】:Is it possible to simplify an if-statement that checks for a combination?是否可以简化检查组合的 if 语句?
【发布时间】:2016-08-08 22:20:26
【问题描述】:

我目前正在为游戏添加音效,虽然我当前的代码运行良好,但我正在寻找一种简化它的方法。 基本上,游戏中的每个物体都有一个字符串值表示其材质(即“木头”、“金属”等),当两个物体碰撞时,会根据组合播放声音效果。代码基本上是这样的:

if( (matA == "metal" && matB == "wood") || (matA == "wood" && matB == "metal") )
{
    //play sound for metal-wood collision
}

但我想知道是否有办法将 if 语句简化为以下内容:

if( one of the materials is wood && one of the materials is metal )
{
    //play sound for metal-wood collision
}

【问题讨论】:

  • 是出于性能原因还是“更好看的代码”需要简化?如果第一个为真,您可以为每种材料分配一个质数而不是字符串(可能使用宏/枚举)并检查两种碰撞材料的乘积。这样,两种材料的每种组合都有一个唯一编号。
  • 可以执行if (new HashSet<string> { matA, matB, }.SetEquals(new HashSet<string> { "metal", "wood", })) 之类的操作,但运行速度会比您所拥有的要慢。你实际上只需要.IsSupersetOf,但这对于代码的读者来说可能不太明显,而且我认为HashSet<> 有优化,这意味着你不会从使用.IsSupersetOf 中获得太多的性能。无论如何,如果您有两个以上的变量,这将开始变得更有意义。有两个变量,坚持你所拥有的,或者从韦斯顿的回答中考虑好的语法。
  • @phil13131 你刚刚推荐了一个素数分解来识别唯一的标志吗?这是一个令人愉快的创造性解决方案,但为它们每个分配 2 的幂,并且仅使用标准但操作方法肯定会更直观和惯用
  • @phil13131 简化是为了让代码看起来更漂亮。我觉得必须有一种更简洁的方法来检查材料的组合,而不用担心出现的顺序或有一个容易出现拼写错误或被遗忘的组合的 if-else 语句墙。
  • @SteveCox 我同意,但是,我提出了这个建议,因为这只会为您留下 32 种可能的 4 字节整数材料,我想可能会出现需要更多的情况(甚至超过 64长期)。但如果可能的话,位图当然是一种更快、更清洁的解决方案。

标签: c# if-statement unity3d


【解决方案1】:

您应该使用enum 来代替字符串,并且可以使用Dictionary 来保存相应的声音组合。您可以跳过多个if 语句并使用Dictionary 自动为每种材料选择相应的对象。例如:

[Flags]
enum Material  
    {  
        Wood=1,
        Iron=2,
        Glass=4
        //...  
    }  
Dictionary<Material,SoundObject> sounds = new Dictionary<Material,SoundObject>();  
sounds.add(Material.Wood,woodSound);  
sounds.add(Material.Iron,ironSound);  
sounds.add(Material.Wood | Material.Iron,woodAndIronSound);

// And play corresponding sound directly without any if statement.  
sounds[object.Material].Play();  
sounds[matA | matB].Play();  

性能优势:

您还可以通过使用这种方法来提高性能。因为枚举值或哈希码的整数比较肯定比字符串比较更容易和更快。而关于字典VS多个if-else语句,if/else if语句系列是线性执行的;所以它的性能很大程度上取决于 if 语句的数量和对象的相等比较器;而Dictionary 基于哈希表。它使用索引优化的集合来存储值,该集合具有有效的恒定访问时间。这意味着通常无论字典中有多少键,您都将在恒定时间内访问值,并且在大多数情况下它比多个 if 语句要快得多。

性能对比:

我们将在这个例子中比较两种方法的性能:

//If you want to try, just copy the code and see the result.  
static Dictionary<char, short> myHashTable = Enumerable.Range((short)'A', (short)'z').ToDictionary((ch) => (char)ch, (sh) => (short)sh);  

static void Main(string[] args)  
{  
    System.Diagnostics.Stopwatch SW = new   System.Diagnostics.Stopwatch();  
    short temp = 0;  
    SW.Start();  
    for(int i=0;i<10000000;i++)  
    temp = getValue('z');  
    SW.Stop();  
    Console.WriteLine(SW.ElapsedMilliseconds );  
    SW.Reset();              
    SW.Start();  
    for(int i =0;i<10000000;i++)  
    temp = myHashTable['a'];  
    SW.Stop();  
    Console.WriteLine(SW.ElapsedMilliseconds);  
}  
static short getValue(char input)  
{  
    if (input == 'a')  
        return (short)'a';  
    else if (input == 'b')  
        return (short)'b';  
    else if (input == 'c')  
        return (short)'c';  
    else if (input == 'd')  
        return (short)'d';  
    else if (input == 'e')  
        return (short)'e';  
    else if (input == 'f')  
        return (short)'f';  
    else if (input == 'g')  
        return (short)'g';  
    else if (input == 'h')  
        return (short)'h';  
    else if (input == 'i')  
        return (short)'i';  
    else if (input == 'j')  
        return (short)'j';  
    else if (input == 'k')  
        return (short)'k';  
    else if (input == 'l')  
        return (short)'l';  
    else if (input == 'm')  
        return (short)'m';  
    else if (input == 'n')  
        return (short)'n';  
    else if (input == 'o')  
        return (short)'o';  
    else if (input == 'p')  
        return (short)'p';  
    else if (input == 'q')  
        return (short)'q';  
    else if (input == 'r')  
        return (short)'r';  
    else if (input == 's')  
        return (short)'s';  
    else if (input == 't')  
        return (short)'t';  
    else if (input == 'u')  
        return (short)'u';  
    else if (input == 'v')  
        return (short)'v';  
    else if (input == 'w')  
        return (short)'w';  
    else if (input == 'x')  
        return (short)'x';  
    else if (input == 'y')  
        return (short)'y';  
    else if (input == 'z')  
        return (short)'z';  
    return 0;  

} 

结果:

if 26 条语句|包含 122 个项目的字典。
593 254
579 256
572 252
570 246
587 248
574 291
576 246
685 265
599 282
723 338

表示字典比 if/else if 语句快 2 倍以上。

【讨论】:

  • 这是个好主意,只需要明确字典必须设置一次并多次重复使用。事实上,看起来 OP 会在检查字典之前这样做。
  • 我想指出 [Flags] 为这种用途设计的枚举属性
  • 完全避免 if-else-statements 的墙听起来很棒,所以我一定会尝试一下。谢谢你:)
  • 指出如果你要做Material.Wood | Material.Iron之类的事情,枚举值应该是2的幂:Wood = 1, Iron = 2, Wool = 4, Glass = 8, ...
  • @weston 感谢您的关注和通知。我编辑了帖子,请您再次访问并提出您的想法。
【解决方案2】:

当你发现自己repeating code 时,通常的做法是提取一个方法:

if (IsWoodAndMetal(matA, matB) || IsWoodAndMetal(matB, matA))
{
    // play sound for metal-wood collision
}

其中IsWoodAndMetal定义为:

public static bool IsWoodAndMetal(string matA, string matB)
{
    return matA == "wood" && matB == "metal";
}

这将与原始代码一样快,这与分配内存的所有 linq/list 和字符串连接解决方​​案不同,这对于频繁的游戏循环来说是坏消息,因为它会导致更频繁和/或更长时间垃圾回收。

如果|| 仍然困扰您,我们可以更进一步,提取:

public static bool EitherParameterOrder<T>(Func<T, T, bool> func, T a, T b)
{
    return func(a, b) || func(b, a);
}

现在是:

if (EitherParameterOrder(IsWoodAndMetal, matA, matB))
{
    // play sound for metal-wood collision
}

而且我仍然喜欢它优于其他解决方案的性能(除了字典解决方案,当你有几个条目时)。

【讨论】:

    【解决方案3】:

    这可能不是最现代的解决方案,但使用质数作为材料的参考可以提高您的性能。我知道并理解“在必要之前进行优化”是许多程序员不推荐的,但是,在这种情况下,我认为它并没有增加代码的复杂性,而是增加了这个(相当微不足道的)任务的性能.

    public static class Materials
    {
       public static uint Wood = 2,
       public static uint Metal = 3,
       public static uint Dirt = 5,
       // etc...
    }
    
    if(matA*matB == Materials.Wood*Materials.Metal)
    {
       //play sound for metal-wood collision
    }
    
    //or with enums but annoying casts are necessary...
    
    enum Materials:uint
    {
       Wood = 2,
       Metal = 3,
       Dirt = 5,
       // etc...
    }
    
    if((uint)matA*(uint)matB == (uint)Materials.Wood*(uint)Materials.Metal)
    {
       //play sound for metal-wood collision
    }
    

    这种方法与材料的顺序无关(交换乘法),不需要对字符串进行任何长比较或任何比整数更复杂的结构。

    假设您希望将所有参考数字保留为 4 字节整数,并且最大 4 字节整数的平方根约为 65535,这将留下大约 6550 个可能低于 65535 的素数,这样任何产品都不会导致整数溢出。对于任何常见的游戏来说,这应该足够了。

    【讨论】:

    • 这只是另一个 answer 中给出的 or'ing 二进制标志的更复杂版本。对机器来说| 更容易,更容易理解正在发生的事情,不需要解释性 cmets,也不需要计算素数。
    • 没有。编程的每个人都知道二进制标志的顺序,1,2,4,8,16,32...。没有人知道或使用素数的序列。
    • 是的,但这只会给您留下 32 种使用 4 字节整数的可能材料。如果他需要更多怎么办?我同意,如果他只需要这么低的微不足道的数量,那就更好了,但如果他不想照顾缩放,我仍然更喜欢我的解决方案。
    • @nhgrif 如果您已完整阅读我的回答,您就会知道我已经提供了该信息。您可以使用 4 字节整数来使用大约 6550 种材料。已经表示他想要“碰撞”材料,所以可以假设他只是指 2 的组合。
    • @nhgrif 目前不在这个问题的范围内。首先,您必须定义多材质碰撞应该是什么样子,以及为什么它听起来与三对独立碰撞不同。无论如何,基于这个问题,只有两种材料是一个安全的假设,这是一个不错的解决方案,如果有点模糊的话。应该针对使用字典进行分析/基准测试,如果性能至关重要,则选择更快的字典 - 对于游戏/物理引擎来说可能是这样。
    【解决方案4】:

    您应该将 mat{A,B} 类型更改为枚举。 其定义如下:

    [Flags]
    enum Materials {
        Wood = 1,
        Metal = 2,
        Concrete = 4,
        // etc ...
    }
    

    那么代码将如下所示:

    Meterials matA = Materials.Wood;
    Meterials matB = Materials.Metal;
    
    if ((matA | matB) == (Materials.Metal | Materials.Wood))
    {
        // Do something
    }
    

    这里唯一的问题是,matA 现在可以同时属于 Wood 和 Metal 类型,但这个问题也存在于字符串解决方案中。

    --- 编辑 ---

    也可以为木头和金属创建枚举别名

    [Flags]
    enum Materials
    {
        Wood = 1,
        Metal = 2,
        WoodMetal = Wood | Metal,
        Concrete = 4,
        // etc
    }
    

    那么代码将如下所示:

    Materials matA = Materials.Wood;
    Materials matB = Materials.Metal;
    
    if ((matA | matB) == Materials.WoodMetal)
    {
        // Do something
    }
    

    【讨论】:

    • WoodMetal 可以定义为WoodMetal = Wood ^ MetalWoodMetal = Wood | Metal(我更喜欢)
    • 您选择的^ 而非| 不是standard for Flags。它也可能导致问题,例如:WoodMetal ^ Wood == MetalMetalWoodMetal | Wood == WoodMetal 如果您想要这样做,前者很好,但是当我们想要组合标志而不是关闭它们时,| 更有意义.
    • @weston 感谢您的评论,我已修复代码。
    • 好的,但是你错过了第一点。 WoodMetal = 3, // alias for Wood | Metal 行可以是:WoodMetal = Wood | Metal,
    【解决方案5】:

    我觉得有必要发布我认为最“明显”的解决方案,但似乎还没有其他人发布过。如果接受的答案对您有用,请选择那个答案。我只是为了完整性添加这个。

    首先,定义一个静态辅助方法来进行双向比较:

    public static bool MaterialsMatch(string candidate1, string candidate2, 
                                       string expected1, string expected2)
    {
        return (candidate1 == expected1 && candidate2 == expected2) || 
               (candidate1 == expected2 && candidate2 == expected1);
    }
    

    然后在您的 if 语句中使用它:

    if (MaterialsMatch(matA, matB, "wood", "metal"))
    {
        // play sound for wood-metal collision
    }
    else if (MaterialsMatch(matA, matB, "wood", "rubber"))
    {
        // play sound for wood-rubber collision
    }
    else if (MaterialsMatch(matA, matB, "metal", "rubber"))
    {
        // play sound for metal-rubber collision
    }
    // etc.
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2023-03-19
      • 2022-06-13
      • 1970-01-01
      • 2014-12-07
      • 2022-07-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多