Qual é a diferença entre auto-tipos e herança de características em Scala?


9

Quando pesquisado no Google, surgem muitas respostas para esse tópico. No entanto, não acho que nenhum deles faça um bom trabalho ilustrando a diferença entre esses dois recursos. Então, eu gostaria de tentar mais uma vez, especificamente ...

O que pode ser feito com tipos próprios e não com herança, e vice-versa?

Para mim, deve haver alguma diferença física quantificável entre os dois, caso contrário eles são apenas nominalmente diferentes.

Se o Traço A estender B ou os tipos próprios B, os dois não ilustram que ser B é um requisito? Onde está a diferença?


Desconfio dos termos que você definiu na recompensa. Por um lado, defina a diferença "física", pois esse é todo o software. Além disso, para qualquer objeto composto criado com mixins, você provavelmente pode criar algo aproximado em função com herança - se você definir a função puramente em termos dos métodos visíveis. Onde eles diferem é em extensibilidade, flexibilidade e composição.
— itsbruce

Se você tiver uma variedade de chapas de aço de tamanhos diferentes, poderá prendê-las para formar uma caixa ou soldá-las. De uma perspectiva estreita, isso seria equivalente em funcionalidade - se você ignorar o fato de que uma pode ser facilmente reconfigurada ou ampliada e a outra não. Tenho a sensação de que você argumentará que são equivalentes, embora eu ficaria feliz em provar que estava errado se você dissesse mais sobre seus critérios.
— itsbruce

Estou mais do que familiar com o que você está dizendo em geral, mas ainda não entendo qual é a diferença nesse caso específico. Você poderia fornecer alguns exemplos de código que mostram que um método é mais extensível e flexível que o outro? * Código base com extensão * código base com auto tipos * Feature adicionado ao estilo extensão * Feature adicionado ao estilo self tipo
— Mark Canlas

OK, acho que posso tentar isso antes da recompensa se esgota;)
— itsbruce

Respostas:


11

Se a característica A estender B, a mistura de A fornecerá B precisamente mais o que A acrescenta ou estende. Por outro lado, se o traço A tiver uma auto-referência explicitamente digitada como B, a classe-mãe final também deverá misturar em B ou um tipo descendente de B (e misture-o primeiro , o que é importante).

Essa é a diferença mais importante. No primeiro caso, o tipo preciso de B é cristalizado no ponto A o estende. No segundo, o designer da classe pai decide qual versão de B é usada, no ponto em que a classe pai é composta.

Outra diferença é onde A e B fornecem métodos com o mesmo nome. Onde A estende B, o método de A substitui B. Onde A é misturado após B, o método de A simplesmente vence.

A auto-referência digitada oferece muito mais liberdade; o acoplamento entre A e B está solto.

ATUALIZAR:

Como você não tem certeza sobre o benefício dessas diferenças ...

Se você usa herança direta, cria a característica A que é B + A. Você definiu o relacionamento em pedra.

Se você usar uma auto-referência digitada, qualquer pessoa que queira usar sua característica A na classe C poderá

  • Misture B e depois A em C.
  • Misture um subtipo de B e depois A em C.
  • Misture A em C, onde C é uma subclasse de B.

E esse não é o limite de suas opções, devido ao modo como o Scala permite instanciar uma característica diretamente com um bloco de código como seu construtor.

Quanto à diferença entre a vitória do método de A , porque A é o último da mistura, comparado a A que estende B, considere isso ...

Onde você mescla uma sequência de características, sempre que o método foo()é chamado, o compilador vai para a última característica misturada para procurar e foo(), se não for encontrado, ele percorre a sequência para a esquerda até encontrar uma característica que implemente foo()e use naquela. A também tem a opção de chamar super.foo(), que também percorre a sequência para a esquerda até encontrar uma implementação e assim por diante.

Portanto, se A tem uma auto-referência digitada para B e o escritor de A sabe que B implementa foo(), A pode chamar super.foo()sabendo que, se nada mais fornecer foo(), B fará. No entanto, o criador da classe C tem a opção de remover qualquer outra característica na qual implementa foo(), e A obterá isso.

Novamente, isso é muito mais poderoso e menos limitativo do que A estendendo B e chamando diretamente a versão de B de foo().


Qual é a diferença funcional entre uma vitória e uma substituição? Estou recebendo A nos dois casos através de mecanismos diferentes? E no seu primeiro exemplo ... No seu primeiro parágrafo, por que não a característica A estende o SuperOfB? Parece que sempre poderíamos remodelar o problema usando qualquer um dos mecanismos. Acho que não estou vendo um caso de uso em que isso não seja possível. Ou estou assumindo muitas coisas.
— precisa

Por que você deseja que A estenda uma subclasse de B, se B define o que você precisa? A auto-referência força B (ou uma subclasse) a estar presente, mas dá à escolha do desenvolvedor? Eles podem misturar algo que escreveram depois que você escreveu a característica A, desde que ela se estenda B. Por que restringi-los apenas ao que estava disponível quando você escreveu a característica A?
— itsbruce

Atualizado para tornar a diferença super clara.
— precisa saber é o seguinte

@itsbruce existe alguma diferença conceitual? IS-A versus HAS-A?
— Jas

@Jas No contexto do relacionamento entre os traços A e B , a herança é IS-A, enquanto a auto-referência digitada fornece HAS-A (um relacionamento de composição). Para a classe na qual as características são misturadas, o resultado é IS-A , independentemente.
— itsbruce

0

Eu tenho algum código que ilustra algumas das diferenças de visibilidade e implementações "padrão" ao estender vs definir um tipo próprio. Ele não ilustra nenhuma das partes já discutidas sobre como as colisões de nomes reais são resolvidas, mas concentra-se no que é possível e não é possível fazer.

trait A1 {
  self: B =>

  def doit {
    println(bar)
  }
}

trait A2 extends B {
  def doit {
    println(bar)
  }
}

trait B {
  def bar = "default bar"
}

trait BX extends B {
  override def bar = "bar bx"
}

trait BY extends B {
  override def bar = "bar by"
}

object Test extends App {
  // object Thing1 extends A1  // FAIL: does not conform to A1 self-type
  object Thing1 extends A1 with B
  object Thing2 extends A2

  object Thing1X extends A1 with BX
  object Thing1Y extends A1 with BY
  object Thing2X extends A2 with BX
  object Thing2Y extends A2 with BY

  Thing1.doit  // default bar
  Thing2.doit  // default bar
  Thing1X.doit // bar bx
  Thing1Y.doit // bar by
  Thing2X.doit // bar bx
  Thing2Y.doit // bar by

  // up-cast
  val a1: A1 = Thing1Y
  val a2: A2 = Thing2Y

  // println(a1.bar)    // FAIL: not visible
  println(a2.bar)       // bar bx
  // println(a2.bary)   // FAIL: not visible
  println(Thing2Y.bary) // 42
}

Uma diferença importante da IMO é que A1ela não expõe que é necessária Bpara qualquer coisa que apenas a veja A1(como ilustrado nas partes pré-fabricadas). O único código que realmente verá que uma especialização específica Bé usada é o código que sabe explicitamente sobre o tipo composto (como Think*{X,Y}).

Outro ponto é que A2(com extensão) será realmente usado Bse nada for especificado, enquanto A1(o tipo próprio) não diz que será usado, a Bmenos que seja substituído, um B concreto deve ser explicitamente fornecido quando os objetos forem instanciados.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.