Статьи

10 подводных камней программирования Scala

Scala отлично подходит для сильно масштабируемых компонентных приложений, поддерживающих параллелизм и распространение. Он использует аспекты объектно-ориентированного и функционального программирования. Этот язык на основе JVM приобрел большую часть своего влияния, когда было объявлено, что его использует Twitter. При правильном использовании Scala может значительно сократить код, необходимый для приложений.

Для программиста Scala DZone собрал эти распространенные ошибки при написании кода. Эти советы приходят от Даниэля Собрала , энтузиаста Scala, который руководил проектами по разработке программного обеспечения на Java и участвовал в проекте FreeBSD .

1. Синтаксическая ошибка

Думая о «доходности», как о «возвращении». Люди попробуют:

for(i <- 0 to 10) {  if (i % 2 == 0)    yield i  else    yield -i}

Где правильное выражение:

for(i <- 0 to 10) yield {  if (i % 2 == 0)    i  else    -i}

 

2. Неправильное использование и синтаксическая ошибка

Использование scala.xml.XML.loadXXX для всего. Этот парсер будет пытаться получить доступ к внешним DTD, комментариям к полосам и другим подобным вещам. В scala.xml.parsing.ConstructingParser.fromXXX есть альтернативный парсер .

Кроме того, забывая пробелы вокруг знака равенства при обработке XML. Это:

val xml=<root/>

действительно означает:

val xml.$equal$less(root).$slash$greater

Это происходит потому, что операторы довольно произвольны, и Scala использует тот факт, что буквенно-цифровой символ должен отделяться от не буквенно-цифровых символов подчеркиванием в допустимом идентификаторе, чтобы можно было принимать выражения, такие как «x + y», не предполагая, что это одиночный идентификатор. Обратите внимание, что «x_ +» является допустимым идентификатором.

Итак, способ написать это назначение:

val xml = <root/>

3. Неправильная ошибка

Использование черты приложения для чего угодно, кроме самого тривиального использования. Проблема с:

object MyScalaApp extends Application {    // ... body ...}

является то, что тело выполняется внутри инициализации одиночного объекта. Во-первых, выполнение инициализации синглетонов синхронизировано, поэтому вся ваша программа не может взаимодействовать с другими потоками. Во-вторых, JIT не будет оптимизировать его, что сделает вашу программу медленнее, чем необходимо.

Кстати, отсутствие взаимодействия с другими потоками означает, что вы можете забыть о тестировании GUI или Actors with Application .


4. Неправильная ошибка

Попытка сопоставить регулярное выражение со строкой, предполагая, что регулярное выражение не ограничено:

val r = """(\d+)""".rval s = "--> 5 <---"s match {  case r(n) => println("This won't match")  case _ => println("This will")}

Проблема здесь в том, что при сопоставлении с образцом регулярное выражение Scala действует так, как если бы оно было ограничено, начинаются и заканчиваются на «^» и «$». Способ получить эту работу:

val r = """(\d+)""".rval s = "--> 5 <---"r findFirstIn s match {  case Some(n) => println("Matches 5 to "+n)  case _ => println("Won't match")}

Или просто убедитесь, что шаблон будет соответствовать любому префиксу и суффиксу:

val r = """.*(\d+).*""".rval s = "--> 5 <---"s match {  case r(n) => println("This will match the first group of r, "+n+", to 5")  case _ => println("Won't match")}


5. Неправильная ошибка

Думая о var и val как о полях.

Scala обеспечивает соблюдение принципа унифицированного доступа , делая невозможным непосредственное обращение к полю. Все обращения к любому полю осуществляются через геттеры и сеттеры. Что на самом деле делают val и var, так это определяют поле, получатель для этого поля и, для var , установщик для этого поля.

Java-программисты часто воспринимают определения var и val как поля и удивляются, когда обнаруживают, что они используют то же пространство имен, что и их методы, поэтому они не могут повторно использовать свои имена. То, что разделяет одно и то же пространство имен, — это автоматически определенные методы получения и установки, а не поле. Затем они много раз пытаются найти способ добраться до поля, чтобы обойти это ограничение — но это невозможно, иначе будет нарушен принцип единого доступа.

Другое следствие этого заключается в том, что при создании подклассов val может переопределять def . Обратный путь невозможен, потому что val добавляет гарантию неизменности, а def — нет.

Нет никаких указаний о том, что называть частными получателями и установщиками, когда вам необходимо переопределить его по какой-либо причине. Компилятор Scala и код библиотеки часто используют псевдонимы и аббревиатуру для частных значений, в отличие от полностьюCamelNamingConventions для открытых методов получения и установки. Другие предложения включают переименование, синглтоны внутри экземпляра или даже создание подклассов. Примеры этих предложений:

Переименование

class User(val name: String, initialPassword: String) {  private lazy var encryptedPassword = encrypt(initialPassword, salt)  private lazy var salt = scala.util.Random.nextInt  private def encrypt(plainText: String, salt: Int): String = { ... }  private def decrypt(encryptedText: String, salt: Int): String = { ... }  def password = decrypt(encryptedPassword, salt)  def password_=(newPassword: String) = encrypt(newPassword, salt)}

одиночка

class User(initialName: String, initialPassword: String) {   private object fields {     var name: String = initialName;     var password: String = initialPassword;   }   def name = fields.name   def name_=(newName: String) = fields.name = newName   def password = fields.password   def password_=(newPassword: String) = fields.password = newPassword }

альтернативно, с классом case, который автоматически определит методы для равенства, hashCode и т. д., которые затем могут быть использованы повторно:

class User(name0: String, password0: String) {  private case class Fields(var name: String, var password0: String)  private object fields extends Fields(name0, password0)  def name = fields.name  def name_=(newName: String) = fields.name = newName  def password = fields.password  def password_=(newPassword: String) = fields.password = newPassword}

подклассов

case class Customer(name: String)class ValidatingCustomer(name0: String) extends Customer(name0) {  require(name0.length < 5)  def name_=(newName : String) =    if (newName.length < 5) error("too short")    else super.name_=(newName)}val cust = new ValidatingCustomer("xyz123")

6. Неправильная ошибка

Забыть о стирании типов . Когда вы объявляете класс C [A], признак T [A] или функцию или метод m [A], A не присутствует во время выполнения . Это означает, например, что любой параметр типа будет фактически скомпилирован как AnyRef , даже если компилятор гарантирует, что время компиляции будет уважать типы.

Это также означает, что вы не можете использовать параметр типа A во время компиляции. Например, это не будет работать:

def checkList[A](l: List[A]) = l match {  case _ : List[Int] => println("List of Ints")  case _ : List[String] => println("List of Strings")  case _ => println("Something else")}

Во время выполнения передаваемый список не имеет параметра типа. Кроме того, List [Int] и List [String] оба станут List [_] , поэтому будет вызываться только первый случай.

Вы можете обойти это, в некоторой степени, используя экспериментальную функцию Manifest , например:

def checkList[A](l: List[A])(implicit m: scala.reflect.Manifest[A]) = m.toString match {  case "int" => println("List of Ints")  case "java.lang.String" => println("List of Strings")  case _ => println("Something else")}

7. Ошибка проектирования

Неосторожное использование следствий. Последствия могут быть очень сильными, но следует позаботиться о том, чтобы не использовать неявные параметры общих типов или неявные преобразования между общими типами.

Например, создание неявного, такого как:

implicit def string2Int(s: String): Int = s.toInt

это очень плохая идея, потому что кто-то может по ошибке использовать строку вместо Int. В тех случаях, когда для этого есть применение, просто лучше использовать класс:

case class Age(n: Int)implicit def string2Age(s: String) = Age(s.toInt)implicit def int2Age(n: Int) = new Age(n)implicit def age2Int(a: Age) = a.n

Это позволит вам свободно комбинировать Age с String или Int, но никогда не String с Int.

Аналогично, при использовании неявных параметров, никогда не делайте что-то вроде:

case class Person(name: String)(implicit age: Int)

Это не только облегчит конфликты неявных параметров, но и может привести к тому, что неявный возраст будет незаметно передан чему-то, ожидающему неявный Int чего-то другого. Опять же, решение заключается в использовании определенных классов.

Еще одно проблемное неявное использование — довольный ими оператор. Вы можете подумать, что «~» является идеальным оператором для сопоставления строк, но другие могут использовать его для таких вещей, как эквивалентность матриц, конкатенация синтаксического анализатора и т. Д. Итак, если вы собираетесь их предоставлять, убедитесь, что легко выделить область их действия. использование.


8. Ошибка проектирования

Плохо проектируем методы равенства. В частности:

  • Попытка изменить «==» вместо «равно» (что дает вам «! =» Бесплатно).
  • Определяя это как
def equals(other: MyClass): Boolean

        вместо

override def equals(other: Any): Boolean
  • Также можно забыть переопределить hashCode , чтобы гарантировать, что если a == b, то a.hashCode == b.hashCode (обратное предложение не обязательно должно быть действительным).
  • Не делать его коммутативным: если a == b, то b == a . Особенно подумайте о подклассе — знает ли суперкласс, как сравнивать с подклассом, который он даже не знает, существует? Посмотрите canEquals, если это необходимо.
  • Не делая его транзитивным: если a == b и b == c, то a == c .

9. Ошибка использования

В Unix / Linux / * BSD, указав имя вашего хоста (в соответствии с именем хоста ), не объявляя его в файле hosts. В частности, следующая команда не будет работать:

ping `hostname`

В таких случаях ни fsc, ни scala не будут работать, хотя и будет работать scalac. Это потому, что fsc продолжает работать в фоновом режиме, слушая соединения через TCP-сокет, чтобы ускорить компиляцию, и scala использует это для ускорения выполнения скрипта.

10. Ошибка стиля

Используя пока . У него есть свои обычаи, но, в большинстве случаев, решение с более широким пониманием лучше.

Говоря о непонимании, использование их для генерации индексов — тоже плохая идея. Вместо того:

def matchingChars(string: String, characters: String) = {  var m = ""  for(i <- 0 until string.length)    if ((characters contains string(i)) && !(m contains string(i)))      m += string(i)  m}

Использование:

def matchingChars(string: String, characters: String) = {  var m = ""  for(c <- string)    if ((characters contains c) && !(m contains c))      m += c  m}

Если нужно вернуть индекс, можно использовать приведенный ниже шаблон вместо итерации по индексам. Может быть лучше применить поверх проекции (Scala 2.7) или вида (Scala 2.8), если производительность имеет значение.

def indicesOf(s: String, c: Char) = for {  (sc, index) <- s.zipWithIndex  if c == sc} yield index