เจเนริกและพารามิเตอร์ชนิดข้อมูล
ทำความเข้าใจวิธีเขียนคลาสและเมธอดแบบเจเนริก เพื่อสร้างองค์ประกอบที่นำกลับมาใช้ซ้ำได้พร้อมความปลอดภัยด้านชนิดข้อมูล
เจเนริกและพารามิเตอร์ชนิดข้อมูล เป็นบทเรียน Scala for Backend Engineering & Functional Programming ฟรีบน CoddyKit นี่คือบทเรียนที่ 1 จากทั้งหมด 3 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Scala for Backend Engineering & Functional Programming และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Scala for Backend Engineering & Functional Programming มีบทเรียนทั้งหมด 3 บทเรียน
เจเนริกส์คืออะไร
ลองจินตนาการว่าคุณต้องสร้างคอนเทนเนอร์ที่เก็บ ข้อมูลชนิดใดก็ได้ เช่น String, Int หรือออบเจ็กต์ที่กำหนดเอง หากไม่มีเจเนริกส์ คุณจะต้องเขียนคอนเทนเนอร์แยกสำหรับแต่ละชนิด ทำให้เกิดโค้ดซ้ำซ้อน หรือใช้ชนิดที่กว้างมากอย่าง Any
เจเนริกส์ช่วยให้คุณเขียนโค้ดที่ยืดหยุ่นและนำกลับมาใช้ใหม่ได้ ซึ่งทำงานกับชนิดข้อมูลหลากหลาย พร้อมยังคงมี ความปลอดภัยของชนิดข้อมูล ระหว่างการคอมไพล์ เปรียบเสมือนพิมพ์เขียวที่คุณสามารถเลือกวัสดุที่แตกต่างกันมาใส่ภายหลังได้
กับดักของชนิด `Any`
การใช้ Any เป็นตัวแทนของชนิดข้อมูลอาจก่อให้เกิดปัญหาได้ แม้ว่าจะช่วยให้คุณเก็บข้อมูลอะไรก็ได้ แต่คุณจะสูญเสียข้อมูลชนิดที่เจาะจงไป ซึ่งหมายความว่าบ่อยครั้งคุณต้อง แปลงชนิด ของข้อมูลกลับเป็นชนิดเดิม การทำเช่นนี้มีโอกาสเกิดข้อผิดพลาดสูง และอาจทำให้โปรแกรมหยุดทำงานขณะเรียกใช้ หากแปลงชนิดไม่ถูกต้อง
ลองเรียกใช้ตัวอย่างนี้และพิจารณาปัญหาที่อาจเกิดขึ้น:
class ItemHolder(val item: Any)
object Main {
def main(args: Array[String]): Unit = {
val stringHolder = new ItemHolder("Hello Scala")
val numberHolder = new ItemHolder(123)
// We need to cast, and it's unsafe if we get it wrong
val retrievedString = stringHolder.item.asInstanceOf[String]
println(s"Retrieved String: $retrievedString")
// If we uncomment the line below, it compiles but will crash at runtime!
// val wrongType = numberHolder.item.asInstanceOf[String]
// println(s"Wrong Type: $wrongType")
}
}แนะนำพารามิเตอร์ชนิดข้อมูล
เจเนริกส์แก้ปัญหากับดักของชนิด Any ด้วยการแนะนำ พารามิเตอร์ชนิดข้อมูล ซึ่งทำหน้าที่เหมือนตัวแทนของชนิดข้อมูลจริงที่คุณระบุเมื่อสร้างอินสแตนซ์ของคลาสหรือเรียกใช้เมธอด
ใน Scala พารามิเตอร์ชนิดข้อมูลจะกำหนดโดยใช้ วงเล็บเหลี่ยม ([]) ต่อท้ายชื่อคลาสหรือเมธอด โดยทั่วไปจะใช้อักษรตัวพิมพ์ใหญ่เดี่ยว เช่น T, A, B เป็นต้น ตัวอย่างเช่น class Box[T] จะประกาศคลาสทั่วไปชื่อ Box ที่รับพารามิเตอร์ชนิดข้อมูลหนึ่งตัว คือ T
สร้างกล่องแบบเจเนริก
มาสร้างคลาสทั่วไปอย่างง่ายชื่อ Box ที่สามารถเก็บเนื้อหาชนิดใดก็ได้กันครับ สังเกตว่า T ถูกใช้ตลอดการประกาศคลาสเพื่อแทนชนิดของเนื้อหา
วิธีนี้ทำให้เราสร้างกล่องสำหรับสตริง จำนวนเต็ม หรือชนิดข้อมูลอื่น ๆ ได้ โดยไม่ต้องเขียนคลาสแยกสำหรับแต่ละชนิด
class Box[T](val content: T) {
def get: T = content
override def toString: String = s"Box($content)"
}
object Main {
def main(args: Array[String]): Unit = {
val stringBox = new Box("Scala is fun!")
val intBox = new Box(42)
val booleanBox = new Box(true)
println(stringBox)
println(intBox)
println(booleanBox)
}
}ความปลอดภัยของชนิดข้อมูลด้วยเจเนริกส์
ข้อดีที่สำคัญที่สุดของเจเนริกส์คือ ความปลอดภัยของชนิดข้อมูลระหว่างการคอมไพล์ เมื่อคุณสร้าง Box[String] คอมไพเลอร์จะทราบว่าภายในควรมีเฉพาะสตริงเท่านั้น หากคุณพยายามใส่จำนวนเต็มลงไปหรือดึงข้อมูลออกมาเป็นจำนวนเต็ม คอมไพเลอร์จะแจ้งข้อผิดพลาดให้ทราบทันที
วิธีนี้ช่วยป้องกันข้อผิดพลาดขณะเรียกใช้ที่พบบ่อย ซึ่งอาจเกิดจากแนวทางที่ปลอดภัยด้านชนิดข้อมูลน้อยกว่า เช่น การใช้ Any
class Box[T](val content: T) {
def get: T = content
}
object Main {
def main(args: Array[String]): Unit = {
val myStringBox: Box[String] = new Box("CoddyKit")
val myIntBox: Box[Int] = new Box(100)
// This works, type is String
val s: String = myStringBox.get
println(s"String from box: $s")
// This works, type is Int
val i: Int = myIntBox.get
println(s"Int from box: $i")
// The compiler prevents this line from compiling:
// val wrong: String = myIntBox.get // Type mismatch error!
}
}เมธอดก็ใช้เจเนริกส์ได้เช่นกัน
เช่นเดียวกับคลาส เมธอดแต่ละเมธอดก็สามารถเป็นเจเนริกได้เช่นกัน เมธอดแบบเจเนริก สามารถรับพารามิเตอร์ชนิดข้อมูลที่ใช้เฉพาะภายในเมธอดนั้น ทำให้เมธอดทำงานกับชนิดข้อมูลต่าง ๆ ได้ โดยไม่จำเป็นต้องทำให้ทั้งคลาสเป็นเจเนริก
วิธีนี้มีประโยชน์เมื่อคุณมีการดำเนินการเฉพาะอย่างที่ใช้ได้กับชนิดข้อมูลหลายแบบ แต่ตัวคลาสเองไม่ใช่คอนเทนเนอร์แบบเจเนริก
เมธอด `printPair` ที่ยืดหยุ่น
ต่อไปนี้คือตัวอย่างเมธอดแบบเจเนริก printPair เมธอดนี้รับพารามิเตอร์ชนิดข้อมูลสองตัว คือ A และ B จึงสามารถพิมพ์คู่ข้อมูลที่มีชนิดใดก็ได้สองชนิด สังเกตว่าพารามิเตอร์ชนิดข้อมูลถูกประกาศทันทีหลังชื่อเมธอด
ทำให้เมธอดนี้นำกลับมาใช้ใหม่ได้อย่างยืดหยุ่นกับชุดข้อมูลหลายรูปแบบ
object Util {
def printPair[A, B](first: A, second: B): Unit = {
println(s"Pair: ($first, $second)")
println(s"Type of first: ${first.getClass.getName}")
println(s"Type of second: ${second.getClass.getName}")
}
}
object Main {
def main(args: Array[String]): Unit = {
Util.printPair("Hello", 123)
Util.printPair(true, List(1, 2, 3))
Util.printPair(4.5, 'C')
}
}การอนุมานชนิดข้อมูลอัจฉริยะ
หนึ่งในความสามารถอันทรงพลังของ Scala คือ การอนุมานชนิดข้อมูล เมื่อใช้คลาสหรือเมธอดแบบเจเนริก คุณมักไม่จำเป็นต้องระบุพารามิเตอร์ชนิดข้อมูลอย่างชัดเจน คอมไพเลอร์ของ Scala ฉลาดพอที่จะอนุมานชนิดเหล่านั้นจากอาร์กิวเมนต์ที่คุณส่งให้
ทำให้โค้ดกระชับและอ่านง่ายขึ้น โดยไม่สูญเสียความปลอดภัยของชนิดข้อมูล
class Container[T](val item: T) {
def getItem: T = item
}
object Main {
def main(args: Array[String]): Unit = {
// Scala infers T as String
val stringContainer = new Container("Inferred String")
println(s"Content: ${stringContainer.getItem}, Type: ${stringContainer.getItem.getClass.getName}")
// Scala infers T as Double
val doubleContainer = new Container(3.14)
println(s"Content: ${doubleContainer.getItem}, Type: ${doubleContainer.getItem.getClass.getName}")
// You can also specify explicitly, but often not needed
val explicitIntContainer: Container[Int] = new Container(500)
println(s"Content: ${explicitIntContainer.getItem}, Type: ${explicitIntContainer.getItem.getClass.getName}")
}
}จำกัดชนิดข้อมูลเจเนริกด้วยขอบเขต
บางครั้งคุณต้องการให้คลาสหรือเมธอดแบบเจเนริกทำงานเฉพาะกับชนิดข้อมูลที่มีความสามารถบางอย่างเท่านั้น ตัวอย่างเช่น เมธอดที่บวกจำนวนควรรับเฉพาะชนิดข้อมูลตัวเลข
ขอบเขตชนิดข้อมูล ช่วยจำกัดชนิดข้อมูลที่สามารถใช้เป็นพารามิเตอร์ชนิดข้อมูลได้ ขอบเขตด้านบน (<:) หมายความว่าพารามิเตอร์ชนิดข้อมูลต้องเป็นชนิดย่อยของชนิดข้อมูลที่กำหนด ตัวอย่างเช่น [T <: Animal] หมายความว่า T ต้องเป็น Animal หรือชนิดย่อยของ Animal
class Animal { def speak(): String = "..." }
class Dog extends Animal { override def speak(): String = "Woof!" }
class Cat extends Animal { override def speak(): String = "Meow!" }
// This box can only hold types that are Animal or its subtypes
class AnimalShelter[T <: Animal](val animal: T) {
def getAnimalSound: String = animal.speak()
}
object Main {
def main(args: Array[String]): Unit = {
val dogShelter = new AnimalShelter(new Dog())
println(s"Dog says: ${dogShelter.getAnimalSound}")
val catShelter = new AnimalShelter(new Cat())
println(s"Cat says: ${catShelter.getAnimalSound}")
// This would NOT compile because Car is not an Animal:
// class Car
// val carShelter = new AnimalShelter(new Car())
}
}โจทย์ท้าทายคลาสแบบเจเนริก
ได้เวลาตรวจสอบความเข้าใจเรื่องเจเนริกส์ของคุณแล้วครับ
อ่านข้อความต่อไปนี้อย่างละเอียด แล้วเลือกทุกข้อที่ถูกต้องเกี่ยวกับคลาสและเมธอดแบบเจเนริกใน Scala
เจเนริกส์: ประเด็นสำคัญ
ในบทเรียนนี้ เราได้เรียนรู้เจเนริกส์ของ Scala ซึ่งเป็นความสามารถอันทรงพลังสำหรับเขียนโค้ดที่ยืดหยุ่นและปลอดภัยด้านชนิดข้อมูล
- เจเนริกส์ ช่วยให้คลาสและเมธอดทำงานกับชนิดข้อมูลต่าง ๆ ได้
- ใช้ พารามิเตอร์ชนิดข้อมูล (เช่น
[T]) เป็นตัวแทนของชนิดข้อมูลจริง - เจเนริกส์มอบ ความปลอดภัยของชนิดข้อมูลระหว่างการคอมไพล์ ซึ่งช่วยป้องกันข้อผิดพลาดขณะเรียกใช้
- การอนุมานชนิดข้อมูล ของ Scala มักช่วยให้โค้ดแบบเจเนริกกระชับ
- ขอบเขตชนิดข้อมูล (เช่น
<:) สามารถจำกัดพารามิเตอร์ชนิดข้อมูลให้เป็นชนิดย่อยบางประเภทได้
การใช้เจเนริกส์ช่วยให้คุณเขียนองค์ประกอบที่นำกลับมาใช้ใหม่ได้ แข็งแกร่ง และปรับใช้กับชนิดข้อมูลหลากหลาย ทำให้โค้ด Scala มีประสิทธิภาพมากขึ้นและเกิดข้อผิดพลาดน้อยลง
คำถามที่พบบ่อย
บทเรียน “เจเนริกและพารามิเตอร์ชนิดข้อมูล” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “เจเนริกและพารามิเตอร์ชนิดข้อมูล” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Scala for Backend Engineering & Functional Programming ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Scala for Backend Engineering & Functional Programming มีบทเรียนทั้งหมด 3 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “เจเนริกและพารามิเตอร์ชนิดข้อมูล”
ทำความเข้าใจวิธีเขียนคลาสและเมธอดแบบเจเนริก เพื่อสร้างองค์ประกอบที่นำกลับมาใช้ซ้ำได้พร้อมความปลอดภัยด้านชนิดข้อมูล คุณปฏิบัติ Scala for Backend Engineering & Functional Programming ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Scala for Backend Engineering & Functional Programming หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Scala for Backend Engineering & Functional Programming บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 1 จากทั้งหมด 3 บทเรียน
บทเรียน “เจเนริกและพารามิเตอร์ชนิดข้อมูล” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Scala for Backend Engineering & Functional Programming นี้ได้ไหม
ได้ บทเรียน Scala for Backend Engineering & Functional Programming ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- เจเนริกและพารามิเตอร์ชนิดข้อมูล
- ความแปรผัน: โคแวเรียนซ์และคอนทราแวเรียนซ์
- คลาสชนิดข้อมูลและอิมพลิซิต