¿Cuál es la ventaja de utilizar clases abstractas en lugar de rasgos?
¿Cuál es la ventaja de utilizar 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.
Puedo pensar en dos diferencias
- Las clases abstractas pueden tener parámetros de constructor así como parámetros de tipo. Los rasgos solo pueden tener parámetros de tipo. Hubo cierta discusión sobre que en el futuro incluso los rasgos pueden tener parámetros de constructor.
- Las clases abstractas son totalmente interoperables con Java. Puede llamarlos desde código Java sin envoltorios. Los rasgos son totalmente interoperables sólo si no contienen ningún código de implementación.
Hay una sección en Programación en Scala llamada "¿Trazar o no trazar?" que aborda esta cuestión. Dado que la primera edición está disponible en línea, espero que esté bien citarlo todo aquí. (Cualquier programador serio de Scala debería comprar el libro):
Siempre que implementes una colección de comportamiento reutilizable, tendrás que decidir si quieres usar un rasgo o una clase abstracta. No existe una regla firme, pero esta sección contiene algunas pautas a considerar.
Si el comportamiento no se va a reutilizar , conviértalo en una clase concreta. Después de todo, no es un comportamiento reutilizable.
Si puede reutilizarse en varias clases no relacionadas , conviértalo en un rasgo. Sólo los rasgos pueden mezclarse en diferentes partes de la jerarquía de clases.
Si desea heredar de él en código Java , utilice una clase abstracta. Dado que los rasgos con código no tienen un análogo cercano de Java, tiende a resultar incómodo heredar de un rasgo en una clase de Java. Mientras tanto, heredar de una clase Scala es exactamente como heredar de una clase Java. Como excepción, un rasgo de 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 que hereden de él, puede inclinarse por utilizar una clase abstracta. El problema es que cuando un rasgo gana o pierde un miembro, todas las clases que heredan de él deben recompilarse, incluso si no han cambiado. Si los clientes externos solo recurren al comportamiento, en lugar de heredarlo, entonces usar un rasgo está bien.
Si la eficiencia es muy importante , inclínate por utilizar 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 interfaces y, por lo tanto, pueden generar una ligera sobrecarga de rendimiento. Sin embargo, debe tomar esta decisión sólo si sabe que el rasgo en cuestión constituye un cuello de botella en el rendimiento y tiene evidencia de que usar una clase realmente resuelve el problema.
Si aún no lo sabes , después de considerar lo anterior, comienza por convertirlo en un rasgo. Siempre puedes cambiarlo más tarde y, en general, el uso de un rasgo mantiene abiertas más opciones.
Como mencionó @Mushtaq Ahmed, un rasgo no puede tener ningún parámetro pasado al constructor principal de una clase.
Otra diferencia es el tratamiento de super
.
La otra diferencia entre clases y rasgos es que mientras que en las clases
super
las llamadas están vinculadas estáticamente, en los rasgos están vinculadas dinámicamente. Si escribesuper.toString
en una clase, sabrá exactamente qué implementación de método se invocará. Sin embargo, cuando escribes lo mismo en un rasgo, la implementación del método a invocar para la superllamada no está definida cuando defines el rasgo.
Consulte el resto del Capítulo 12 para obtener más detalles.
Edición 1 (2013):
Existe una diferencia sutil en la forma en que se comportan las clases abstractas en comparación con los rasgos. Una de las reglas de linealización es que preserva la jerarquía de herencia de las clases, lo que tiende a empujar a las clases abstractas más adelante en la cadena, mientras que los rasgos se pueden mezclar felizmente. En ciertas circunstancias, en realidad es preferible estar en la última posición de la linealización de clases. , por lo que se podrían usar clases abstractas para eso. Consulte Restringir la linealización de clases (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 la versión 2.12, agregar o eliminar un miembro al rasgo requería la recompilación de todas las clases que heredaban el rasgo, incluso si las clases no habían 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 realiza alguna de las siguientes acciones, sus subclases aún requieren recompilación:
- definir campos (
val
ovar
, pero una constante está bien,final val
sin tipo de resultado)- vocación
super
- declaraciones inicializadoras en el cuerpo
- extendiendo una clase
- confiar en la linealización para encontrar implementaciones en el superrasgo correcto
Pero si el rasgo no es así, ahora puedes actualizarlo sin romper la compatibilidad binaria.