Apa perbedaan antara tipe-diri dan pewarisan sifat dalam Scala?

9

Saat Google, banyak tanggapan untuk topik ini muncul. Namun, saya tidak merasa ada di antara mereka melakukan pekerjaan yang baik untuk menggambarkan perbedaan antara kedua fitur ini. Jadi saya ingin mencoba sekali lagi, khususnya ...

Apa yang bisa dilakukan dengan tipe diri dan bukan dengan warisan, dan sebaliknya?

Bagi saya, harus ada beberapa perbedaan fisik yang dapat dikuantifikasi di antara keduanya, jika tidak keduanya hanya berbeda secara nominal.

Jika Sifat A meluas B atau tipe diri B, tidakkah keduanya menggambarkan bahwa menjadi B adalah persyaratan? Dimana perbedaannya?

Mark Canlas
sumber
Saya waspada dengan ketentuan yang Anda tentukan pada hadiah. Untuk satu hal, tentukan perbedaan "fisik", mengingat ini semua adalah perangkat lunak. Di luar itu, untuk objek komposit apa pun yang Anda buat dengan mixin, Anda mungkin dapat membuat sesuatu yang mendekati fungsi dengan pewarisan - jika Anda mendefinisikan fungsi murni dalam hal metode yang terlihat. Di mana mereka akan berbeda adalah dalam ekstensibilitas, fleksibilitas dan kompabilitas.
bruce
Jika Anda memiliki bermacam-macam pelat baja dengan ukuran yang berbeda, Anda dapat membautnya menjadi satu kotak atau Anda dapat mengelasnya. Dari satu perspektif, sempit, ini akan setara dalam fungsionalitas - jika Anda mengabaikan fakta bahwa seseorang dapat dengan mudah dikonfigurasi ulang atau diperluas dan yang lainnya tidak. Saya merasa bahwa Anda akan berpendapat bahwa mereka setara, meskipun saya akan senang terbukti salah jika Anda mengatakan lebih banyak tentang kriteria Anda.
bruce
Saya lebih dari terbiasa dengan apa yang Anda katakan secara umum, tapi saya masih tidak mengerti apa bedanya dalam kasus khusus ini. Bisakah Anda memberikan beberapa contoh kode yang menunjukkan bahwa satu metode lebih fleksibel dan fleksibel daripada yang lain? * Kode dasar dengan ekstensi * Kode dasar dengan tipe mandiri * Fitur ditambahkan ke gaya ekstensi * Fitur ditambahkan ke gaya tipe mandiri
Mark Canlas
OK, saya pikir saya bisa mencobanya sebelum bounty habis;)
bruce

Jawaban:

11

Jika sifat A memanjang B, maka mencampurkan dalam A memberi Anda tepat B ditambah apa pun yang ditambahkan atau diperluas oleh A. Sebaliknya, jika sifat A memiliki referensi diri yang secara eksplisit diketik sebagai B, maka kelas induk akhir juga harus dicampur dalam B atau tipe B turunan (dan mencampurnya terlebih dahulu , yang penting).

Itulah perbedaan terpenting. Dalam kasus pertama, tipe B yang tepat dikristalisasi pada titik A yang meluas. Pada yang kedua, perancang kelas induk dapat memutuskan versi B yang digunakan, pada titik di mana kelas induk disusun.

Perbedaan lainnya adalah di mana A dan B menyediakan metode dengan nama yang sama. Di mana A meluas B, metode A menimpa B. Di mana A dicampur setelah B, metode A hanya menang.

Referensi diri yang diketik memberi Anda lebih banyak kebebasan; kopling antara A dan B longgar.

MEMPERBARUI:

Karena Anda tidak jelas tentang manfaat perbedaan ini ...

Jika Anda menggunakan pewarisan langsung, maka Anda membuat sifat A yang merupakan B + A. Anda telah mengatur hubungan di atas batu.

Jika Anda menggunakan referensi diri yang diketik, maka siapa pun yang ingin menggunakan sifat A Anda di kelas C bisa

  • Campur B dan A menjadi C.
  • Campur subtipe B dan kemudian A menjadi C.
  • Campur A menjadi C, di mana C adalah subkelas dari B.

Dan ini bukan batas dari pilihan mereka, mengingat cara Scala memungkinkan Anda untuk instantiate sifat secara langsung dengan blok kode sebagai konstruktornya.

Adapun perbedaan antara metode menang A, karena A dicampur pada akhirnya, dibandingkan dengan A memperluas B, pertimbangkan ini ...

Di mana Anda mencampur dalam urutan sifat, setiap kali metode foo()dipanggil, kompilator pergi ke sifat terakhir yang dicampur untuk mencari foo(), lalu (jika tidak ditemukan), itu melintasi urutan ke kiri sampai menemukan sifat yang mengimplementasikan foo()dan menggunakan bahwa. A juga memiliki opsi untuk memanggil super.foo(), yang juga melintasi urutan ke kiri hingga menemukan implementasi, dan seterusnya.

Jadi jika A memiliki referensi diri yang diketik untuk B dan penulis A tahu bahwa B mengimplementasikan foo(), A dapat memanggil super.foo()mengetahui bahwa jika tidak ada yang lain menyediakan foo(), B akan melakukannya. Namun, pencipta kelas C memiliki opsi untuk menghapus semua sifat lain yang diimplementasikan foo(), dan A akan mendapatkannya.

Sekali lagi, ini jauh lebih kuat dan tidak terlalu membatasi daripada A memperluas B dan langsung memanggil versi B dari foo().

itu otaknya
sumber
Apa perbedaan fungsional antara A menang vs A overriding? Saya mendapatkan nilai A dalam kedua kasus melalui mekanisme yang berbeda? Dan dalam contoh pertama Anda ... Dalam paragraf pertama Anda, mengapa tidak memiliki sifat A memperpanjang SuperOfB? Rasanya seperti kita selalu bisa merombak masalah menggunakan kedua mekanisme. Saya kira saya tidak melihat use case di mana ini tidak mungkin. Atau saya berasumsi terlalu banyak hal.
Mark Canlas
Um, mengapa Anda ingin memiliki A memperpanjang subclass dari B, jika B mendefinisikan apa yang Anda butuhkan? Referensi diri memaksa B (atau subkelas) untuk hadir, tetapi memberikan pilihan pengembang? Mereka dapat mencampurkan sesuatu yang mereka tulis setelah Anda menulis sifat A, selama itu meluas B. Mengapa membatasi mereka hanya pada apa yang tersedia ketika Anda menulis sifat A?
itsbruce
Diperbarui untuk membuat perbedaan menjadi sangat jelas.
itsbruce
@itsbruce apakah ada perbedaan konseptual? IS-A versus HAS-A?
Jas
@Jas Dalam konteks hubungan antara sifat-sifat A dan B , warisan adalah IS-A, sedangkan referensi-sendiri yang diketik memberikan HAS-A (hubungan komposisi). Untuk kelas di mana sifat dicampur, hasilnya adalah IS-A , terlepas.
bruce
0

Saya memiliki beberapa kode yang menggambarkan beberapa perbedaan visibilitas visibilitas dan implementasi "default" ketika memperluas vs pengaturan tipe diri. Itu tidak menggambarkan salah satu bagian yang sudah dibahas tentang bagaimana tabrakan nama aktual diselesaikan, tetapi berfokus pada apa yang mungkin dan tidak mungkin dilakukan.

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
}

Satu perbedaan penting IMO adalah bahwa A1tidak mengekspos bahwa itu perlu Buntuk apa pun yang hanya melihatnya sebagai A1(seperti diilustrasikan di bagian atas pemain). Satu-satunya kode yang benar-benar akan melihat bahwa spesialisasi tertentu Bdigunakan, adalah kode yang secara eksplisit tahu tentang tipe yang dikomposisikan (seperti Think*{X,Y}).

Poin lain adalah bahwa A2(dengan ekstensi) akan benar-benar digunakan Bjika tidak ada yang lain yang ditentukan sementara A1(tipe diri) tidak mengatakan bahwa itu akan digunakan Bkecuali diganti, beton B harus diberikan secara eksplisit ketika objek dipakai.

Rouzbeh Delavari
sumber