Подтвердить что ты не робот

Что делает @elidable аннотация в Scala, и когда я должен ее использовать?

Я заметил в некотором коде библиотеки scala, особенно Predef, есть код вроде:

/** Tests an expression, throwing an `AssertionError` if false.
*  Calls to this method will not be generated if `-Xelide-below`
*  is at least `ASSERTION`.
*
*  @see elidable
*  @param p   the expression to test
*/
@elidable(ASSERTION)
def assert(assertion: Boolean) {
if (!assertion)
  throw new java.lang.AssertionError("assertion failed")
}

Эта аннотация позволяет мне во время компиляции исключить код. Когда я компилирую с помощью -Xelide-below MAXIMUM, делает ли это

  • удалить метод и все его вызовы? (Если это так, что произойдет, если другая библиотека ожидает, что этот метод будет там?), Получаем ли мы NoSuchMethodError или что-то еще?
  • оставьте метод там, но удалите весь код из метода, оставив пустой метод?
  • просто удалите вызовы метода, но оставьте там метод?

Можно ли использовать его для уменьшения скомпилированного размера класса? Поэтому, если бы у меня было:

class Foobar {
    // extremely expensive toString method for debugging purposes
    @elidable(FINE) def toString(): String = "xxx"
}

и скомпилирован с помощью -Xelide-below WARNING, будет ли toString в этом классе вообще исчезнуть? Обратите внимание, что в этом примере я хотел бы, чтобы метод был удален из класса, потому что я не хотел бы, чтобы он вызывался.

Вторая часть: я видел он предложил, что это будет использоваться для устранения кода регистрации отладки. Учитывая, что большинство фреймворков (log4j заметно) позволяют установить уровень ведения журнала во время выполнения, я не думаю, что это хороший вариант использования. Лично я хотел бы, чтобы этот код поддерживался. Итак, помимо методов assert() в Predef, что является хорошим вариантом использования для @elidable?

4b9b3361

Ответ 1

Короткий ответ

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

Обратите внимание, что современные рамки ведения журналов стараются максимально уменьшить этот размер (например, Logback оптимизирует вызовы is*Enabled() и SLF4S передает сообщение по имени, чтобы избежать ненужных конкатенаций строк).

Длинный один

Мой тестовый код:

import scala.annotation.elidable
import scala.annotation.elidable._

class Foobar {
    info()
    warning()

    @elidable(INFO) def info() {println("INFO")}
    @elidable(WARNING) def warning() {println("WARNING")}
}

Доказывается, что при -Xelide-below 800 оба оператора печатаются, а при 900 появляется только "WARNING". Итак, что происходит под капотом?

$ scalac -Xelide-below 800 Foobar.scala && javap -c Foobar

public class Foobar extends java.lang.Object implements scala.ScalaObject{
public void info();
//...

public void warning();
//...

public Foobar();
  Code:
   0:   aload_0
   1:   invokespecial   #26; //Method java/lang/Object."<init>":()V
   4:   aload_0
   5:   invokevirtual   #30; //Method info:()V
   8:   aload_0
   9:   invokevirtual   #32; //Method warning:()V
   12:  return
}

Как вы можете видеть, это компилируется нормально. Однако, когда эта инструкция используется:

$ scalac -Xelide-below 900 Foobar.scala && javap -c Foobar

вызывает info(), и сам метод исчезает из байт-кода:

public class Foobar extends java.lang.Object implements scala.ScalaObject{
public void warning();
//...

public Foobar();
  Code:
   0:   aload_0
   1:   invokespecial   #23; //Method java/lang/Object."<init>":()V
   4:   aload_0
   5:   invokevirtual   #27; //Method warning:()V
   8:   return

}

Я ожидал бы, что NoSuchMethodError будет запущен во время выполнения, когда удаленный метод вызывается из кода клиента, скомпилированного с версией Foobar с нижним порогом elide-below. Также он пахнет хорошим старым препроцессором C, и, как таковой, я подумал бы дважды, прежде чем использовать @elidable.

Ответ 2

В качестве дополнения к Томаш Нуркевич ответьте на два комментария.

(1) Стиль С++

Поскольку я пришел из С++, я определил

/** ''Switch'' between '''Debug''' and '''Release''' version. */
object BuildLevel {
  type only = annotation.elidable
  final val DEBUG = annotation.elidable.INFO
}

и используйте это в старом стиле препроцессора С++, например

import BuildLevel._
@only(DEBUG)
private def checkExpensive(...) {
  ...
}

override def compare(that: ): Int = {
  checkExpensive(...)
  ...
}

для отметки дорогих проверок (проверка предварительных условий или инвариантов, которые должны всегда выполняться), которые я хочу отключить в сборках релизов.

Конечно, это похоже на пример использования, за исключением разницы рефакторинга дорогого кода в отдельном методе, который должен быть отключен в целом. Но все это стоит только для действительно дорогостоящих проверок. В проекте 10k строк у меня есть только 3 отмеченных чека. Более дешевые тесты я бы не выключил и не оставил в коде, потому что они повышают его надежность.

(2) Подпись блока

Этот подход подходит только для методов с сигнатурой (...) => Unit. Если вы используете результат такого отключенного метода, например

@only(DEBUG)
def checkExpensive(that: Any): Int = {
  4
}
val n = checkExpensive(this)

по крайней мере мой Scala 2.9.1.final компилятор сбой. Однако в такой подписи нет особого смысла. Потому что: Какое значение должно возвращать такой отключенный метод?

Ответ 3

На самом деле выражения не могут просто исчезнуть, потому что они имеют результат. Когда вы отказываетесь от вызова метода типа результата Boolean, вы заканчиваете с false и так далее.

Был вопрос через несколько месяцев после того, как этот вопрос был опубликован, чтобы решить, что происходит. Ничего не происходит. Результат заключался в том, чтобы достигнуть ???.