Mustervergleich vs if-else

75

Ich bin Anfänger in Scala. Vor kurzem habe ich eine Hobby-App geschrieben und mich dabei erwischt, wie ich in vielen Fällen versucht habe, Pattern Matching anstelle von if-else zu verwenden.

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

anstatt

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

Sind diese Ansätze gleich? Ist einer von ihnen aus irgendeinem Grund vorzuziehen als der andere?

Soterisch
quelle

Antworten:

105
class MatchVsIf {
  def i(b: Boolean) = if (b) 5 else 4
  def m(b: Boolean) = b match { case true => 5; case false => 4 }
}

Ich bin mir nicht sicher, warum Sie die längere und klobigere zweite Version verwenden möchten.

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

Und das ist auch viel mehr Bytecode für das Match. Trotzdem ist es ziemlich effizient (es gibt kein Boxen, es sei denn, das Match wirft einen Fehler auf, der hier nicht auftreten kann), aber für Kompaktheit und Leistung sollte man if/ bevorzugen else. Wenn die Klarheit Ihres Codes durch die Verwendung von Übereinstimmungen erheblich verbessert wird, fahren Sie fort (außer in den seltenen Fällen, in denen Sie wissen, dass die Leistung kritisch ist, und dann möchten Sie möglicherweise den Unterschied vergleichen).

Rex Kerr
quelle
3
Ich habe gerade den Eindruck eines Mustervergleichs. Ich denke, deshalb versuche ich es überall zu verwenden :) Danke, ich werde deinem Rat folgen.
Soteric
14
@Soteric Das ist eine häufige Phase für Scala-Programmierer. Sie werden andere, schlimmere Phasen durchlaufen. :-)
Daniel C. Sobral
@ Daniel Wie wäre es mit mehrzeiligen Typensignaturen?
Ziggystar
10
@ DanielC.Sobral Ich denke, es wäre schön, eine Liste dieser "nicht übertreiben" -Phasen zusammenzustellen ...
paradigmatisch
6
Sie können den Unterschied in der Bytecode-Größe als Fehler betrachten. Es besteht die Hoffnung, dass der Scala-Compiler die Musterübereinstimmung so optimiert, dass sie in Zukunft so eng wie das Wenn-Sonst ist. Dann kommt es nur noch auf die Lesbarkeit an, wie es sollte.
Ebruchez
31

Nicht Musterübereinstimmung auf einem einzelnen Booleschen Wert; benutze ein if-else.

Übrigens ist der Code besser geschrieben, ohne zu duplizieren println.

println(
  if(user.password == enteredPassword) 
    "User is authenticated"
  else 
    "Entered password is invalid"
)
Retronym
quelle
D'oh. Das hätte mein Beispiel sein sollen.
Ziggystar
15

Ein wohl besserer Weg wäre, die Musterübereinstimmung direkt auf der Zeichenfolge und nicht auf dem Ergebnis des Vergleichs vorzunehmen, da dadurch "boolesche Blindheit" vermieden wird. http://existentialtype.wordpress.com/2011/03/15/boolean-blindness/

Ein Nachteil ist die Notwendigkeit, Backquotes zu verwenden, um die Variable enterPassword vor Schatten zu schützen.

Grundsätzlich sollten Sie vermeiden, sich so weit wie möglich mit Booleschen Werten zu befassen, da diese keine Informationen auf Typebene vermitteln.

user.password match {
    case `enteredPassword` => Right(user)
    case _ => Left("passwords don't match")
}
Clément D.
quelle
11

Beide Anweisungen sind hinsichtlich der Codesemantik äquivalent. Es ist jedoch möglich, dass der Compiler in einem Fall (dem match) komplizierteren (und damit ineffizienten) Code erstellt .

Pattern Matching wird normalerweise verwendet, um kompliziertere Konstrukte wie polymorphe Ausdrücke oder das Dekonstruieren von unapplyObjekten in ihre Komponenten zu zerlegen. Ich würde nicht raten, es als Ersatz für eine einfache if-else- Aussage zu verwenden - es ist nichts falsch mit if-else .

Beachten Sie, dass Sie es als Ausdruck in Scala verwenden können. So können Sie schreiben

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

Ich entschuldige mich für das dumme Beispiel.

Zikzystar
quelle
5

Für die große Mehrheit des Codes, der nicht leistungsabhängig ist, gibt es viele gute Gründe, warum Sie den Mustervergleich verwenden möchten, wenn / sonst:

  • Es erzwingt einen gemeinsamen Rückgabewert und Typ für jeden Ihrer Zweige
  • In Sprachen mit Vollständigkeitsprüfungen (wie Scala) müssen Sie alle Fälle explizit berücksichtigen (und nicht die Fälle, die Sie nicht benötigen).
  • Es verhindert frühzeitige Rückgaben, die schwerer zu begründen sind, wenn sie kaskadieren, an Zahl zunehmen oder die Zweige länger als die Höhe Ihres Bildschirms werden (an diesem Punkt werden sie unsichtbar). Wenn Sie eine zusätzliche Einrückungsstufe haben, werden Sie gewarnt, dass Sie sich in einem Bereich befinden.
  • Es kann Ihnen helfen, die Logik zum Herausziehen zu identifizieren. In diesem Fall hätte der Code wie folgt umgeschrieben und trockener, debuggbarer und testbarer gemacht werden können:
val errorMessage = user.password == enteredPassword match {
  case true => "User is authenticated"
  case false => "Entered password is invalid"
}

println(errorMesssage)

Hier ist eine äquivalente if / else-Blockimplementierung:

var errorMessage = ""

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

println(errorMessage)

Ja, Sie können argumentieren, dass Sie für etwas so Einfaches wie eine boolesche Prüfung einen if-Ausdruck verwenden können. Dies ist hier jedoch nicht relevant und lässt sich nicht gut auf Bedingungen mit mehr als 2 Zweigen skalieren.

Wenn Ihr Hauptanliegen die Wartbarkeit oder Lesbarkeit ist, ist der Mustervergleich fantastisch und Sie sollten ihn auch für kleinere Dinge verwenden!

Kevin Li
quelle
5
Die Verwendung von if / else erfordert keine Mutation. Das Äquivalent des ternären Operators in Scala würde dies lösen: val errorMessage = if (user.password == enteredPassword) "User is authenticated" else "Entered password is invalid"
Jean-Marc S.
Ich habe dies in meinem ursprünglichen Kommentar angesprochen: "Ja, Sie können argumentieren, dass Sie für etwas so Einfaches wie eine boolesche Prüfung einen if-Ausdruck verwenden können. Aber das ist hier nicht relevant und lässt sich nicht gut auf Bedingungen mit mehr als 2 Zweigen skalieren. ""
Kevin Li
Sie haben geschrieben "Während das Schreiben mit if / else eine Mutation erforderlich gemacht hätte". Das ist immer noch falsch. Sie benötigen keine Mutation für if / else, solange alle Zweige vom gleichen Typ sind. Beispiel:val k = if (false) "1" else if (false) "2" else "3"
Jean-Marc S.
Ich hatte gehofft, dass der zuvor erwähnte Kommentar die von Ihnen zitierte Zeile abgelöst hätte - eine if-expression / ternary-Anweisung ist nicht dasselbe wie ein if / else-Block (und daher für die ursprüngliche Frage nicht relevant) und auch nicht lesbar auf mehr als 2 Zweige skalieren. Es ist möglich, einen if / else-Block in geschweifte Klammern zu setzen und diesen Wert zu verwenden, aber ich glaube nicht, dass dies idiomatisch ist. Auf jeden Fall habe ich meine Antwort aktualisiert, um Verwirrung zu beseitigen.
Kevin Li
1
Verwechseln Sie Ihre Meinung nicht mit Fakten. If '/ else skaliert gut für mehrere Zweige. Musterübereinstimmung eignet sich auch hervorragend für ... passende Muster.
Jean-Marc S.
2

Ich bin auf dieselbe Frage gestoßen und hatte Tests geschrieben:

     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 timeMatch: Long = 1092

Andrey
quelle
Diese Art von Benchmarking ist ehrlich gesagt schrecklich. Erstens System.currentTimeMillis()hat schreckliche Präzision; System.nanoTimeist in der Regel besser. Trotzdem sollten Sie die Auswirkungen der JIT-Kompilierung, der Speicherbereinigung usw. entfernen. Verwenden Sie am besten ein Mikro-Benchmarking-Tool (wie ScalaMeter , um beide Ansätze richtig zu bewerten).
Mike Allen
@ MikeAllen yeah Kumpel, es ist lange her) Ich bin mit Ihnen auf Mikro-Benchmarking-Tools
Andrey
Hahaha! Meinetwegen. ;-)
Mike Allen
1

Ich bin hier, um eine andere Meinung zu vertreten: Für das spezifische Beispiel, das Sie anbieten, ist der zweite (wenn ... sonst ...) Stil tatsächlich besser, weil er viel einfacher zu lesen ist.

Wenn Sie Ihr erstes Beispiel in IntelliJ einfügen, wird empfohlen, zum zweiten (wenn ... sonst ...) Stil zu wechseln. Hier ist der Vorschlag für den IntelliJ-Stil:

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 {
      ???
    }
Yang
quelle
1

Es ist 2020, der Scala-Compiler generiert im Pattern-Matching-Fall einen weitaus effizienteren Bytecode. Die Leistungskommentare in der akzeptierten Antwort sind im Jahr 2020 irreführend.

Der durch Musterübereinstimmung generierte Bytecode stellt eine harte Konkurrenz zu if-else-Gewinnern dar, die zuweilen viel bessere und konsistentere Ergebnisse erzielen.

Man kann Pattern Match oder If-else verwenden, je nach Situation und Einfachheit. Der Mustervergleich hat jedoch eine schlechte Leistung. Die Schlussfolgerung ist nicht mehr gültig.

Sie können das folgende Snippet ausprobieren und die Ergebnisse anzeigen:

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: Da die Zahlen nanospezifisch sind, stimmen die Ergebnisse möglicherweise nicht genau mit den exakten Zahlen überein, aber das Argument zur Leistung gilt.

Gana
quelle
Wissen Sie, welche Scala-Version diese Verbesserungen vorgenommen hat? Leider schreiben einige von ihnen 2020 keinen Code, da wir bei Spark- und 2.11-Code stecken bleiben. Hoffentlich erreichen wir in nicht allzu ferner Zukunft 2.12!
Nick
0

In meiner Umgebung (Scala 2.12 und Java 8) erhalte ich unterschiedliche Ergebnisse. Match schneidet im obigen Code durchweg besser ab:

timeIf: Long = 249 timeMatch: Long = 68

Frank Eggink
quelle