Зіставлення шаблону проти 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")

Чи рівні такі підходи? Чи є з них чомусь кращим за інший?

Відповіді:


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. Якщо чіткість коду значно покращилася за допомогою матчу, продовжуйте (за винятком тих рідкісних випадків, коли ви знаєте, що продуктивність є критичною, і тоді ви можете порівняти різницю).


3
Я просто відчуваю відповідність шаблону. Думаю, саме тому я намагаюся використовувати його скрізь :) Дякую, я буду слідувати вашим порадам.
Soteric

14
@Soteric Це загальний етап для програмістів Scala. Ви пройдете інші, гірші фази. :-)
Даніель К. Собрал

@Daniel Подобається мати підписи типу, що охоплюють кілька рядків?
ziggystar

10
@ DanielC.Sobral Я думаю, було б непогано скласти список тих фаз "не перестаратися" ...
парадигматичний

6
Ви можете розглянути різницю в розмірі байт-коду як помилку. Є надія, що компілятор Scala оптимізує відповідність шаблону, щоб бути настільки ж щільним, як if-else у майбутньому. Тоді це зводиться лише до читабельності, як і слід.
ebruchez

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/

Одним з недоліків є необхідність використання зворотних лапок для захисту введеної змінноїPassword від затемнення.

В основному, вам слід схилятися до того, щоб якомога більше уникати спілкування з логічними значеннями, оскільки вони не передають ніякої інформації на рівні типу.

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

11

Обидва твердження еквівалентні з точки зору семантики коду. Але може бути можливо, що компілятор створює більш складний (і, отже, неефективний) код в одному випадку (match ).

Зіставлення шаблонів зазвичай використовується для розбиття більш складних конструкцій, таких як поліморфні вирази або деконструкція ( unapplyвведення) об’єктів у їх компоненти. Я б не рада , щоб використовувати його в якості сурогату для простого , якщо-інакше заяви - немає нічого поганого , якщо- то ще .

Зверніть увагу, що ви можете використовувати його як вираз у 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. Але це тут не актуально і погано масштабується до умов з більш ніж 2 відділеннями.

Якщо ваша більша проблема полягає у ремонтопридатності чи читабельності, збіг шаблонів є неймовірним, і ви повинні використовувати його навіть для незначних речей!


5
Використання if / else не вимагає мутації. Еквівалент тернарного оператора в Скалі вирішив би це: val errorMessage = if (user.password == enteredPassword) "User is authenticated" else "Entered password is invalid"
Жан-Марк С.

Я звернувся до цього у своєму оригінальному коментарі: "Так, ви можете стверджувати, що для чогось такого простого, як логічна перевірка, ви можете використовувати вираз if. Але це тут не актуально і погано масштабується до умов з більш ніж 2 гілками. "
Kevin Li

Ви написали "Тоді як написання цього з використанням if / else вимагало б мутації". Це все ще неправильно. Вам не потрібна мутація для if / else, якщо всі гілки одного типу. Приклад:val k = if (false) "1" else if (false) "2" else "3"
Жан-Марк С.

Я сподівався, що коментар, про який я згадав раніше, замінив би цитований вами рядок - вираз if-expression / тернар не є таким самим, як блок if / else (і, отже, не стосується вихідного питання), і не читабельно масштабувати до більш ніж 2 гілок. Можна вкласти в дужки блок if / else і використовувати це значення, але я не вважаю, що це ідіоматично. У будь-якому випадку я оновив свою відповідь, щоб усунути плутанину.
Kevin Li

1
Не плутайте свою думку з фактами. Якщо '/ 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 timeMatch: Long = 1092


Цей тип порівняльних показників, відверто кажучи, жахливий. По-перше, System.currentTimeMillis()має жахливу точність; System.nanoTimeяк правило, краще. Незважаючи на це, слід усунути вплив компіляції JIT, збору сміття тощо. Найкраще використовувати інструмент мікро-тестування (наприклад, ScalaMeter, щоб правильно оцінити обидва підходи.
Майк Аллен,

@MikeAllen так, товариш, це було давно) Я з вами на інструментах мікро-тестування
Андрій

Ха-ха-ха! Досить справедливо. ;-)
Майк Аллен

2

Настав 2020 рік, компілятор Scala генерує набагато ефективніший байт-код у випадку зіставлення зразків. Коментарі до результатів у прийнятій відповіді вводять в оману в 2020 році.

Згенерований байт-код збігу шаблону дає жорстку конкуренцію, якщо-небудь інколи вигравання відповідності зразків дає набагато кращі та стабільніші результати.

Можна скористатися збігом зразків або 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 внесла ці вдосконалення? На жаль, деякі з нас не пишуть код у 2020 році, оскільки ми застрягли в коді Spark та 2.11. Сподіваємось, ми дійдемо до 2.12 у не надто далекому майбутньому!
Нік

1

Я тут, щоб запропонувати іншу думку: для конкретного прикладу, який ви пропонуєте, другий (якщо ... ще ...) стиль насправді кращий, оскільки його набагато легше читати.

Насправді, якщо ви вкладете свій перший приклад у IntelliJ, він запропонує вам перейти до другого (якщо ... ще ...) стилю. Ось пропозиція щодо стилю 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) я отримую різні результати. Match відповідає стабільно кращим результатам у наведеному вище коді:

timeIf: Long = 249 timeMatch: Long = 68

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.