¿Cuál es la ventaja de usar clases abstractas en lugar de rasgos?

371

¿Cuál es la ventaja de usar una clase abstracta en lugar de un rasgo (aparte del rendimiento)? Parece que las clases abstractas pueden ser reemplazadas por rasgos en la mayoría de los casos.

Ralf
fuente

Respuestas:

372

Se me ocurren dos diferencias

  1. Las clases abstractas pueden tener parámetros de constructor, así como parámetros de tipo. Los rasgos solo pueden tener parámetros de tipo. Se discutió que en el futuro incluso los rasgos pueden tener parámetros de constructor
  2. Las clases abstractas son totalmente interoperables con Java. Puede llamarlos desde código Java sin envoltorios. Los rasgos son completamente interoperables solo si no contienen ningún código de implementación
Mushtaq Ahmed
fuente
173
Anexo muy importante: una clase puede heredar de múltiples rasgos pero solo de una clase abstracta. Creo que esta debería ser la primera pregunta que hace un desarrollador al considerar cuál usar en casi todos los casos.
BAR
16
salvavidas: "Los rasgos son completamente interoperables solo si no contienen ningún código de implementación"
Walrus the Cat
2
abstracto: cuando los comportamientos colectivos definen o conducen a un objeto (rama de objeto) pero aún no están compuestos para ser como un objeto (listo). Rasgos, cuando necesita inducir capacidades, es decir, las capacidades nunca se originan con la creación del objeto, evoluciona o se requiere cuando un objeto sale del aislamiento y tiene que comunicarse.
Ramiz Uddin
55
La segunda diferencia no existe en Java8, piense.
Duong Nguyen
14
Según Scala 2.12, un rasgo se compila en una interfaz Java 8: scala-lang.org/news/2.12.0#traits-compile-to-interfaces .
Kevin Meredith
209

Hay una sección en Programación en Scala llamada "¿Rasgo o no rasgo?" que aborda esta pregunta. Como la primera edición está disponible en línea, espero que esté bien citar todo aquí. (Cualquier programador serio de Scala debería comprar el libro):

Cada vez que implemente una colección reutilizable de comportamiento, tendrá que decidir si desea usar un rasgo o una clase abstracta. No existe una regla firme, pero esta sección contiene algunas pautas a tener en cuenta.

Si el comportamiento no se reutilizará , conviértalo en una clase concreta. No es un comportamiento reutilizable después de todo.

Si se puede reutilizar en varias clases no relacionadas , conviértalo en un rasgo. Solo los rasgos se pueden mezclar en diferentes partes de la jerarquía de clases.

Si desea heredar de él en código Java , use una clase abstracta. Dado que los rasgos con código no tienen un análogo de Java cercano, tiende a ser incómodo heredar de un rasgo en una clase de Java. Heredar de una clase Scala, mientras tanto, es exactamente como heredar de una clase Java. Como una excepción, un rasgo Scala con solo miembros abstractos se traduce directamente a una interfaz Java, por lo que debe sentirse libre de definir dichos rasgos incluso si espera que el código Java herede de él. Consulte el Capítulo 29 para obtener más información sobre cómo trabajar con Java y Scala juntos.

Si planea distribuirlo en forma compilada y espera que grupos externos escriban clases heredadas de él, puede inclinarse hacia el uso de una clase abstracta. El problema es que cuando un rasgo gana o pierde un miembro, cualquier clase que herede de él debe volver a compilarse, incluso si no ha cambiado. Si los clientes externos solo recurrirán al comportamiento, en lugar de heredar de él, entonces usar un rasgo está bien.

Si la eficiencia es muy importante , inclínese hacia el uso de una clase. La mayoría de los tiempos de ejecución de Java hacen que la invocación de un método virtual de un miembro de la clase sea una operación más rápida que la invocación de un método de interfaz. Los rasgos se compilan en las interfaces y, por lo tanto, pueden generar una ligera sobrecarga de rendimiento. Sin embargo, debe hacer esta elección solo si sabe que el rasgo en cuestión constituye un cuello de botella de rendimiento y tiene evidencia de que el uso de una clase en realidad resuelve el problema.

Si aún no lo sabe , después de considerar lo anterior, comience haciéndolo como un rasgo. Siempre puede cambiarlo más tarde y, en general, usar un rasgo mantiene más opciones abiertas.

Como @Mushtaq Ahmed mencionó, un rasgo no puede tener ningún parámetro pasado al constructor primario de una clase.

Otra diferencia es el tratamiento de super.

La otra diferencia entre clases y rasgos es que, mientras que en las clases, las superllamadas están vinculadas estáticamente, en los rasgos, están vinculadas dinámicamente. Si escribe super.toStringen una clase, sabe exactamente qué implementación de método se invocará. Sin embargo, cuando escribe lo mismo en un rasgo, la implementación del método para invocar la súper llamada no está definida cuando define el rasgo.

Vea el resto del Capítulo 12 para más detalles.

Edición 1 (2013):

Hay una sutil diferencia en la forma en que las clases abstractas se comportan en comparación con los rasgos. Una de las reglas de linealización es que conserva la jerarquía de herencia de las clases, lo que tiende a impulsar las clases abstractas más adelante en la cadena, mientras que los rasgos pueden mezclarse felizmente. En ciertas circunstancias, es preferible estar en la última posición de la linealización de clases , por lo que podrían usarse clases abstractas para eso. Ver linealización de clase restrictiva (orden de mezcla) en Scala .

Edición 2 (2018):

A partir de Scala 2.12, el comportamiento de compatibilidad binaria del rasgo ha cambiado. Antes de 2.12, agregar o eliminar un miembro al rasgo requería la compilación de todas las clases que heredan el rasgo, incluso si las clases no han cambiado. Esto se debe a la forma en que se codificaron los rasgos en JVM.

A partir de Scala 2.12, los rasgos se compilan en interfaces Java , por lo que el requisito se ha relajado un poco. Si el rasgo hace algo de lo siguiente, sus subclases aún requieren recompilación:

  • definir campos ( valo var, pero una constante está bien, final valsin tipo de resultado)
  • vocación super
  • declaraciones de inicializador en el cuerpo
  • extendiendo una clase
  • confiando en la linealización para encontrar implementaciones en el supertrait correcto

Pero si el rasgo no lo hace, ahora puede actualizarlo sin romper la compatibilidad binaria.

Eugene Yokota
fuente
2
If outside clients will only call into the behavior, instead of inheriting from it, then using a trait is fine- ¿Alguien podría explicar cuál es la diferencia aquí? extendsvs with?
0fnt
2
@ 0fnt Su distinción no se trata de extender vs con. Lo que está diciendo es que si solo mezcla el rasgo dentro de la misma compilación, los problemas de compatibilidad binaria no se aplican. Sin embargo, si su API está diseñada para permitir a los usuarios mezclar el rasgo ellos mismos, entonces tendrá que preocuparse por la compatibilidad binaria.
John Colanduoni
2
@ 0fnt: No hay absolutamente ninguna diferencia semántica entre extendsy with. Es puramente sintáctico. Si hereda de varias plantillas, la primera obtiene extend, todas las demás obtienen with, eso es todo. Piense withcomo una coma: class Foo extends Bar, Baz, Qux.
Jörg W Mittag
77

Para lo que sea que valga la pena, la Programación en Scala de Odersky et al recomienda que, cuando dudes, uses rasgos. Siempre puede cambiarlos a clases abstractas más adelante si es necesario.

Daniel C. Sobral
fuente
20

Aparte del hecho de que no puede extender directamente múltiples clases abstractas, pero puede mezclar múltiples rasgos en una clase, vale la pena mencionar que los rasgos son apilables, ya que las súper llamadas en un rasgo están vinculadas dinámicamente (se refiere a una clase o rasgo mezclado antes) el actual).

De la respuesta de Thomas en Diferencia entre clase abstracta y rasgo :

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
Nemanja Boric
fuente
9

Al extender una clase abstracta, esto muestra que la subclase es de un tipo similar. Creo que este no es necesariamente el caso cuando se usan rasgos.

peter p
fuente
¿Tiene esto alguna implicación práctica, o solo hace que el código sea más fácil de entender?
Ralf
8

En Programming Scala, los autores dicen que las clases abstractas hacen una relación clásica "es-una" orientada a objetos, mientras que los rasgos son una forma de composición de escala.

Marta
fuente
5

Las clases abstractas pueden contener comportamiento: pueden parametrizarse con argumentos de constructor (que los rasgos no pueden) y representan una entidad de trabajo. En cambio, los rasgos solo representan una característica única, una interfaz de una funcionalidad.

Darío
fuente
8
Espero que no estés implicando que los rasgos no pueden contener el comportamiento. Ambos pueden contener código de implementación.
Mitch Blevins
1
@ Mitch Blevins: Por supuesto que no. Pueden contener código, pero cuando se define trait Enumerablecon muchas funciones auxiliares, no las llamaría comportamiento sino solo funcionalidad relacionada con una característica.
Dario el
44
@Dario Veo "comportamiento" y "funcionalidad" como sinónimos, por lo que su respuesta es muy confusa.
David J.
3
  1. Una clase puede heredar de múltiples rasgos pero solo de una clase abstracta.
  2. Las clases abstractas pueden tener parámetros de constructor, así como parámetros de tipo. Los rasgos solo pueden tener parámetros de tipo. Por ejemplo, no puede decir rasgo t (i: Int) {}; El parámetro i es ilegal.
  3. Las clases abstractas son totalmente interoperables con Java. Puede llamarlos desde código Java sin envoltorios. Los rasgos son completamente interoperables solo si no contienen ningún código de implementación.
pavan.vn101
fuente