Странное поведение компилятора Scala при инициализации класса с ленивым аргументом

Как возможно, что первый является правильным кодом Scala, а второй даже не скомпилируется?

Тот, который компилируется

object First {
  class ABC(body: => Unit) {
    val a = 1
    val b = 2
    println(body)
  }

  def main(args: Array[String]): Unit = {
    val x = new ABC {
      a + b
    }
  }
}

Этот не компилируется на Scala 2.11 и 2.12.

object Second {
  class ABC(body: => Int) {
    val a = 1
    val b = 2
    println(body)
  }

  def main(args: Array[String]): Unit = {
    val x = new ABC {
      a + b
    }
  }
}

person tartakynov    schedule 28.08.2017    source источник


Ответы (2)


Это совсем не странно. Давайте посмотрим на первый пример:

Вы объявляете свой класс ABC для получения параметра передачи по имени, который возвращает Unit, и вы думаете, что этот фрагмент:

   val x = new ABC {
      a + b
    }

передает этот параметр body, нет. На самом деле происходит следующее:

val x = new ABC(()) { a + b }

Если вы запустите этот код, вы увидите, что println(body) prints (), потому что вы не передаете значение для вашего параметра body, компилятор разрешает его компилировать, потому что, как утверждает scaladoc, есть только 1 значение введите Unit:

Unit является подтипом scala.AnyVal. Существует только одно значение типа Unit (()), и оно не представлено никаким объектом в базовой системе выполнения. Метод с возвращаемым типом Unit аналогичен методу Java, объявленному пустым.

Поскольку есть только одно значение, компилятор позволяет вам опустить его, и оно заполнит пробел. Этого не происходит с одноэлементными объектами, потому что они не расширяют AnyVal. Просто значение по умолчанию для Int равно 0, значение по умолчанию для Unit равно (), и поскольку доступно только это значение, компилятор принимает его.

Из документации:

Если ee имеет некоторый тип значения и ожидаемый тип — Unit, ee преобразуется в ожидаемый тип путем встраивания его в терм { ee; () }.

Объекты-одиночки не расширяют AnyVal, поэтому с ними обращаются по-разному.

Когда вы используете такой синтаксис, как:

new ABC {
 // Here comes code that gets executed after the constructor code. 
 // Code here can returns Unit by default because a constructor always 
 // returns the type it is constructing. 
}

Вы просто добавляете что-то в тело конструктора, вы не передаете параметры.

Второй пример не компилируется, потому что компилятор не может вывести значение по умолчанию для body: => Int, поэтому вы должны передать его явно.

Вывод

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

person pedromss    schedule 28.08.2017
comment
new X вызывает конструктор X. Я отредактирую свой ответ относительно new ABC(null) - person pedromss; 28.08.2017
comment
Посмотри, стало ли теперь яснее. Я не должен был говорить new ABC(null) - person pedromss; 28.08.2017
comment
Отличное исправление нуля - теперь (()) имеет смысл для меня. Я все еще думаю, что объяснение о возврате типа, который он создает, вводит в заблуждение. Тело функции-конструктора никогда не должно возвращать ничего, т. е. всегда ожидается, что оно вернет Unit, и поэтому может возвращать что угодно, независимо от того, какой тип вы создаете. - person Suma; 28.08.2017
comment
Это тоже не совсем правильно: поскольку есть только одно значение, компилятор позволяет вам его опустить, и оно заполнит пробел. Вы можете попробовать разницу между f(a: => Unit) и f(a: => Unit, b => Unit) - во втором случае компилятор не заполнит пробелы. . - person Suma; 28.08.2017
comment
То же самое относится и к компилятору, который не может вывести значение по умолчанию для тела - я не думаю, что он даже пытается это сделать. Я думаю, что причина, по которой new A эквивалентен new A(())), другая (признаюсь, я не знаю, в чем причина). - person Suma; 28.08.2017
comment
@Suma с двумя параметрами не работает. точно так же, если у вас есть метод с 0 параметрами, вы можете использовать m1 вместо m1(), но если ваш метод принимает 2 параметра, вы должны явно передать их. Также конструктор — это метод, который возвращает экземпляр чего-либо. Он не возвращает Unit. Scala позволяет передать выражение, возвращающее Unit. Вы не можете сделать вывод, что результатом этого выражения является возвращаемый тип конструктора. - person pedromss; 28.08.2017
comment
Я думаю, что ваше описание эквивалентности new A и new A(()) верно. Я согласен, что оно отвечает на вопрос. Однако я думаю, что объяснение, почему это принято (заполните пробел единственным возможным значением, не может вывести значение по умолчанию для тела), вводит в заблуждение. Есть и другие типы только с одним возможным значением — одноэлементные (объектные) типы, и вы не увидите с ними такого же поведения: object A;class ABC(a: A.type) .... - person Suma; 28.08.2017
comment
Мне жаль не соглашаться. Можете ли вы предоставить некоторую ссылку, подтверждающую, что компилятор понятий пытается найти значения по умолчанию для параметров, полученных из AnyVal? - person Suma; 28.08.2017
comment
Поведение каким-то образом специфично для new вызовов. Следующее НЕ будет компилироваться: def f(a: => Unit) = {};f - person Suma; 28.08.2017
comment
Не надо извиняться, здоровый аргумент. Здесь: scala-lang.org/files/ archive/spec/2.11/ Если ee имеет некоторый тип значения, а ожидаемый тип — Unit, ee преобразуется в ожидаемый тип путем встраивания его в терм { ee; () }. - Всякий раз, когда ожидается тип Unit, компилятор вставляет () - person pedromss; 28.08.2017
comment
Спасибо за отличный ответ! - person tartakynov; 28.08.2017

Вы не можете передать один аргумент конструктору в фигурных скобках, потому что это будет проанализировано как определение анонимного класса. Если вы хотите сделать это, вам нужно также заключить фигурные скобки в обычные скобки, например:

new ABC({
  a + b
})

Что касается того, почему компилятор принимает new ABC {a + b}, объяснение немного запутанное и неожиданное:

  1. new ABC {...} эквивалентно new ABC() {...}
  2. #P3#
    def f(a: Unit) = {}
    f()
    
    def g(a: (Int, Int)) = {}
    g(0,1)
    
    #P4# <цитата> #P5# #P6#
person Suma    schedule 29.08.2017