การประกาศคลาสเคสสกาล่ามีข้อเสียอะไรบ้าง?


105

หากคุณกำลังเขียนโค้ดที่ใช้โครงสร้างข้อมูลที่สวยงามและไม่เปลี่ยนรูปจำนวนมากคลาสเคสดูเหมือนจะเป็นสิ่งที่มาจากสวรรค์ทำให้คุณได้รับสิ่งต่อไปนี้ทั้งหมดฟรีด้วยคีย์เวิร์ดเดียว:

  • ทุกอย่างไม่เปลี่ยนรูปตามค่าเริ่มต้น
  • Getters กำหนดโดยอัตโนมัติ
  • การใช้งาน toString () ที่เหมาะสม
  • สอดคล้องกับ () และ hashCode ()
  • อ็อบเจ็กต์ Companion ด้วยวิธี unapply () สำหรับการจับคู่

แต่อะไรคือข้อเสียของการกำหนดโครงสร้างข้อมูลที่ไม่เปลี่ยนรูปเป็นคลาสเคส?

มีข้อ จำกัด อะไรบ้างในชั้นเรียนหรือลูกค้า

มีสถานการณ์ที่คุณควรเลือกคลาสที่ไม่ใช่เคสหรือไม่?


ดูคำถามที่เกี่ยวข้องนี้: stackoverflow.com/q/4635765/156410
David

18
เหตุใดจึงไม่สร้างสรรค์ ม็อดในไซต์นี้เข้มงวดเกินไป คำตอบที่เป็นข้อเท็จจริงที่เป็นไปได้มีจำนวน จำกัด
Eloff

5
เห็นด้วยกับ Eloff นี่เป็นคำถามที่ฉันต้องการคำตอบเช่นกันและคำตอบที่ให้มาก็มีประโยชน์มากและไม่ปรากฏเป็นอัตนัย ฉันได้เห็นคำถาม 'วิธีแก้ไขรหัสที่ตัดตอนมา' จำนวนมากซึ่งทำให้เกิดการถกเถียงและแสดงความคิดเห็นมากขึ้น
Herc

คำตอบ:


51

ข้อเสียใหญ่อย่างหนึ่ง: คลาสเคสไม่สามารถขยายคลาสเคสได้ นั่นคือข้อ จำกัด

ข้อดีอื่น ๆ ที่คุณพลาดระบุไว้เพื่อความสมบูรณ์: การทำให้เป็นอนุกรม / การแยกสารที่เป็นไปตามข้อกำหนดไม่จำเป็นต้องใช้คำหลัก "ใหม่" ในการสร้าง

ฉันชอบคลาสที่ไม่ใช่เคสสำหรับอ็อบเจ็กต์ที่มีสถานะไม่แน่นอนรัฐส่วนตัวหรือไม่มีสถานะ (เช่นส่วนประกอบเดี่ยวส่วนใหญ่) คลาสเคสสำหรับทุกอย่างอื่น ๆ


48
คุณสามารถย่อยคลาสเคสได้ คลาสย่อยไม่สามารถเป็นคลาสเคสได้เช่นกันนั่นคือข้อ จำกัด
Seth Tisue

99

ก่อนอื่นบิตที่ดี:

ทุกอย่างไม่เปลี่ยนรูปตามค่าเริ่มต้น

ได้และยังสามารถลบล้างได้ (โดยใช้var) หากคุณต้องการ

Getters กำหนดโดยอัตโนมัติ

เป็นไปได้ในคลาสใด ๆ โดยนำหน้าพารามิเตอร์ด้วย val

toString()การใช้งานที่เหมาะสม

ใช่มีประโยชน์มาก แต่สามารถทำได้ด้วยตนเองในทุกชั้นเรียนหากจำเป็น

ได้มาตรฐานequals()และhashCode()

เมื่อรวมกับการจับคู่รูปแบบที่ง่ายดายนี่คือเหตุผลหลักที่ผู้คนใช้คลาสเคส

วัตถุร่วมด้วยunapply()วิธีการจับคู่

นอกจากนี้ยังสามารถทำได้ด้วยมือในทุกคลาสโดยใช้ตัวแยก

รายการนี้ควรรวมถึงวิธีการคัดลอกที่ทรงพลังซึ่งเป็นหนึ่งในสิ่งที่ดีที่สุดที่จะมาถึง Scala 2.8


สิ่งที่เลวร้ายมีข้อ จำกัด ที่แท้จริงเพียงไม่กี่ข้อกับคลาสเคส:

คุณไม่สามารถกำหนดapplyในอ็อบเจ็กต์ที่แสดงร่วมโดยใช้ลายเซ็นเดียวกันกับวิธีที่สร้างโดยคอมไพเลอร์

แม้ว่าในทางปฏิบัติจะไม่ค่อยมีปัญหา การเปลี่ยนพฤติกรรมของวิธีการใช้งานที่สร้างขึ้นนั้นรับประกันได้ว่าจะทำให้ผู้ใช้ประหลาดใจและควรหลีกเลี่ยงอย่างยิ่งเหตุผลเดียวในการทำเช่นนั้นคือการตรวจสอบพารามิเตอร์อินพุต - งานที่ทำได้ดีที่สุดในเนื้อหาตัวสร้างหลัก (ซึ่งทำให้การตรวจสอบพร้อมใช้งานเมื่อใช้งานcopy)

คุณไม่สามารถคลาสย่อยได้

จริงอยู่แม้ว่าจะยังคงเป็นไปได้ที่คลาสเคสจะเป็นลูกหลาน รูปแบบทั่วไปอย่างหนึ่งคือการสร้างลำดับชั้นของลักษณะโดยใช้คลาสเคสเป็นโหนดใบไม้ของต้นไม้

นอกจากนี้ยังควรสังเกตsealedตัวปรับแต่ง คลาสย่อยใด ๆ ของคุณสมบัติที่มีตัวปรับแต่งนี้จะต้องถูกประกาศในไฟล์เดียวกัน เมื่อจับคู่รูปแบบกับอินสแตนซ์ของลักษณะคอมไพเลอร์จะเตือนคุณได้หากคุณยังไม่ได้ตรวจสอบคลาสย่อยที่เป็นไปได้ทั้งหมด เมื่อรวมกับคลาสเคสจะทำให้คุณมีความมั่นใจในระดับสูงมากในโค้ดของคุณหากคอมไพล์โดยไม่มีการเตือนล่วงหน้า

ในฐานะที่เป็นคลาสย่อยของ Product คลาสเคสต้องมีพารามิเตอร์ไม่เกิน 22 ตัว

ไม่มีวิธีแก้ปัญหาที่แท้จริงยกเว้นการหยุดการใช้ชั้นเรียนที่ไม่เหมาะสมด้วยพารามิเตอร์มากมายนี้ :)

นอกจากนี้ ...

ข้อ จำกัด อีกประการหนึ่งที่บางครั้งระบุไว้คือ Scala ไม่ (ในปัจจุบัน) รองรับ lazy params (เช่นlazy vals แต่เป็นพารามิเตอร์) วิธีแก้ปัญหานี้คือการใช้พารามิเตอร์ตามชื่อและกำหนดให้กับ lazy val ในตัวสร้าง น่าเสียดายที่พารามิเตอร์ตามชื่อไม่ผสมกับการจับคู่รูปแบบซึ่งจะป้องกันไม่ให้ใช้เทคนิคกับคลาสเคสเนื่องจากจะทำลายตัวแยกที่สร้างโดยคอมไพเลอร์

สิ่งนี้มีความเกี่ยวข้องหากคุณต้องการใช้โครงสร้างข้อมูลขี้เกียจที่ใช้งานได้สูงและหวังว่าจะได้รับการแก้ไขด้วยการเพิ่มพารามิเตอร์ขี้เกียจลงใน Scala ในอนาคต


1
ขอบคุณสำหรับคำตอบที่ครอบคลุม ฉันคิดว่าทุกอย่างยกเว้น "คุณไม่สามารถคลาสย่อย" ไม่น่าจะเฟสฉันเร็ว ๆ นี้
Graham Lea

15
คุณสามารถย่อยคลาสเคสได้ คลาสย่อยไม่สามารถเป็นคลาสเคสได้เช่นกันนั่นคือข้อ จำกัด
Seth Tisue

5
ขีด จำกัด 22 พารามิเตอร์สำหรับคลาสเคสจะถูกลบออกใน Scala 2.11 issue.scala-lang.org/browse/SI-7296
Jonathan Crosmer

ไม่ถูกต้องที่จะยืนยันว่า "คุณไม่สามารถกำหนดใช้ในอ็อบเจ็กต์ที่แสดงร่วมโดยใช้ลายเซ็นเดียวกับเมธอดที่สร้างโดยคอมไพเลอร์" แม้ว่าจะต้องกระโดดผ่านห่วงบางอย่างในการทำเช่นนั้น (หากคุณตั้งใจที่จะเก็บฟังก์ชันการทำงานที่เคยสร้างขึ้นโดยคอมไพเลอร์ส
กาลาอย่างสุดลูกหูลูกตา

ฉันใช้คลาสเคสของ Scala อย่างแพร่หลายและได้สร้าง "case class pattern" (ซึ่งสุดท้ายจะกลายเป็นมาโคร Scala) ซึ่งช่วยแก้ปัญหาต่างๆที่ระบุไว้ข้างต้น: codereview.stackexchange.com/a/98367 / 4758
อลวน 3quilibrium

10

ฉันคิดว่าหลักการ TDD ใช้ที่นี่: อย่าออกแบบมากเกินไป เมื่อคุณประกาศว่าเป็น a case classคุณกำลังประกาศฟังก์ชันการทำงานมากมาย ซึ่งจะลดความยืดหยุ่นที่คุณมีในการเปลี่ยนคลาสในอนาคต

ตัวอย่างเช่น a case classมีequalsวิธีการเหนือพารามิเตอร์ตัวสร้าง คุณอาจไม่สนใจเรื่องนี้เมื่อคุณเขียนชั้นเรียนเป็นครั้งแรก แต่ในภายหลังอาจตัดสินใจว่าคุณต้องการความเท่าเทียมกันโดยไม่สนใจพารามิเตอร์เหล่านี้บางส่วนหรือทำอะไรที่แตกต่างออกไปเล็กน้อย อย่างไรก็ตามรหัสไคลเอ็นต์อาจเขียนในช่วงเวลาเฉลี่ยที่ขึ้นอยู่กับcase classความเท่าเทียมกัน


4
ฉันไม่คิดว่ารหัสไคลเอ็นต์ควรขึ้นอยู่กับความหมายที่แท้จริงของ 'เท่ากับ' ขึ้นอยู่กับชั้นเรียนที่จะตัดสินใจว่า 'เท่ากับ' หมายถึงอะไร ผู้เขียนชั้นเรียนควรมีอิสระที่จะเปลี่ยนการใช้ 'เท่ากับ' ในบรรทัด
pkaeding

8
@pkaeding คุณมีอิสระที่จะไม่มีรหัสไคลเอนต์ขึ้นอยู่กับวิธีการส่วนตัวใด ๆ ทุกสิ่งที่เปิดเผยต่อสาธารณะเป็นสัญญาที่คุณตกลง
Daniel C. Sobral

3
@ DanielC.Sobral True แต่การใช้งานเท่ากับ () (ฟิลด์ใดที่ขึ้นอยู่กับ) ไม่จำเป็นต้องอยู่ในสัญญา อย่างน้อยที่สุดคุณสามารถแยกออกจากสัญญาได้อย่างชัดเจนเมื่อคุณเขียนชั้นเรียนครั้งแรก
herman

2
@ DanielC.Sobral คุณกำลังขัดแย้งกับตัวเอง: คุณบอกว่าผู้คนจะพึ่งพาการใช้ค่าเริ่มต้นเท่ากับการใช้งาน (ซึ่งเปรียบเทียบเอกลักษณ์ของวัตถุ) หากเป็นเช่นนั้นจริงและคุณเขียนการใช้งานเท่ากับที่แตกต่างกันในภายหลังโค้ดของพวกเขาก็จะพังเช่นกัน อย่างไรก็ตามหากคุณระบุเงื่อนไขก่อน / โพสต์และค่าคงที่และผู้คนไม่สนใจพวกเขานั่นคือปัญหาของพวกเขา
herman

2
@herman ไม่มีความขัดแย้งในสิ่งที่ฉันพูด สำหรับ "ปัญหาของพวกเขา" แน่นอนเว้นแต่มันจะกลายเป็นปัญหาของคุณ ตัวอย่างเช่นพูดว่าเนื่องจากพวกเขาเป็นลูกค้ารายใหญ่ของสตาร์ทอัพของคุณหรือเพราะผู้จัดการของพวกเขาโน้มน้าวผู้บริหารระดับสูงว่ามีค่าใช้จ่ายสูงเกินไปสำหรับการเปลี่ยนแปลงดังนั้นคุณต้องยกเลิกการเปลี่ยนแปลงหรือเนื่องจากการเปลี่ยนแปลงทำให้เกิดเงินหลายล้านดอลลาร์ ข้อผิดพลาดและได้รับการเปลี่ยนกลับเป็นต้น แต่ถ้าคุณกำลังเขียนโค้ดเพื่องานอดิเรกและไม่สนใจผู้ใช้ให้ดำเนินการต่อ
Daniel C. Sobral

7

มีสถานการณ์ที่คุณควรเลือกคลาสที่ไม่ใช่เคสหรือไม่?

Martin Odersky ทำให้เรามีจุดเริ่มต้นที่ดีในหลักสูตรFunctional Programming Principles ใน Scala (Lecture 4.6 - Pattern Matching) ที่เราสามารถใช้เมื่อต้องเลือกระหว่างคลาสและคลาสเคส บทที่ 7 ของScala By Exampleมีตัวอย่างเดียวกัน

สมมติว่าเราต้องการเขียนล่ามสำหรับนิพจน์เลขคณิต เพื่อให้สิ่งต่างๆง่ายขึ้นในตอนแรกเรา จำกัด ตัวเองไว้ที่ตัวเลขและการดำเนินการ + เท่านั้น expres- sions ดังกล่าวสามารถแสดงเป็นลำดับชั้นของคลาสโดยมี Expr คลาสพื้นฐานที่เป็นนามธรรมเป็นรูทและสองคลาสย่อย Number และ Sum จากนั้นนิพจน์ 1 + (3 + 7) จะแสดงเป็น

ผลรวมใหม่ (หมายเลขใหม่ (1) ผลรวมใหม่ (หมายเลขใหม่ (3) หมายเลขใหม่ (7)))

abstract class Expr {
  def eval: Int
}

class Number(n: Int) extends Expr {
  def eval: Int = n
}

class Sum(e1: Expr, e2: Expr) extends Expr {
  def eval: Int = e1.eval + e2.eval
}

นอกจากนี้การเพิ่มคลาส Prod ใหม่ไม่ได้ทำให้เกิดการเปลี่ยนแปลงใด ๆ กับโค้ดที่มีอยู่:

class Prod(e1: Expr, e2: Expr) extends Expr {
  def eval: Int = e1.eval * e2.eval
}

ในทางตรงกันข้ามการเพิ่มวิธีการใหม่ต้องมีการปรับเปลี่ยนคลาสที่มีอยู่ทั้งหมด

abstract class Expr { 
  def eval: Int 
  def print
} 

class Number(n: Int) extends Expr { 
  def eval: Int = n 
  def print { Console.print(n) }
}

class Sum(e1: Expr, e2: Expr) extends Expr { 
  def eval: Int = e1.eval + e2.eval
  def print { 
   Console.print("(")
   print(e1)
   Console.print("+")
   print(e2)
   Console.print(")")
  }
}

ปัญหาเดียวกันได้รับการแก้ไขด้วยคลาสเคส

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
  }
}
case class Number(n: Int) extends Expr
case class Sum(e1: Expr, e2: Expr) extends Expr

การเพิ่มวิธีการใหม่เป็นการเปลี่ยนแปลงเฉพาะที่

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
  }
  def print = this match {
    case Number(n) => Console.print(n)
    case Sum(e1,e2) => {
      Console.print("(")
      print(e1)
      Console.print("+")
      print(e2)
      Console.print(")")
    }
  }
}

การเพิ่มคลาส Prod ใหม่อาจต้องเปลี่ยนการจับคู่รูปแบบทั้งหมด

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
    case Prod(e1,e2) => e1.eval * e2.eval
  }
  def print = this match {
    case Number(n) => Console.print(n)
    case Sum(e1,e2) => {
      Console.print("(")
      print(e1)
      Console.print("+")
      print(e2)
      Console.print(")")
    }
    case Prod(e1,e2) => ...
  }
}

ถอดเสียงจาก videolecture 4.6 Pattern Matching

การออกแบบทั้งสองแบบนี้ทำได้ดีอย่างสมบูรณ์แบบและบางครั้งการเลือกระหว่างพวกเขาก็เป็นเรื่องของสไตล์ แต่ถึงกระนั้นก็มีเกณฑ์บางอย่างที่สำคัญ

เกณฑ์หนึ่งอาจเป็นได้คุณสร้างนิพจน์ย่อยใหม่บ่อยขึ้นหรือคุณสร้างเมธอดใหม่บ่อยขึ้น? ดังนั้นจึงเป็นเกณฑ์ที่พิจารณาถึงความสามารถในการขยายในอนาคตและส่วนขยายที่เป็นไปได้ของระบบของคุณ

หากสิ่งที่คุณทำส่วนใหญ่เป็นการสร้างคลาสย่อยใหม่จริง ๆ แล้วโซลูชันการสลายตัวที่มุ่งเน้นวัตถุจะเป็นมือบน เหตุผลก็คือมันง่ายมากและเป็นการเปลี่ยนแปลงในท้องถิ่นเพียงแค่สร้างคลาสย่อยใหม่ด้วยวิธีการ evalโดยที่ในโซลูชันการทำงานคุณจะต้องย้อนกลับและเปลี่ยนรหัสภายในวิธีการ eval และเพิ่มกรณีใหม่ ไปเลย

ในทางกลับกันหากสิ่งที่คุณทำจะเป็นการสร้างวิธีการใหม่ ๆ มากมาย แต่ลำดับชั้นของคลาสนั้นจะยังคงค่อนข้างคงที่ดังนั้นการจับคู่รูปแบบจึงเป็นประโยชน์อย่างแท้จริง เพราะอีกครั้งวิธีการใหม่แต่ละวิธีในโซลูชันการจับคู่รูปแบบเป็นเพียงการเปลี่ยนแปลงเฉพาะที่ไม่ว่าคุณจะวางไว้ในคลาสฐานหรืออาจจะอยู่นอกลำดับชั้นของคลาส ในขณะที่วิธีการใหม่เช่นการแสดงในการสลายตัวเชิงวัตถุจะต้องมีการเพิ่มขึ้นใหม่คือแต่ละคลาสย่อย ดังนั้นจะมีหลายส่วนที่คุณต้องสัมผัส

ดังนั้นปัญหาของการขยายนี้ในสองมิติที่คุณอาจต้องการที่จะเพิ่มชั้นเรียนใหม่ในการลำดับชั้นหรือคุณอาจต้องการที่จะเพิ่มวิธีการใหม่หรืออาจจะทั้งสองได้รับการตั้งชื่อปัญหาการแสดงออก

โปรดจำไว้ว่าเราต้องใช้สิ่งนี้เป็นจุดเริ่มต้นและไม่เหมือนกับเกณฑ์เดียว

ป้อนคำอธิบายภาพที่นี่


0

ฉันกำลัง quoting นี้จากScala cookbookจากAlvin Alexanderบทที่ objects6:

นี่เป็นหนึ่งในหลาย ๆ สิ่งที่ฉันพบว่าน่าสนใจในหนังสือเล่มนี้

ในการจัดหาตัวสร้างหลายตัวสำหรับคลาสเคสสิ่งสำคัญคือต้องรู้ว่าการประกาศคลาสเคสทำอะไรได้จริง

case class Person (var name: String)

หากคุณดูโค้ดที่คอมไพลเลอร์ Scala สร้างขึ้นสำหรับตัวอย่างคลาสเคสคุณจะเห็นว่ามันสร้างไฟล์เอาต์พุตสองไฟล์คือ Person $ .class และ Person.class หากคุณแยกชิ้นส่วนบุคคล $ .class ด้วยคำสั่ง javap คุณจะเห็นว่ามีวิธีการใช้งานพร้อมด้วยวิธีอื่น ๆ อีกมากมาย:

$ javap Person$
Compiled from "Person.scala"
public final class Person$ extends scala.runtime.AbstractFunction1 implements scala.ScalaObject,scala.Serializable{
public static final Person$ MODULE$;
public static {};
public final java.lang.String toString();
public scala.Option unapply(Person);
public Person apply(java.lang.String); // the apply method (returns a Person) public java.lang.Object readResolve();
        public java.lang.Object apply(java.lang.Object);
    }

คุณยังสามารถแยกชิ้นส่วน Person.class เพื่อดูว่ามีอะไรบ้าง สำหรับคลาสง่ายๆเช่นนี้จะมีวิธีการเพิ่มเติมอีก 20 วิธี การขยายตัวที่ซ่อนอยู่นี้เป็นเหตุผลหนึ่งที่นักพัฒนาบางคนไม่ชอบคลาสเคส

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.