0Pricing
Scala for Backend Engineering & Functional Programming · บทเรียน

เจเนริกและพารามิเตอร์ชนิดข้อมูล

ทำความเข้าใจวิธีเขียนคลาสและเมธอดแบบเจเนริก เพื่อสร้างองค์ประกอบที่นำกลับมาใช้ซ้ำได้พร้อมความปลอดภัยด้านชนิดข้อมูล

เจเนริกและพารามิเตอร์ชนิดข้อมูล เป็นบทเรียน 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 ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. เจเนริกและพารามิเตอร์ชนิดข้อมูล
  2. ความแปรผัน: โคแวเรียนซ์และคอนทราแวเรียนซ์
  3. คลาสชนิดข้อมูลและอิมพลิซิต
← กลับไปที่ Scala for Backend Engineering & Functional Programming