模式匹配与if-else


76

我是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")

这些方法是否相等?由于某种原因,其中一个比另一个更可取吗?

Answers:


105
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大大提高了代码的清晰度,请继续进行下去(除非在少数情况下,您知道性能至关重要,然后您可能希望比较两者之间的差异)。


3
我只是对模式匹配有印象。我认为这就是为什么我尝试在任何地方使用它的原因:)谢谢,我会按照您的建议进行操作。
Soteric

14
@Soteric这是Scala程序员的常见阶段。您将经历其他更糟糕的阶段。:-)
Daniel C. Sobral

@Daniel喜欢跨几行的类型签名吗?
ziggystar

10
@ DanielC.Sobral我认为最好汇编这些“不要过度”阶段的列表...
范式

6
您可以将字节码大小的差异视为错误。希望Scala编译器将来可以优化模式匹配,使其与if-else一样紧密。然后,它只能归结为可读性。
ebruchez 2012年

31

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

顺便说一句,最好编写代码而不重复println

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

天啊 那应该是我的榜样。
ziggystar

15

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

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

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

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

11

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

模式匹配通常用于分解更复杂的构造,例如多态表达式或将unapply对象解构成其组件。我不会建议使用它作为替代一个简单的if-else语句-有没有错的if-else

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

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

我为这个愚蠢的例子表示歉意。


5

对于大多数对性能不敏感的代码,有很多原因使您想在if / else上使用模式匹配:

  • 它为每个分支强制使用一个公共返回值和类型
  • 在具有详尽性检查的语言(例如Scala)中,它迫使您明确考虑所有情况(对不需要的情况不做任何操作)
  • 它可以防止提前退货,如果退货,层叠数量增加或分支的增长时间超过屏幕高度(此时它们变得不可见),则更难推理。缩进量过多会警告您处于示波器内。
  • 它可以帮助您确定退出的逻辑。在这种情况下,代码可能会被重写并使其更加干燥,可调试和可测试,如下所示:
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表达式。但这与此处无关,并且不适用于具有两个以上分支的条件。

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


5
使用if / else不需要突变。等价于Scala中的三元运算符可以解决此问题: val errorMessage = if (user.password == enteredPassword) "User is authenticated" else "Entered password is invalid"
Jean-Marc S.

我在最初的评论中提到了这一点:“是的,您可以争辩说,对于像布尔检查一样简单的事情,您可以使用if表达式。但这在这里不相关,并且不适用于具有两个以上分支的条件。 ”
凯文·李

您编写了“使用if / else编写此代码将需要进行突变的地方”。那仍然是不正确的。只要所有分支都是同一类型,就不需要if / else突变。例如:val k = if (false) "1" else if (false) "2" else "3"
Jean-Marc S.

我希望我前面提到的注释将取代您引用的行-if-expression / ternary语句与if / else块不同(因此与原始问题无关),并且不合理地扩展到2个以上的分支。可以将if / else块括在括号中并使用该值,但我认为这不是惯用语言。无论如何,我都更新了答案以消除混乱。
Kevin Li

1
不要将您的观点与事实混淆。If'/ else可以很好地扩展到多个分支。模式匹配对于...匹配模式也非常有用。
Jean-Marc S.

2

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

     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次比赛Match:Long = 1092


坦率地说,这种基准测试很糟糕。首先,System.currentTimeMillis()精度很差;System.nanoTime通常更好。即使这样,您也应该消除JIT编译,垃圾收集等的影响。最好使用微型基准测试工具(例如ScalaMeter来正确评估这两种方法。)
Mike Allen

@MikeAllen,是的,很久以前)我和您一起使用微基准测试工具
Andrey

哈哈哈!很公平。;-)
Mike Allen

2

到2020年,Scala编译器在模式匹配的情况下生成效率更高的字节码。公认答案中的绩效评论在2020年具有误导性。

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

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

您可以尝试以下代码片段并查看结果:

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版本进行了这些改进吗?不幸的是,由于我们被Spark和2.11代码所困扰,其中一些在2020年不会编写代码。希望我们能在不久的将来达到2.12!
尼克

1

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

实际上,如果您将第一个示例放入IntelliJ中,它将建议您更改为第二种(如果... 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 {
      ???
    }

0

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

timeIf:Long = 249 timeMatch:Long = 68

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.