Saya faham bahawa varians jenis tidak penting untuk menulis kod Scala. Sudah lebih kurang setahun sejak saya menggunakan Scala untuk pekerjaan seharian saya, dan sejujurnya, saya tidak perlu terlalu risau.
Namun, saya fikir ia adalah topik "lanjutan" yang menarik, jadi saya mula mempelajarinya. Tidak mudah untuk memahaminya dengan segera, tetapi dengan contoh yang tepat, mungkin sedikit lebih mudah difahami. Izinkan saya mencuba menggunakan analogi berasaskan makanan ...
Apakah varians jenis?
Pertama sekali, kita harus menentukan apa jenis varians. Apabila anda berkembang dalam bahasa Berorientasikan Objek, anda dapat menentukan jenis kompleks. Itu bermaksud bahawa suatu jenis boleh diparamatisasi menggunakan jenis lain (jenis komponen).
Fikirkan List
misalnya. Anda tidak dapat menentukan List
tanpa menentukan jenis mana yang ada di dalam senarai. Anda melakukannya dengan meletakkan jenis yang terkandung dalam senarai di dalam kurungan persegi: List[String]
. Apabila anda menentukan jenis kompleks, anda dapat menentukan bagaimana ia akan mengubah hubungan subjenisnya mengikut hubungan antara jenis komponen dan subjenisnya.
Ok, kedengaran seperti huru-hara ... Mari praktikkan sedikit.
Membina empayar restoran
Matlamat kami adalah untuk membina empayar restoran. Kami mahukan restoran generik dan khusus. Setiap restoran yang akan kami buka memerlukan menu yang terdiri daripada pelbagai resipi, dan (mungkin) koki berbintang.
Resipi boleh terdiri daripada pelbagai jenis makanan (ikan, daging, daging putih, sayur-sayuran, dan lain-lain), sementara koki yang kami sewa mesti dapat memasak makanan semacam itu. Ini adalah model kami. Sekarang masanya pengekodan!
Jenis makanan yang berbeza
Sebagai contoh berasaskan makanan, kita mulakan dengan menentukan nama Trait Food
, hanya memberikan nama makanan.
trait Food { def name: String }
Kemudian kita dapat membuat Meat
dan Vegetable
, itu adalah subkelas dari Food
.
class Meat(val name: String) extends Food
class Vegetable(val name: String) extends Food
Pada akhirnya, kami menentukan WhiteMeat
kelas yang merupakan subkelas Meat
.
class WhiteMeat(override val name: String) extends Meat(name)
Kedengarannya munasabah bukan? Oleh itu, kita mempunyai jenis hierarki ini.
Kita boleh membuat beberapa contoh makanan dari pelbagai jenis. Mereka akan menjadi ramuan resipi yang akan kami sajikan di restoran kami.
// Food <- Meat val beef = new Meat("beef") // Food <- Meat <- WhiteMeat val chicken = new WhiteMeat("chicken") val turkey = new WhiteMeat("turkey") // Food <- Vegetable val carrot = new Vegetable("carrot") val pumpkin = new Vegetable("pumpkin")
Resipi, jenis kovarian
Mari tentukan jenis kovarian Recipe
. Ia memerlukan jenis komponen yang menyatakan makanan asas untuk resipi - iaitu resipi berdasarkan daging, sayur-sayuran, dll.
trait Recipe[+A] { def name: String def ingredients: List[A] }
Ini Recipe
mempunyai nama dan senarai ramuan. Senarai ramuan mempunyai jenis yang sama Recipe
. Untuk menyatakan bahawa itu Recipe
adalah kovarian dalam jenisnya A
, kami menulisnya sebagai Recipe[+A]
. Resipi generik didasarkan pada setiap jenis makanan, resipi daging berdasarkan daging, dan resipi daging putih hanya mempunyai daging putih dalam senarai ramuannya.
case class GenericRecipe(ingredients: List[Food]) extends Recipe[Food] { def name: String = s"Generic recipe based on ${ingredients.map(_.name)}" }
case class MeatRecipe(ingredients: List[Meat]) extends Recipe[Meat] { def name: String = s"Meat recipe based on ${ingredients.map(_.name)}" }
case class WhiteMeatRecipe(ingredients: List[WhiteMeat]) extends Recipe[WhiteMeat] { def name: String = s"Meat recipe based on ${ingredients.map(_.name)}" }
Jenis adalah kovarian jika mengikut hubungan subtipe yang sama dengan jenis komponennya. Ini bermaksud Recipe
mengikuti hubungan subjenis yang sama dari komponennya.
Mari tentukan beberapa resipi yang akan menjadi sebahagian daripada menu yang berbeza.
// Recipe[Food]: Based on Meat or Vegetable val mixRecipe = new GenericRecipe(List(chicken, carrot, beef, pumpkin)) // Recipe[Food] <- Recipe[Meat]: Based on any kind of Meat val meatRecipe = new MeatRecipe(List(beef, turkey)) // Recipe[Food] <- Recipe[Meat] <- Recipe[WhiteMeat]: Based only on WhiteMeat val whiteMeatRecipe = new WhiteMeatRecipe(List(chicken, turkey))
Chef, jenis yang bertentangan
Kami menentukan beberapa resipi, tetapi kami memerlukan seorang tukang masak untuk memasaknya. Ini memberi kita peluang untuk bercakap mengenai kontravarian. Jenisnya bertentangan jika mengikuti hubungan terbalik subtipe jenis komponennya. Mari tentukan jenis kompleks kita Chef
, yang bertentangan dengan jenis komponen. Jenis komponen akan menjadi makanan yang boleh dimasak oleh tukang masak.
trait Chef[-A] { def specialization: String def cook(recipe: Recipe[A]): String }
A Chef
mempunyai pengkhususan dan kaedah untuk memasak resipi berdasarkan makanan tertentu. Kami menyatakan bahawa bertentangan dengan menulisnya sebagai Chef[-A]
. Sekarang kita boleh membuat seorang koki yang dapat memasak makanan generik, seorang tukang masak yang dapat memasak daging dan seorang koki yang khusus untuk daging putih.
class GenericChef extends Chef[Food] { val specialization = "All food" override def cook(recipe: Recipe[Food]): String = s"I made a ${recipe.name}" }
class MeatChef extends Chef[Meat] { val specialization = "Meat" override def cook(recipe: Recipe[Meat]): String = s"I made a ${recipe.name}" }
class WhiteMeatChef extends Chef[WhiteMeat] { override val specialization = "White meat" def cook(recipe: Recipe[WhiteMeat]): String = s"I made a ${recipe.name}" }
Oleh kerana Chef
bertentangan, Chef[Food]
adalah subkelas Chef[Meat]
yang merupakan subkelas dari Chef[WhiteMeat]
. Ini bermaksud bahawa hubungan antara subtipe adalah kebalikan dari jenis komponennya Makanan.
Baiklah, sekarang kita dapat menentukan chef yang berbeza dengan pelbagai pengkhususan untuk disewa di restoran kami.
// Chef[WhiteMeat]: Can cook only WhiteMeat val giuseppe = new WhiteMeatChef giuseppe.cook(whiteMeatRecipe) // Chef[WhiteMeat] <- Chef[Meat]: Can cook only Meat val alfredo = new MeatChef alfredo.cook(meatRecipe) alfredo.cook(whiteMeatRecipe) // Chef[WhiteMeat]<- Chef[Meat] <- Chef[Food]: Can cook any Food val mario = new GenericChef mario.cook(mixRecipe) mario.cook(meatRecipe) mario.cook(whiteMeatRecipe)
Restoran, tempat bersatu
Kami mempunyai resipi, kami mempunyai koki, sekarang kami memerlukan restoran di mana koki dapat memasak menu resipi.
trait Restaurant[A] { def menu: List[Recipe[A]] def chef: Chef[A] def cookMenu: List[String] = menu.map(chef.cook) }
Kami tidak berminat dengan hubungan subjenis antara restoran, jadi kami dapat menentukannya sebagai tidak berubah. Jenis invarian tidak mengikuti hubungan antara subtipe jenis komponen. Dengan kata lain, Restaurant[Food]
bukan subkelas atau superclass Restaurant[Meat]
. Mereka tidak berkaitan.
We will have a GenericRestaurant
, where you can eat different type of food. The MeatRestaurant
is specialised in meat-based dished and the WhiteMeatRestaurant
is specialised only in dishes based on white meat. Every restaurant to be instantiated needs a menu, that is a list of recipes, and a chef able to cook the recipes in the menu. Here is where the subtype relationship of Recipe
and Chef
comes into play.
case class GenericRestaurant(menu: List[Recipe[Food]], chef: Chef[Food]) extends Restaurant[Food]
case class MeatRestaurant(menu: List[Recipe[Meat]], chef: Chef[Meat]) extends Restaurant[Meat]
case class WhiteMeatRestaurant(menu: List[Recipe[WhiteMeat]], chef: Chef[WhiteMeat]) extends Restaurant[WhiteMeat]
Let's start defining some generic restaurants. In a generic restaurant, the menu is composed of recipes of various type of food. Since Recipe
is covariant, a GenericRecipe
is a superclass of MeatRecipe
and WhiteMeatRecipe
, so I can pass them to my GenericRestaurant
instance. The thing is different for the chef. If the Restaurant requires a chef that can cook generic food, I cannot put in it a chef able to cook only a specific one. The class Chef
is covariant, so GenericChef
is a subclass of MeatChef
that is a subclass of WhiteMeatChef
. This implies that I cannot pass to my instance anything different from GenericChef
.
val allFood = new GenericRestaurant(List(mixRecipe), mario) val foodParadise = new GenericRestaurant(List(meatRecipe), mario) val superFood = new GenericRestaurant(List(whiteMeatRecipe), mario)
Perkara yang sama berlaku MeatRestaurant
dan WhiteMeatRestaurant
. Saya boleh menyampaikan contoh hanya menu yang terdiri daripada resipi yang lebih spesifik daripada yang diperlukan, tetapi koki yang dapat memasak makanan lebih generik daripada yang diperlukan.
val meat4All = new MeatRestaurant(List(meatRecipe), alfredo) val meetMyMeat = new MeatRestaurant(List(whiteMeatRecipe), mario)
val notOnlyChicken = new WhiteMeatRestaurant(List(whiteMeatRecipe), giuseppe) val whiteIsGood = new WhiteMeatRestaurant(List(whiteMeatRecipe), alfredo) val wingsLovers = new WhiteMeatRestaurant(List(whiteMeatRecipe), mario)
Itu sahaja, empayar restoran kami siap menjana banyak wang!
Kesimpulannya
Baiklah, dalam kisah ini, saya melakukan yang terbaik untuk menjelaskan perbezaan jenis di Scala. Ini adalah topik lanjutan, tetapi perlu diketahui kerana ingin tahu. Saya harap contoh restoran dapat membantu menjadikannya lebih mudah difahami. Sekiranya ada sesuatu yang tidak jelas, atau jika saya menulis sesuatu yang salah (saya masih belajar!) Jangan ragu untuk memberikan komen!
Jumpa! ?