Sự khác biệt giữa các lớp con tự và đặc điểm là gì?


387

Một kiểu tự cho một đặc điểm A:

trait B
trait A { this: B => }

nói rằng " Akhông thể trộn lẫn vào một lớp cụ thể mà cũng không mở rộng B" .

Mặt khác, như sau:

trait B
trait A extends B

nói rằng "bất kỳ lớp (cụ thể hoặc trừu tượng) nào Acũng sẽ được trộn trong B" .

Không phải hai câu này có nghĩa giống nhau sao? Kiểu tự dường như chỉ phục vụ để tạo khả năng xảy ra lỗi thời gian biên dịch đơn giản.

Tôi đang thiếu gì?


Tôi thực sự quan tâm ở đây về sự khác biệt giữa các loại bản thân và phân lớp trong các đặc điểm. Tôi biết một số cách sử dụng phổ biến cho các loại tự; Tôi chỉ không thể tìm thấy một lý do tại sao họ sẽ không được thực hiện rõ ràng hơn cùng một cách với phân nhóm.
Dave

32
Người ta có thể sử dụng các tham số loại trong tự loại: trait A[Self] {this: Self => }là hợp pháp, trait A[Self] extends Selfkhông.
Blaisorblade

3
Một kiểu bản thân cũng có thể là một lớp, nhưng một đặc điểm không thể kế thừa từ một lớp.
cvogt

10
@cvogt: một đặc điểm có thể thừa hưởng từ một lớp (ít nhất là từ 2.10): pastebin.com/zShvr8LX
Erik Kaplun

1
@Blaisorblade: không phải là một cái gì đó có thể được giải quyết bằng một thiết kế lại ngôn ngữ nhỏ, và không phải là một giới hạn cơ bản? (ít nhất là từ quan điểm của câu hỏi)
Erik Kaplun

Câu trả lời:


273

Nó chủ yếu được sử dụng cho Dependency Injection , chẳng hạn như trong Mẫu Bánh. Có tồn tại một bài viết tuyệt vời bao gồm nhiều hình thức tiêm phụ thuộc khác nhau trong Scala, bao gồm cả Mô hình Bánh. Nếu bạn Google "Mẫu bánh và Scala", bạn sẽ nhận được nhiều liên kết, bao gồm cả bản trình bày và video. Bây giờ, đây là một liên kết đến một câu hỏi khác .

Bây giờ, như sự khác biệt giữa một loại bản thân và mở rộng một đặc điểm, đó là đơn giản. Nếu bạn nói B extends A, thì B một A. Khi bạn sử dụng tự loại, B yêu cầu một A. Có hai yêu cầu cụ thể được tạo với kiểu tự:

  1. Nếu Bđược mở rộng, thì bạn bắt buộc phải kết hợp A.
  2. Khi một lớp cụ thể cuối cùng mở rộng / trộn lẫn - trong những đặc điểm này, một số lớp / tính trạng phải thực hiện A.

Hãy xem xét các ví dụ sau:

scala> trait User { def name: String }
defined trait User

scala> trait Tweeter {
     |   user: User =>
     |   def tweet(msg: String) = println(s"$name: $msg")
     | }
defined trait Tweeter

scala> trait Wrong extends Tweeter {
     |   def noCanDo = name
     | }
<console>:9: error: illegal inheritance;
 self-type Wrong does not conform to Tweeter's selftype Tweeter with User
       trait Wrong extends Tweeter {
                           ^
<console>:10: error: not found: value name
         def noCanDo = name
                       ^

Nếu Tweeterlà một lớp con của User, sẽ không có lỗi. Trong đoạn mã trên, chúng ta cần một Userbất cứ khi nào Tweeterđược sử dụng, tuy nhiên một Userkhông được cung cấp để Wrong, vì vậy chúng tôi đã nhận lỗi. Bây giờ, với mã ở trên vẫn còn trong phạm vi, hãy xem xét:

scala> trait DummyUser extends User {
     |   override def name: String = "foo"
     | }
defined trait DummyUser

scala> trait Right extends Tweeter with User {
     |   val canDo = name
     | }
defined trait Right 

scala> trait RightAgain extends Tweeter with DummyUser {
     |   val canDo = name
     | }
defined trait RightAgain

Với Right, yêu cầu để trộn vào một Userđược thỏa mãn. Tuy nhiên, yêu cầu thứ hai được đề cập ở trên không được thỏa mãn: gánh nặng của việc thực hiện Uservẫn còn đối với các lớp / đặc điểm mở rộng Right.

Với RightAgaincả hai yêu cầu đều được thỏa mãn. A Uservà việc thực hiện Userđược cung cấp.

Đối với các trường hợp sử dụng thực tế hơn, xin vui lòng xem các liên kết ở đầu câu trả lời này! Nhưng, hy vọng bây giờ bạn có được nó.


3
Cảm ơn. Mẫu Bánh là 90% ý nghĩa của lý do tại sao tôi nói về sự cường điệu xung quanh kiểu tự ... đó là nơi lần đầu tiên tôi nhìn thấy chủ đề. Ví dụ của Jonas Boner rất hay vì nó nhấn mạnh điểm của câu hỏi của tôi. Nếu bạn thay đổi kiểu tự trong ví dụ máy sưởi của anh ấy thành phụ đề thì sự khác biệt sẽ là gì (khác với lỗi bạn gặp phải khi xác định Hợp phần hóa nếu bạn không trộn đúng thứ?
Dave

29
@Dave: Ý bạn là như thế trait WarmerComponentImpl extends SensorDeviceComponent with OnOffDeviceComponentnào? Điều đó sẽ gây ra WarmerComponentImplnhững giao diện đó. Chúng sẽ có sẵn cho bất cứ điều gì mở rộng WarmerComponentImpl, điều này rõ ràng là sai, vì nó không phải là a SensorDeviceComponent, cũng không phải là a OnOffDeviceComponent. Là một kiểu tự, những phụ thuộc này có sẵn dành riêng cho WarmerComponentImpl. A Listcó thể được sử dụng như một Array, và ngược lại. Nhưng họ không giống nhau.
Daniel C. Sobral

10
Cảm ơn Daniel. Đây có lẽ là sự khác biệt lớn mà tôi đang tìm kiếm. Vấn đề thực tế là việc sử dụng phân lớp sẽ rò rỉ chức năng vào giao diện của bạn mà bạn không có ý định. Đó là kết quả của việc vi phạm quy tắc "is-part-a" lý thuyết hơn cho các đặc điểm. Kiểu tự thể hiện mối quan hệ "sử dụng-a" giữa các bộ phận.
Dave

11
@Rodney Không, không nên. Trong thực tế, sử dụng thisvới các kiểu tự là một cái gì đó tôi nhìn xuống, vì nó không có lý do chính đáng cho bản gốc this.
Daniel C. Sobral

9
@opensas Hãy thử self: Dep1 with Dep2 =>.
Daniel C. Sobral

156

Tự loại cho phép bạn xác định phụ thuộc theo chu kỳ. Ví dụ: bạn có thể đạt được điều này:

trait A { self: B => }
trait B { self: A => }

Kế thừa sử dụng extendskhông cho phép điều đó. Thử:

trait A extends B
trait B extends A
error:  illegal cyclic reference involving trait A

Trong sách Oderky, hãy xem phần 33.5 (Tạo chương UI bảng tính) trong đó đề cập đến:

Trong ví dụ về bảng tính, Mô hình lớp kế thừa từ Trình đánh giá và do đó có được quyền truy cập vào phương thức đánh giá của nó. Theo cách khác, Trình đánh giá lớp định nghĩa kiểu tự của nó là Kiểu, như sau:

package org.stairwaybook.scells
trait Evaluator { this: Model => ...

Hi vọng điêu nay co ich.


3
Tôi đã không xem xét kịch bản này. Đây là ví dụ đầu tiên về một cái gì đó mà tôi đã thấy không giống với kiểu tự như với một lớp con. Tuy nhiên, nó có vẻ như là một casey cạnh và quan trọng hơn, nó có vẻ như là một ý tưởng tồi (tôi thường đi xa khỏi cách của tôi KHÔNG định nghĩa các phụ thuộc theo chu kỳ!). Bạn có thấy đây là sự khác biệt quan trọng nhất?
Dave

4
Tôi nghĩ vậy. Tôi không thấy bất kỳ lý do nào khác tại sao tôi thích tự loại để mở rộng mệnh đề. Kiểu tự là dài dòng, chúng không được kế thừa (vì vậy bạn phải thêm kiểu tự vào tất cả các kiểu con như một nghi thức) và bạn chỉ có thể nhìn thấy thành viên nhưng không thể ghi đè lên chúng. Tôi nhận thức rõ về mẫu Bánh và nhiều bài viết đề cập đến các loại tự cho DI. Nhưng không hiểu sao tôi không bị thuyết phục. Tôi đã tạo một ứng dụng mẫu ở đây từ lâu ( bitbucket.org/mushtaq/scala-di ). Nhìn cụ thể vào thư mục / src / configs. Tôi đã đạt được DI để thay thế các cấu hình Spring phức tạp mà không cần tự gõ.
Mushtaq Ahmed

Mushtaq, chúng tôi đồng ý. Tôi nghĩ rằng tuyên bố của Daniel về việc không phơi bày chức năng không chủ ý là một điều quan trọng nhưng, như bạn nói, có một cái nhìn phản chiếu về 'tính năng' này ... rằng bạn không thể ghi đè chức năng hoặc sử dụng nó trong các lớp con trong tương lai. Điều này khá rõ ràng cho tôi biết khi nào thiết kế sẽ gọi cho nhau. Tôi sẽ tránh tự loại cho đến khi tôi tìm thấy một nhu cầu thực sự - tức là nếu tôi bắt đầu sử dụng các đối tượng như các mô-đun như Daniel chỉ ra. Tôi đang tự động phụ thuộc với các tham số ngầm định và một đối tượng bootstrapper đơn giản. Tôi thích sự đơn giản.
Dave

@ DanielC.Sobral có thể là nhờ bình luận của bạn nhưng tại thời điểm này, nó có nhiều upvote hơn anser của bạn. Nâng cấp cả hai :)
rintcius

Tại sao không chỉ tạo một đặc điểm AB? Vì các đặc điểm A và B phải luôn được kết hợp trong bất kỳ lớp cuối cùng nào, tại sao lại tách chúng ra ngay từ đầu?
Giàu Oliver

56

Một sự khác biệt nữa là các kiểu tự có thể chỉ định các kiểu không phải là lớp. Ví dụ

trait Foo{
   this: { def close:Unit} => 
   ...
}

Loại tự ở đây là một loại cấu trúc. Hiệu quả là để nói rằng bất cứ điều gì trộn lẫn trong Foo đều phải thực hiện một đơn vị trả về phương thức "đóng" không có đối số. Điều này cho phép mixins an toàn để gõ vịt.


41
Trên thực tế, bạn cũng có thể sử dụng tính kế thừa với các kiểu cấu trúc: lớp trừu tượng A mở rộng {def close: Unit}
Adrian

12
Tôi nghĩ rằng gõ cấu trúc đang sử dụng sự phản chiếu, vì vậy chỉ sử dụng khi không có lựa chọn nào khác ...
Eran Medan

@Adrian, tôi tin rằng nhận xét của bạn không chính xác. `lớp trừu tượng A mở rộng {def close: Unit}` chỉ là một lớp trừu tượng với siêu lớp Object. nó chỉ là một cú pháp cho phép của Scala đối với các biểu thức vô nghĩa. Bạn có thể `lớp X kéo dài {def f = 1}; ví dụ X (). f` mới
Alexey

1
@Alexey Tôi không thấy lý do tại sao ví dụ của bạn (hoặc của tôi) lại vô nghĩa.
Adrian

1
@Adrian, abstract class A extends {def close:Unit}tương đương với abstract class A {def close:Unit}. Vì vậy, nó không liên quan đến các loại cấu trúc.
Alexey

13

Phần 2.3 "Chú thích Selftype" của bản tóm tắt thành phần Scala gốc của Martin Oderky thực sự giải thích rõ ràng mục đích của selftype ngoài thành phần mixin: cung cấp một cách khác để liên kết một lớp với một loại trừu tượng.

Ví dụ được đưa ra trong bài báo giống như sau, và nó dường như không có một phóng viên lớp con thanh lịch:

abstract class Graph {
  type Node <: BaseNode;
  class BaseNode {
    self: Node =>
    def connectWith(n: Node): Edge =
      new Edge(self, n);
  }
  class Edge(from: Node, to: Node) {
    def source() = from;
    def target() = to;
  }
}

class LabeledGraph extends Graph {
  class Node(label: String) extends BaseNode {
    def getLabel: String = label;
    def self: Node = this;
  }
}

Đối với những người thắc mắc tại sao việc phân lớp sẽ không giải quyết được điều này, Phần 2.3 cũng cho biết điều này: Số Mỗi toán hạng của một thành phần mixin C_0 với ... với C_n, phải tham chiếu đến một lớp. Cơ chế thành phần mixin không cho phép bất kỳ C_i nào đề cập đến một loại trừu tượng. Hạn chế này cho phép kiểm tra tĩnh các mơ hồ và ghi đè xung đột tại điểm mà một lớp được tạo.
Luke Maurer

12

Một điều khác chưa được đề cập: bởi vì kiểu tự không phải là một phần của hệ thống phân cấp của lớp bắt buộc, chúng có thể được loại trừ khỏi khớp mẫu, đặc biệt là khi bạn khớp hoàn toàn với hệ thống phân cấp kín. Điều này thuận tiện khi bạn muốn mô hình hóa các hành vi trực giao như:

sealed trait Person
trait Student extends Person
trait Teacher extends Person
trait Adult { this : Person => } // orthogonal to its condition

val p : Person = new Student {}
p match {
  case s : Student => println("a student")
  case t : Teacher => println("a teacher")
} // that's it we're exhaustive

10

TL; DR tóm tắt các câu trả lời khác:

  • Các loại bạn mở rộng được tiếp xúc với các kiểu được kế thừa, nhưng các kiểu tự thì không

    ví dụ: class Cow { this: FourStomachs }cho phép bạn sử dụng các phương pháp chỉ có sẵn cho động vật nhai lại, chẳng hạn như digestGrass. Tuy nhiên, những đặc điểm mở rộng Bò sẽ không có những đặc quyền như vậy. Mặt khác, class Cow extends FourStomachssẽ tiếp xúc digestGrassvới bất cứ ai extends Cow .

  • tự loại cho phép phụ thuộc theo chu kỳ, mở rộng các loại khác không


9

Hãy bắt đầu với sự phụ thuộc theo chu kỳ.

trait A {
  selfA: B =>
  def fa: Int }

trait B {
  selfB: A =>
  def fb: String }

Tuy nhiên, tính mô-đun của giải pháp này không tuyệt vời như lần đầu tiên xuất hiện, bởi vì bạn có thể ghi đè các kiểu tự như vậy:

trait A1 extends A {
  selfA1: B =>
  override def fb = "B's String" }
trait B1 extends B {
  selfB1: A =>
  override def fa = "A's String" }
val myObj = new A1 with B1

Mặc dù, nếu bạn ghi đè một thành viên của loại tự, bạn sẽ mất quyền truy cập vào thành viên ban đầu, vẫn có thể được truy cập thông qua siêu sử dụng kế thừa. Vì vậy, những gì thực sự đạt được khi sử dụng thừa kế là:

trait AB {
  def fa: String
  def fb: String }
trait A1 extends AB
{ override def fa = "A's String" }        
trait B1 extends AB
{ override def fb = "B's String" }    
val myObj = new A1 with B1

Bây giờ tôi không thể yêu cầu hiểu tất cả sự tinh tế của mẫu bánh, nhưng điều gây ấn tượng với tôi là phương pháp chính để thực thi mô đun là thông qua thành phần chứ không phải kiểu thừa kế hoặc kiểu tự.

Phiên bản thừa kế ngắn hơn, nhưng lý do chính tôi thích kế thừa hơn các kiểu tự là tôi thấy khó khăn hơn nhiều để có được thứ tự khởi tạo chính xác với các kiểu tự. Tuy nhiên, có một số điều bạn có thể làm với kiểu tự mà bạn không thể làm với thừa kế. Các kiểu tự có thể sử dụng một kiểu trong khi thừa kế yêu cầu một đặc điểm hoặc một lớp như trong:

trait Outer
{ type T1 }     
trait S1
{ selfS1: Outer#T1 => } //Not possible with inheritance.

Bạn thậm chí có thể làm:

trait TypeBuster
{ this: Int with String => }

Mặc dù bạn sẽ không bao giờ có thể khởi tạo nó. Tôi không thấy bất kỳ lý do tuyệt đối nào cho việc không thể kế thừa từ một loại, nhưng tôi chắc chắn cảm thấy sẽ hữu ích khi có các lớp và đặc điểm của hàm tạo đường dẫn khi chúng ta có các đặc điểm / lớp của hàm tạo. Thật không may

trait InnerA extends Outer#Inner //Doesn't compile

Chúng tôi có thứ này:

trait Outer
{ trait Inner }
trait OuterA extends Outer
{ trait InnerA extends Inner }
trait OuterB extends Outer
{ trait InnerB extends Inner }
trait OuterFinal extends OuterA with OuterB
{ val myV = new InnerA with InnerB }

Hoặc này:

  trait Outer
  { trait Inner }     
  trait InnerA
  {this: Outer#Inner =>}
  trait InnerB
  {this: Outer#Inner =>}
  trait OuterFinal extends Outer
  { val myVal = new InnerA with InnerB with Inner }

Một điểm cần được đồng cảm nhiều hơn là các đặc điểm có thể mở rộng các lớp. Cảm ơn David Maclver đã chỉ ra điều này. Đây là một ví dụ từ mã của riêng tôi:

class ScnBase extends Frame
abstract class ScnVista[GT <: GeomBase[_ <: TypesD]](geomRI: GT) extends ScnBase with DescripHolder[GT] )
{ val geomR = geomRI }    
trait EditScn[GT <: GeomBase[_ <: ScenTypes]] extends ScnVista[GT]
trait ScnVistaCyl[GT <: GeomBase[_ <: ScenTypes]] extends ScnVista[GT]

ScnBasekế thừa từ lớp Swing Frame, do đó, nó có thể được sử dụng như một kiểu tự và sau đó trộn vào cuối (tại thời điểm khởi tạo). Tuy nhiên, val geomRcần phải được khởi tạo trước khi nó được sử dụng bằng cách kế thừa các đặc điểm. Vì vậy, chúng ta cần một lớp để thực thi khởi tạo trước geomR. Lớp ScnVistasau đó có thể được kế thừa từ nhiều đặc điểm trực giao mà bản thân chúng có thể được thừa hưởng từ đó. Sử dụng nhiều tham số loại (generic) cung cấp một hình thức mô đun thay thế.


7
trait A { def x = 1 }
trait B extends A { override def x = super.x * 5 }
trait C1 extends B { override def x = 2 }
trait C2 extends A { this: B => override def x = 2}

// 1.
println((new C1 with B).x) // 2
println((new C2 with B).x) // 10

// 2.
trait X {
  type SomeA <: A
  trait Inner1 { this: SomeA => } // compiles ok
  trait Inner2 extends SomeA {} // doesn't compile
}

4

Kiểu tự cho phép bạn chỉ định loại nào được phép trộn lẫn một đặc điểm. Ví dụ, nếu bạn có một đặc điểm với kiểu tự Closeable, thì đặc điểm đó biết rằng những điều duy nhất được phép trộn nó vào, phải thực hiện Closeablegiao diện.


3
@Blaisorblade: Tôi tự hỏi liệu bạn có thể đọc sai câu trả lời của kikibobo hay không - kiểu tự thực sự cho phép bạn hạn chế các loại có thể trộn lẫn vào đó, và đó là một phần của tính hữu dụng của nó. Ví dụ: nếu chúng tôi xác định trait A { self:B => ... }thì một khai báo X with Achỉ có giá trị nếu X mở rộng B. Có, bạn có thể nói X with A with Q, trong đó Q không mở rộng B, nhưng tôi tin rằng điểm của kikibobo là X bị hạn chế. Hay tôi đã bỏ lỡ điều gì?
AmigoNico

1
Cảm ơn, bạn đã đúng. Phiếu bầu của tôi đã bị khóa, nhưng may mắn là tôi có thể chỉnh sửa câu trả lời và sau đó thay đổi phiếu bầu của mình.
Blaisorblade

1

Cập nhật: Một điểm khác biệt chính là các kiểu tự có thể phụ thuộc vào nhiều lớp (tôi thừa nhận đó là trường hợp góc một chút). Ví dụ, bạn có thể có

class Person {
  //...
  def name: String = "...";
}

class Expense {
  def cost: Int = 123;
}

trait Employee {
  this: Person with Expense =>
  // ...

  def roomNo: Int;

  def officeLabel: String = name + "/" + roomNo;
}

Điều này cho phép thêm Employeemixin vào bất cứ thứ gì thuộc lớp con PersonExpense. Tất nhiên, điều này chỉ có ý nghĩa nếu Expensekéo dài Personhoặc ngược lại. Vấn đề là việc sử dụng các kiểu tự Employeecó thể độc lập với hệ thống phân cấp của các lớp mà nó phụ thuộc vào. Nó không quan tâm đến những gì mở rộng những gì - Nếu bạn chuyển đổi thứ bậc của Expensevs Person, bạn không phải sửa đổi Employee.


Nhân viên không cần phải là một lớp để đi xuống từ Người. Đặc điểm có thể mở rộng các lớp học. Nếu Nhân viên đặc điểm mở rộng Người thay vì sử dụng loại tự, ví dụ sẽ vẫn hoạt động. Tôi thấy ví dụ của bạn thú vị, nhưng dường như nó không minh họa trường hợp sử dụng cho các kiểu tự.
Morgan Creighton

@MorganCreighton Đủ công bằng, tôi không biết rằng các đặc điểm có thể mở rộng các lớp học. Tôi sẽ suy nghĩ về nó nếu tôi có thể tìm thấy một ví dụ tốt hơn.
Petr Pudlák

Vâng, đó là một tính năng ngôn ngữ đáng ngạc nhiên. Nếu đặc điểm Nhân viên lớp mở rộng, thì bất cứ lớp nào cuối cùng "rút lui" Nhân viên cũng sẽ phải gia hạn Người. Nhưng hạn chế đó vẫn còn nếu Nhân viên sử dụng loại tự thay vì mở rộng Người. Chúc mừng, Petr!
Morgan Creighton

1
Tôi không thấy lý do tại sao "điều này chỉ có ý nghĩa nếu Chi phí kéo dài Người hoặc ngược lại."
Robin Green

0

trong trường hợp đầu tiên, một tính trạng phụ hoặc lớp con của B có thể được trộn lẫn vào bất cứ điều gì sử dụng A. Vì vậy, B có thể là một tính trạng trừu tượng.


Không, B có thể (và thực sự là) một "đặc điểm trừu tượng" trong cả hai trường hợp. Vì vậy, không có sự khác biệt từ quan điểm đó.
Robin Green
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.