Qual é a vantagem de usar classes abstratas em vez de características?

371

Qual é a vantagem de usar uma classe abstrata em vez de uma característica (além do desempenho)? Parece que as classes abstratas podem ser substituídas por características na maioria dos casos.

Ralf
fonte

Respostas:

371

Eu posso pensar em duas diferenças

  1. As classes abstratas podem ter parâmetros de construtor e parâmetros de tipo. As características podem ter apenas parâmetros de tipo. Houve uma discussão que, no futuro, até características podem ter parâmetros construtores
  2. As classes abstratas são totalmente interoperáveis ​​com Java. Você pode chamá-los do código Java sem nenhum wrapper. As características são totalmente interoperáveis ​​apenas se não contiverem nenhum código de implementação
Mushtaq Ahmed
fonte
172
Adendo muito importante: Uma classe pode herdar de várias características, mas apenas uma classe abstrata. Acho que essa deve ser a primeira pergunta que um desenvolvedor faz ao considerar qual usar em quase todos os casos.
BAR
15
lifesaver: "Os traços são totalmente interoperáveis ​​apenas se não contiverem nenhum código de implementação"
Walrus the Cat
2
abstract - quando comportamentos coletivos definem ou levam a um objeto (ramo do objeto), mas ainda não foram compostos para serem como objetos (prontos). Traços, quando você precisa induzir recursos, isto é, nunca se originam com a criação do objeto, ele evolui ou é necessário quando um objeto sai do isolamento e precisa se comunicar.
Ramiz Uddin
5
A segunda diferença não existe no Java8, pense.
Duong Nguyen
14
No Scala 2.12, uma característica é compilada em uma interface Java 8 - scala-lang.org/news/2.12.0#traits-compile-to-interfaces .
Kevin Meredith
209

Há uma seção em Programação em Scala chamada "Caracterizar ou não caracterizar?" que aborda esta questão. Como a 1ª edição está disponível on-line, espero que seja correto citar tudo aqui. (Qualquer programador Scala sério deve comprar o livro):

Sempre que você implementar uma coleção reutilizável de comportamento, terá que decidir se deseja usar uma característica ou uma classe abstrata. Não existe uma regra firme, mas esta seção contém algumas diretrizes a serem consideradas.

Se o comportamento não for reutilizado , torne-o uma classe concreta. Afinal, não é um comportamento reutilizável.

Se puder ser reutilizado em várias classes não relacionadas , faça disso uma característica. Somente traços podem ser misturados em diferentes partes da hierarquia de classes.

Se você deseja herdar dele no código Java , use uma classe abstrata. Como as características com código não têm um análogo Java próximo, tende a ser estranho herdar uma característica em uma classe Java. Enquanto isso, herdar uma classe Scala é exatamente como herdar de uma classe Java. Como uma exceção, uma característica do Scala com apenas membros abstratos se traduz diretamente em uma interface Java, portanto, você deve definir tais características, mesmo que espere que o código Java seja herdado. Consulte o Capítulo 29 para obter mais informações sobre como trabalhar com Java e Scala juntos.

Se você planeja distribuí-lo na forma compilada , e espera que grupos externos escrevam classes herdadas, você pode usar uma classe abstrata. O problema é que, quando uma característica ganha ou perde um membro, todas as classes que herdam dela devem ser recompiladas, mesmo que não tenham sido alteradas. Se os clientes externos apenas chamarem o comportamento, em vez de herdá-lo, usar uma característica é bom.

Se a eficiência é muito importante , incline-se a usar uma classe. A maioria dos tempos de execução Java torna a invocação de método virtual de um membro da classe uma operação mais rápida que a invocação de método de interface. As características são compiladas nas interfaces e, portanto, podem causar uma pequena sobrecarga de desempenho. No entanto, você deve fazer essa escolha apenas se souber que o traço em questão constitui um gargalo de desempenho e tiver evidências de que o uso de uma classe resolve o problema.

Se você ainda não sabe , depois de considerar o exposto, comece fazendo isso como uma característica. Você sempre pode alterá-lo mais tarde e, em geral, o uso de uma característica mantém mais opções em aberto.

Como o @Mushtaq Ahmed mencionou, uma característica não pode ter nenhum parâmetro passado para o construtor primário de uma classe.

Outra diferença é o tratamento de super.

A outra diferença entre classes e características é que, enquanto nas classes, as superchamadas são vinculadas estaticamente, nas características, elas são vinculadas dinamicamente. Se você escreve super.toStringem uma classe, sabe exatamente qual implementação de método será chamada. Quando você escreve a mesma coisa em uma característica, no entanto, a implementação do método a ser chamada para a super chamada é indefinida quando você define a característica.

Veja o restante do capítulo 12 para mais detalhes.

Edição 1 (2013):

Há uma diferença sutil na maneira como as classes abstratas se comportam em comparação com as características. Uma das regras de linearização é que ela preserva a hierarquia de herança das classes, que tende a empurrar classes abstratas mais tarde na cadeia, enquanto os traços podem ser misturados com alegria. Em certas circunstâncias, é realmente preferível estar na última posição da linearização de classes , então classes abstratas podem ser usadas para isso. Consulte Linearização de classe restritiva (ordem de mixagem) em Scala .

Edição 2 (2018):

No Scala 2.12, o comportamento de compatibilidade binária do traço mudou. Antes da versão 2.12, adicionar ou remover um membro à característica exigia a recompilação de todas as classes que herdam a característica, mesmo que as classes não tenham sido alteradas. Isso se deve à maneira como as características foram codificadas na JVM.

No Scala 2.12, os traços são compilados nas interfaces Java , portanto, o requisito diminuiu um pouco. Se a característica seguir um destes procedimentos, suas subclasses ainda exigirão recompilação:

  • definindo campos ( valou var, mas uma constante está ok - final valsem tipo de resultado)
  • chamando super
  • instruções inicializador no corpo
  • estendendo uma classe
  • confiando na linearização para encontrar implementações no super estreito certo

Mas se a característica não existir, você poderá atualizá-la sem interromper a compatibilidade binária.

Eugene Yokota
fonte
2
If outside clients will only call into the behavior, instead of inheriting from it, then using a trait is fine- Alguém poderia explicar qual é a diferença aqui? extendsvs with?
0fnt 14/06
2
@ 0fnt Sua distinção não é sobre estende vs com. O que ele está dizendo é que, se você apenas misturar o traço na mesma compilação, os problemas de compatibilidade binária não se aplicarão. No entanto, se a sua API for projetada para permitir que os usuários misturem os traços por conta própria, será necessário se preocupar com a compatibilidade binária.
John Colanduoni 15/09/2015
2
@ 0fnt: Não há absolutamente nenhuma diferença semântica entre extendse with. É puramente sintático. Se você herdar de vários modelos, o primeiro recebe extend, todos os outros recebem with, é isso. Pense withcomo uma vírgula: class Foo extends Bar, Baz, Qux.
Jörg W Mittag
77

Por tudo o que vale a pena, a Programação em Scala de Odersky et al recomenda que, quando você duvida, use traços. Você sempre pode transformá-los em classes abstratas posteriormente, se necessário.

Daniel C. Sobral
fonte
20

Além do fato de que você não pode estender diretamente várias classes abstratas, mas você pode misturar várias características em uma classe, vale mencionar que as características são empilháveis, pois as super chamadas em uma característica são dinamicamente ligadas (está se referindo a uma classe ou característica misturada antes atual).

Da resposta de Thomas na diferença entre classe abstrata e característica :

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
fonte
9

Ao estender uma classe abstrata, isso mostra que a subclasse é de um tipo semelhante. Este não é necessariamente o caso ao usar traços, eu acho.

Pedro Pedro
fonte
Isso tem implicações práticas ou apenas facilita a compreensão do código?
Ralf
8

Na Programming Scala, os autores dizem que as classes abstratas fazem um relacionamento clássico "is-a" orientado a objetos, enquanto os traços são um modo de composição de scala.

Marta
fonte
5

As classes abstratas podem conter comportamento - elas podem ser parametrizadas com argumentos do construtor (que não podem ser características) e representam uma entidade que funciona. Os traços representam apenas um único recurso, uma interface de uma funcionalidade.

Dario
fonte
8
Espero que você não esteja insinuando que os traços não podem conter comportamento. Ambos podem conter código de implementação.
Mitch Blevins
11
@ Mitch Blevins: Claro que não. Eles podem conter código, mas quando você define trait Enumerablecom muitas funções auxiliares, eu não as chamaria de comportamento, mas apenas de funcionalidade conectada a um recurso.
Dario
4
@Dario Eu vejo "comportamento" e "funcionalidade" como sinônimos, então acho sua resposta muito confusa.
David J.
3
  1. Uma classe pode herdar de várias características, mas apenas uma classe abstrata.
  2. As classes abstratas podem ter parâmetros de construtor e parâmetros de tipo. As características podem ter apenas parâmetros de tipo. Por exemplo, você não pode dizer traço t (i: Int) {}; o parâmetro i é ilegal.
  3. As classes abstratas são totalmente interoperáveis ​​com Java. Você pode chamá-los do código Java sem nenhum wrapper. As características são totalmente interoperáveis ​​apenas se não contiverem nenhum código de implementação.
pavan.vn101
fonte