Оптимизация использования класса case в качестве символов
Я работаю с Java API, который передает идентификаторы в виде строк. Мне кажется, немного лучше использовать для этого печатные символы, поэтому я написал это:
object Helpers {
implicit def actionToString(action: Action): String =
action.getClass.getName.stripSuffix("$").replaceAll(".*\\$", "")
object Action{
def apply(name: String): Action = {
Class.forName("tutorial.HelloInput$Helpers$" + name).newInstance()
.asInstanceOf[Action]
}
}
sealed abstract class Action {
override def toString: String = actionToString(this)
}
final case class Rotate() extends Action
final case class Right() extends Action
final case class Left() extends Action
final case class Pause() extends Action
}
Это позволяет "сериализовать" и "десериализовать" строки и действия естественным образом, например, я могу сопоставить паттерн на Action(Pause)
но я тоже могу пройти Pause()
в библиотеку Java, которая ожидает строку благодаря неявному преобразованию.
Есть ли лучший способ сделать это, особенно с точки зрения производительности? Есть ли какие-либо проблемы с этим методом, которые могут вернуться, чтобы укусить меня позже?
Я немного читал о типах фантомов в Dotty и удивлялся, могут ли они быть использованы для повышения производительности при работе с символами (или, возможно, новые Enums будут лучшей альтернативой).
2 ответа
Неявные обращения имеют привычку возвращаться, чтобы кусать вас. В этом случае это означает, что вы можете использовать один из ваших типов действий в любом методе, где требуется строка, а не только там, где вы хотите. Это также не мешает вам просто передать какую-то другую строку в библиотеку.
Вместо того, чтобы использовать преобразование для непосредственного взаимодействия с библиотекой Java, я бы создал оболочку вокруг нее, которая принимает и возвращает ваши типы действий. Таким образом, вы получите хороший набранный API и не нужно никаких преобразований.
Поскольку ваши классы не имеют параметров, имеет смысл использовать объекты case, поэтому вам нужен только один экземпляр для каждого типа действия вместо того, чтобы каждый раз создавать новый.
Использование рефлексии касается и меня. При создании действия из строки может быть реализовано использование сопоставления с шаблоном, а преобразование в строку может быть выполнено, если каждый тип определяет его строковое значение, или даже просто использует productPrefix, если имя класса всегда совпадает с нужной вам строкой (хотя я предпочел бы определить это явно). Я подозреваю, что этот метод будет быстрее, но вам нужно будет проверить его, чтобы быть уверенным.
Каковы преимущества вашего подхода по сравнению со следующим:?
object Helpers {
type Action = String
val Rotate: Action = "Rotate"
val Right: Action = "Right"
val Left: Action = "Left"
val Pause: Action = "Pause"
}
Вы можете обменять шаблон на безопасность типов (без потери производительности), используя класс значений:
object Helpers {
// This won't allocate anything more than the previous solution!
final case class Action(name: String) extends AnyVal
val Rotate: Action = Action("Rotate")
val Right: Action = Action("Right")
val Left: Action = Action("Left")
val Pause: Action = Action("Pause")
}
Я думаю, что оба вышеупомянутых подхода более надежны, чем использование отражения + неявного преобразования. Например, ваше решение будет молча ломаться при перемещении кода или переименовании tutorial
пакет.
Есть ли какие-либо проблемы с этим методом, которые могут вернуться, чтобы укусить меня позже?
Неявное обращение - это дьявол. Я настоятельно советую вам никогда не учитывать их в своем дизайне, так легко "получить что-то, что работает", и сожалеть об этом неделю или две спустя... Тот факт, что с вашим решением следующие компиляции, IMO, будет неприемлемым:
val myAction: Action = ...
myAction.foreach(...)
myAction.map(...)
Я немного читал о типах фантомов в Dotty и удивлялся, могут ли они быть использованы для повышения производительности при работе с символами (или, возможно, новые Enums будут лучшей альтернативой).
Я думаю, что в этом случае могут быть полезны объединенные типы + литеральные одноэлементные типы, вы можете определить следующий псевдоним типа:
type Action = "Rotate" | "Right" | "Left" | "Pause"
Тогда, если метод должен вызываться только с подмножеством этих типов, вы очень точно поместите это в его API! (За исключением того, что у нас не может быть буквальных одноэлементных типов или типов, но я уверен, что они будут поддерживаться в какой-то момент, см. Эту проблему). Enum - это всего лишь синтаксис того, что вы уже можете делать с классами sealed trait + case, они не должны помочь ни с чем, кроме как "сохранить несколько нажатий клавиш".