В чем преимущество использования абстрактных классов вместо признаков?

371

В чем преимущество использования абстрактного класса вместо черты (помимо производительности)? Кажется, что абстрактные классы могут быть заменены чертами в большинстве случаев.

Ralf
источник

Ответы:

372

Я могу думать о двух различиях

  1. Абстрактные классы могут иметь параметры конструктора, а также параметры типа. Черты могут иметь только параметры типа. Было некоторое обсуждение, что в будущем даже черты могут иметь параметры конструктора
  2. Абстрактные классы полностью совместимы с Java. Вы можете вызывать их из кода Java без каких-либо оберток. Черты полностью совместимы, только если они не содержат кода реализации
Муштак Ахмед
источник
173
Очень важное дополнение: класс может наследовать от нескольких признаков, но только от одного абстрактного класса. Я думаю, что это должен быть первый вопрос, который задает разработчик при рассмотрении вопроса, который использовать почти во всех случаях.
BAR
16
lifesaver: «Черты полностью совместимы, только если они не содержат кода реализации»
Walrus the Cat
2
абстрактный - когда коллективное поведение определяет или ведет к объекту (ветви объекта), но еще не составлено как (готовый) объект. Черты, когда вам нужно индуцировать способности, то есть способности никогда не возникают при создании объекта, они развиваются или требуются, когда объект выходит из изоляции и должен взаимодействовать.
Рамиз Уддин
5
Второе отличие не существует в Java8, думаю.
Дуонг Нгуен
14
В Scala 2.12 черта компилируется в интерфейс Java 8 - scala-lang.org/news/2.12.0#traits-compile-to-interfaces .
Кевин Мередит
209

В Scala есть раздел «Программирование, черта или нет?» который решает этот вопрос. Так как 1-е издание доступно в Интернете, я надеюсь, что все в порядке, чтобы процитировать все это здесь. (Любой серьезный программист Scala должен купить книгу):

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

Если поведение не будет использоваться повторно , сделайте его конкретным классом. Это не многоразовое поведение в конце концов.

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

Если вы хотите наследовать от него в коде Java , используйте абстрактный класс. Поскольку признаки с кодом не имеют близкого аналога Java, обычно бывает неудобно наследовать признаки в классе Java. В то же время наследование от класса Scala точно такое же, как наследование от класса Java. В качестве одного исключения, черта Scala, содержащая только абстрактные элементы, транслируется непосредственно в интерфейс Java, поэтому вы можете свободно определять такие черты, даже если ожидаете, что код Java будет наследоваться от него. См. Главу 29 для получения дополнительной информации о совместной работе с Java и Scala.

Если вы планируете распространять его в скомпилированной форме и ожидаете, что внешние группы будут писать классы, унаследованные от него, вы можете склоняться к использованию абстрактного класса. Проблема в том, что когда признак получает или теряет член, любые классы, которые наследуют его, должны быть перекомпилированы, даже если они не изменились. Если внешние клиенты будут вызывать только поведение, а не наследовать его, тогда использовать черту вполне нормально.

Если эффективность очень важна , склоняйтесь к использованию класса. Большинство сред выполнения Java делают вызов виртуального метода членом класса более быстрой операцией, чем вызов метода интерфейса. Черты компилируются в интерфейсы и, следовательно, могут немного снизить производительность. Однако этот выбор следует делать только в том случае, если вы знаете, что рассматриваемая черта представляет собой узкое место в производительности, и у вас есть доказательства того, что использование класса вместо этого фактически решает проблему.

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

Как упоминал @Mushtaq Ahmed, в признаке не может быть никаких параметров, передаваемых первичному конструктору класса.

Другое отличие заключается в лечении super.

Другое различие между классами и признаками состоит в том, что, хотя в классах superвызовы статически связаны, в признаках они динамически связаны. Если вы пишете super.toStringв классе, вы точно знаете, какая реализация метода будет вызвана. Однако, когда вы пишете то же самое в признаке, реализация метода, вызываемая для супер-вызова, не определяется при определении признака.

См. Остальную часть главы 12 для более подробной информации.

Изменить 1 (2013):

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

Изменить 2 (2018):

Начиная с Scala 2.12, поведение бинарной совместимости черты изменилось. До 2.12 добавление или удаление члена для признака требовало перекомпиляции всех классов, которые наследуют признак, даже если классы не изменились. Это связано с тем, как черты кодировались в JVM.

Начиная с Scala 2.12, черты компилируются в интерфейсы Java , поэтому требования немного ослабли. Если признак выполняет одно из следующих действий, его подклассы все еще требуют перекомпиляции:

  • определение полей ( valили varконстанта в порядке - final valбез типа результата)
  • призвание super
  • оператор инициализатора в теле
  • продление класса
  • опираясь на линеаризацию, чтобы найти реализации в правильном супертрейте

Но если этого не происходит, вы можете обновить его, не нарушая бинарную совместимость.

Евгений Йокота
источник
2
If outside clients will only call into the behavior, instead of inheriting from it, then using a trait is fine- Может кто-нибудь объяснить, в чем здесь разница? extendsпротив with?
0
2
@ 0fnt Его отличие не в том, что расширяет, а в том, что касается. Он говорит, что если вы смешиваете черту в одной и той же компиляции, проблемы бинарной совместимости не применяются. Однако, если ваш API разработан так, чтобы пользователи могли сами смешивать черты, вам придется беспокоиться о двоичной совместимости.
Джон Коландуони
2
@ 0fnt: нет абсолютно никакой смысловой разницы между extendsи with. Это чисто синтаксический. Если вы наследуете от нескольких шаблонов, первый получает extend, все остальные получают with, вот и все. Думайте о withзапятой class Foo extends Bar, Baz, Qux.
Йорг Миттаг
77

Для чего бы это ни стоило, Odersky et al. Программирование в Scala рекомендует, когда вы сомневаетесь, использовать черты. Вы всегда можете изменить их на абстрактные классы позже, если это необходимо.

Даниэль С. Собрал
источник
20

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

Из ответа Томаса « Разница между абстрактным классом и чертой» :

trait A{
    def a = 1
}

trait X extends A{
    override def a = {
        println("X")
        super.a
    }
}  


trait Y extends A{
    override def a = {
        println("Y")
        super.a
    }
}

scala> val xy = new AnyRef with X with Y
xy: java.lang.Object with X with Y = $anon$1@6e9b6a
scala> xy.a
Y
X
res0: Int = 1

scala> val yx = new AnyRef with Y with X
yx: java.lang.Object with Y with X = $anon$1@188c838
scala> yx.a
X
Y
res1: Int = 1
Неманья Борич
источник
9

При расширении абстрактного класса это показывает, что подкласс имеет аналогичный вид. Я думаю, что это не обязательно так, когда используются черты.

Питер П
источник
Имеет ли это какое-либо практическое значение или только облегчает понимание кода?
Ральф
8

В Программировании Scala авторы говорят, что абстрактные классы создают классические объектно-ориентированные отношения «есть», в то время как черты - это scala-способ композиции.

Куница
источник
5

Абстрактные классы могут содержать поведение - они могут параметризоваться с помощью аргументов конструктора (что не может иметь признаков) и представлять работающий объект. Черты вместо этого просто представляют одну особенность, интерфейс одной функциональности.

Dario
источник
8
Надеюсь, вы не подразумеваете, что черты не могут содержать поведение. Оба могут содержать код реализации.
Митч Блевинс
1
@ Митч Блевинс: Конечно нет. Они могут содержать код, но когда вы определяете trait Enumerableс большим количеством вспомогательных функций, я бы не назвал их поведением, а просто функциональностью, связанной с одной функцией.
Дарио
4
@Dario Я считаю, что «поведение» и «функциональность» являются синонимами, поэтому я нахожу ваш ответ очень запутанным.
Дэвид Дж.
3
  1. Класс может наследовать от нескольких признаков, но только от одного абстрактного класса.
  2. Абстрактные классы могут иметь параметры конструктора, а также параметры типа. Черты могут иметь только параметры типа. Например, вы не можете сказать trait t (i: Int) {}; Параметр i недопустим.
  3. Абстрактные классы полностью совместимы с Java. Вы можете вызывать их из кода Java без каких-либо оберток. Черты полностью совместимы, только если они не содержат кода реализации.
pavan.vn101
источник