【问题标题】:Pattern matching vs if-else模式匹配与 if-else
【发布时间】:2012-02-13 19:36:53
【问题描述】:

我是 Scala 的新手。最近我在写一个爱好应用,发现自己在很多情况下都在尝试使用模式匹配而不是 if-else。

user.password == enteredPassword match {
  case true => println("User is authenticated")
  case false => println("Entered password is invalid")
}

而不是

if(user.password == enteredPassword)
  println("User is authenticated")
else
  println("Entered password is invalid")

这些方法是否相同?出于某种原因,其中一个是否比另一个更受欢迎?

【问题讨论】:

    标签: scala


    【解决方案1】:
    class MatchVsIf {
      def i(b: Boolean) = if (b) 5 else 4
      def m(b: Boolean) = b match { case true => 5; case false => 4 }
    }
    

    我不确定你为什么要使用更长更笨重的第二个版本。

    scala> :javap -cp MatchVsIf
    Compiled from "<console>"
    public class MatchVsIf extends java.lang.Object implements scala.ScalaObject{
    public int i(boolean);
      Code:
       0:   iload_1
       1:   ifeq    8
       4:   iconst_5
       5:   goto    9
       8:   iconst_4
       9:   ireturn
    
    public int m(boolean);
      Code:
       0:   iload_1
       1:   istore_2
       2:   iload_2
       3:   iconst_1
       4:   if_icmpne   11
       7:   iconst_5
       8:   goto    17
       11:  iload_2
       12:  iconst_0
       13:  if_icmpne   18
       16:  iconst_4
       17:  ireturn
       18:  new #14; //class scala/MatchError
       21:  dup
       22:  iload_2
       23:  invokestatic    #20; //Method scala/runtime/BoxesRunTime.boxToBoolean:(Z)Ljava/lang/Boolean;
       26:  invokespecial   #24; //Method scala/MatchError."<init>":(Ljava/lang/Object;)V
       29:  athrow
    

    这也是比赛的更多字节码。即便如此,它仍然相当高效(除非比赛引发错误,否则不会出现拳击,这在此处不会发生),但为了紧凑性和性能,应该支持if/else。但是,如果使用 match 大大提高了代码的清晰度,请继续(除非在极少数情况下,您知道性能至关重要,然后您可能想要比较差异)。

    【讨论】:

    • 我只是对模式匹配有印象。我想这就是我尝试在任何地方使用它的原因:) 谢谢,我会听从你的建议。
    • @Soteric 这是 Scala 程序员的常见阶段。你会经历其他更糟糕的阶段。 :-)
    • @Daniel 喜欢跨越多行的类型签名?
    • @DanielC.Sobral 我认为编制一份“不要过度”阶段的清单会很好......
    • 您可以将字节码大小的差异视为一个错误。希望 Scala 编译器将来会将模式匹配优化为与 if-else 一样紧密。然后它只会归结为可读性,因为它应该。
    【解决方案2】:

    不要在单个布尔值上进行模式匹配;使用 if-else。

    顺便说一句,代码最好不要重复println

    println(
      if(user.password == enteredPassword) 
        "User is authenticated"
      else 
        "Entered password is invalid"
    )
    

    【讨论】:

    • 哦。那应该是我的榜样。
    【解决方案3】:

    一个可以说是更好的方法是直接在字符串上进行模式匹配,而不是在比较的结果上,因为它避免了“布尔盲”。 http://existentialtype.wordpress.com/2011/03/15/boolean-blindness/

    一个缺点是需要使用反引号来保护输入的密码变量不被隐藏。

    基本上,您应该尽量避免处理布尔值,因为它们不会在类型级别传达任何信息。

    user.password match {
        case `enteredPassword` => Right(user)
        case _ => Left("passwords don't match")
    }
    

    【讨论】:

      【解决方案4】:

      两个语句在代码语义方面是等价的。但编译器可能会在一种情况下(match)创建更复杂(因此效率低下)的代码。

      模式匹配通常用于分解更复杂的构造,例如多态表达式或将 (unapplying) 对象解构为它们的组件。我不建议将它用作简单的 if-else 语句的替代项 - if-else 没有任何问题。

      请注意,您可以将其用作 Scala 中的表达式。这样你就可以写了

      val foo = if(bar.isEmpty) foobar else bar.foo
      

      我为这个愚蠢的例子道歉。

      【讨论】:

        【解决方案5】:

        现在是 2020 年,Scala 编译器在模式匹配情况下生成的字节码效率要高得多。已接受答案中的性能指标在 2020 年具有误导性。

        模式匹配生成的字节码给 if-else 带来了激烈的竞争,有时模式匹配会获胜,从而得到更好和一致的结果。

        可以根据情况和简单性使用模式匹配或 if-else。 但模式匹配性能差的结论不再有效。

        你可以试试下面的sn-p看看结果:

        def testMatch(password: String, enteredPassword: String) = {
            val entering = System.nanoTime()
            password == enteredPassword match {
              case true => {
                println(s"User is authenticated. Time taken to evaluate True in match : ${System.nanoTime() - entering}"
                )
              }
              case false => {
                println(s"Entered password is invalid. Time taken to evaluate false in match : ${System.nanoTime() - entering}"
                )
              }
            }
          }
        
        
         testMatch("abc", "abc")
         testMatch("abc", "def")
            
        Pattern Match Results : 
        User is authenticated. Time taken to evaluate True in match : 1798
        Entered password is invalid. Time taken to evaluate false in match : 3878
        
        
        If else :
        
        def testIf(password: String, enteredPassword: String) = {
            val entering = System.nanoTime()
            if (password == enteredPassword) {
              println(
                s"User is authenticated. Time taken to evaluate if : ${System.nanoTime() - entering}"
              )
            } else {
              println(
                s"Entered password is invalid.Time taken to evaluate else ${System.nanoTime() - entering}"
              )
            }
          }
        
        testIf("abc", "abc")
        testIf("abc", "def")
        
        If-else time results:
        User is authenticated. Time taken to evaluate if : 65062652
        Entered password is invalid.Time taken to evaluate else : 1809
        

        PS:由于数字是纳米精度,因此结果可能与确切数字不完全匹配,但性能方面的论点仍然成立。

        【讨论】:

        • 你知道是哪个 scala 版本做出了这些改进吗?不幸的是,有些人在 2020 年不会编写代码,因为我们被 Spark 和 2.11 代码困住了。希望我们能在不久的将来达到 2.12!
        【解决方案6】:

        对于大多数对性能不敏感的代码,有很多重要的理由让您希望在 if/else 上使用模式匹配:

        • 它为您的每个分支强制执行一个通用的返回值和类型
        • 在具有详尽检查的语言(如 Scala)中,它会强制您明确考虑所有情况(以及不需要的情况)
        • 它可以防止提前返回,如果它们级联、数量增加或分支长于屏幕高度(此时它们变得不可见),就会变得难以推理。有一个额外的缩进级别会警告你你在一个范围内。
        • 它可以帮助您识别要提取的逻辑。在这种情况下,代码可以被重写并变得更加 DRY、可调试和可测试,如下所示:
        val errorMessage = user.password == enteredPassword match {
          case true => "User is authenticated"
          case false => "Entered password is invalid"
        }
        
        println(errorMesssage)
        

        这是一个等效的 if/else 块实现:

        var errorMessage = ""
        
        if(user.password == enteredPassword)
          errorMessage = "User is authenticated"
        else
          errorMessage = "Entered password is invalid"
        
        println(errorMessage)
        

        是的,您可以争辩说,对于像布尔检查这样简单的事情,您可以使用 if 表达式。但这在这里无关紧要,并且不能很好地扩展到超过 2 个分支的条件。

        如果您更关心的是可维护性或可读性,那么模式匹配非常棒,您应该将它用于即使是很小的事情!

        【讨论】:

        • 使用 if/else 不需要突变。等效于 Scala 中的三元运算符可以解决这个问题:val errorMessage = if (user.password == enteredPassword) "User is authenticated" else "Entered password is invalid"
        • 我在最初的评论中提到了这个问题:“是的,你可以争辩说,对于像布尔检查这样简单的事情,你可以使用 if 表达式。但这在这里不相关,也不能很好地扩展具有超过 2 个分支的条件。”
        • 您写道“而使用 if/else 编写此代码需要突变”。那还是不对的。只要所有分支都属于同一类型,就不需要 if/else 突变。例如:val k = if (false) "1" else if (false) "2" else "3"
        • 我希望我之前提到的评论会取代你引用的那一行 - if-expression/ternary 语句与 if/else 块不同(因此与原始问题),并且无法读取超过 2 个分支。可以将 if/else 块括在大括号中并使用该值,但我不认为这是惯用的。无论如何,我已经更新了我的答案以消除混淆。
        • 不要把你的观点和事实混为一谈。 If'/else 适用于多个分支。模式匹配也非常适合......匹配模式。
        【解决方案7】:

        我也遇到过同样的问题,并进行了书面测试:

             def factorial(x: Int): Int = {
                def loop(acc: Int, c: Int): Int = {
                  c match {
                    case 0 => acc
                    case _ => loop(acc * c, c - 1)
                  }
                }
                loop(1, x)
              }
        
              def factorialIf(x: Int): Int = {
                def loop(acc: Int, c: Int): Int = 
                    if (c == 0) acc else loop(acc * c, c - 1)
                loop(1, x)
              }
        
            def measure(e: (Int) => Int, arg:Int, numIters: Int): Long = {
                def loop(max: Int): Unit = {
                  if (max == 0)
                    return
                  else {
                    val x = e(arg)
                    loop(max-1)
                  }
                }
        
                val startMatch = System.currentTimeMillis()
                loop(numIters)
                System.currentTimeMillis() - startMatch
              }                  
        val timeIf = measure(factorialIf, 1000,1000000)
        val timeMatch = measure(factorial, 1000,1000000)
        

        timeIf : Long = 22 时间匹配:长 = 1092

        【讨论】:

        • 坦率地说,这种基准测试很糟糕。首先,System.currentTimeMillis() 的精度很差; System.nanoTime 通常更好。即便如此,您也应该消除 JIT 编译、垃圾收集等的影响。最好使用微基准测试工具(例如 ScalaMeter 来正确评估这两种方法。
        • @MikeAllen 是的,伙计,很久以前)我和你一起使用微基准测试工具
        • 哈哈哈!很公平。 ;-)
        【解决方案8】:

        我在这里提出不同的意见: 对于您提供的具体示例,第二种(if...else...)样式实际上更好,因为它更易于阅读。

        实际上,如果您将第一个示例放入 IntelliJ,它会建议您更改为第二种(if...else...)样式。这是 IntelliJ 风格的建议:

        Trivial match can be simplified less... (⌘F1) 
        
        Suggests to replace trivial pattern match on a boolean expression with a conditional statement.
        Before:
            bool match {
              case true => ???
              case false => ???
            }
        After:
            if (bool) {
              ???
            } else {
              ???
            }
        

        【讨论】:

          【解决方案9】:

          在我的环境(scala 2.12 和 java 8)中,我得到了不同的结果。 Match 在上面的代码中始终表现得更好:

          timeIf: Long = 249 timeMatch: Long = 68

          【讨论】:

            猜你喜欢
            • 2021-06-15
            • 2023-04-05
            • 1970-01-01
            • 2011-12-20
            • 2016-05-07
            • 2015-09-04
            • 2017-11-29
            • 1970-01-01
            • 2018-04-18
            相关资源
            最近更新 更多