เหตุใด C # จึงสร้างด้วยคำหลัก“ ใหม่” และ“ เสมือน + แทนที่” ซึ่งไม่เหมือนกับ Java


61

ใน Java ไม่มีvirtual, new, overrideคำหลักสำหรับความหมายวิธีการ ดังนั้นการทำงานของวิธีจึงง่ายต่อการเข้าใจ สาเหตุถ้าDerivedClassขยายBaseClassและมีเมธอดที่มีชื่อเดียวกันและลายเซ็นเดียวกันของBaseClass การแทนที่จะเกิดขึ้นที่ polymorphism แบบรันไทม์ (จัดเตรียมเมธอดไม่ใช่static)

BaseClass bcdc = new DerivedClass(); 
bcdc.doSomething() // will invoke DerivedClass's doSomething method.

ตอนนี้มาถึง C # จะมีความสับสนมากและยากที่จะเข้าใจวิธีการnewหรือvirtual+deriveหรือใหม่ + เสมือนแทนที่คือการทำงาน

ฉันไม่สามารถเข้าใจว่าทำไมในโลกนี้ฉันจะเพิ่มวิธีการในDerivedClassชื่อและลายเซ็นเดียวกันกับBaseClassและกำหนดพฤติกรรมใหม่ แต่ที่ polymorphism เวลาทำงานBaseClassวิธีการจะถูกเรียก! (ซึ่งไม่ใช่การเอาชนะ แต่ควรมีเหตุผล)

ในกรณีที่virtual + overrideแม้ว่าการใช้งานเชิงตรรกะที่ถูกต้อง แต่โปรแกรมเมอร์ต้องคิดว่าวิธีการที่เขาควรให้สิทธิ์แก่ผู้ใช้ในการแทนที่ในเวลาที่เข้ารหัส ซึ่งมีข้อดีบางอย่าง(อย่าไปที่นั่นตอนนี้)

เหตุใดใน C # จึงมีพื้นที่เหลือเฟือสำหรับการใช้เหตุผลและความสับสน ดังนั้นฉันจึงอาจ reframe คำถามของฉันเป็นที่บริบทของโลกแห่งความจริงฉันควรคิดว่าการใช้งานvirtual + overrideแทนnewและยังใช้newแทนvirtual + override?


หลังจากคำตอบที่ดีโดยเฉพาะอย่างยิ่งจากOmarฉันได้รับนักออกแบบ C # ให้ความสำคัญกับโปรแกรมเมอร์มากขึ้นควรคิดก่อนที่พวกเขาจะสร้างวิธีการที่ดีและจัดการข้อผิดพลาดใหม่ ๆ จาก Java

ตอนนี้ฉันมีคำถามอยู่ในใจ เช่นเดียวกับใน Java ถ้าฉันมีรหัสเช่น

Vehicle vehicle = new Car();
vehicle.accelerate();

และต่อมาฉันจะทำให้ระดับใหม่ที่ได้มาจากSpaceShip Vehicleจากนั้นฉันต้องการเปลี่ยนทั้งหมดcarเป็นSpaceShipวัตถุที่ฉันต้องเปลี่ยนรหัสบรรทัดเดียว

Vehicle vehicle = new SpaceShip();
vehicle.accelerate();

สิ่งนี้จะไม่ทำลายตรรกะของฉันที่จุดใด ๆ ของรหัส

แต่ในกรณีของ C # หากSpaceShipไม่ได้แทนที่Vehicleคลาส ' accelerateและใช้newแล้วตรรกะของรหัสของฉันจะถูกทำลาย นั่นเป็นข้อเสียหรือไม่?


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

12
IIRC, C # ทำเช่นนี้เพื่อ "ให้ความชัดเจน" หากคุณต้องพูดว่า "ใหม่" หรือ "แทนที่" อย่างชัดเจนมันทำให้เกิดความชัดเจนและชัดเจนในทันทีว่าเกิดอะไรขึ้นมากกว่าที่คุณต้องพยายามคิดหาวิธีที่จะเอาชนะพฤติกรรมบางอย่างในคลาสฐานหรือไม่ ฉันยังพบว่ามีประโยชน์มากที่จะสามารถระบุวิธีการที่ฉันต้องการระบุเป็นเสมือนจริงและวิธีการที่ฉันไม่ชอบ (Java ทำสิ่งนี้ด้วยfinal; มันเป็นวิธีที่ตรงกันข้าม)
Robert Harvey

15
“ใน Java ไม่มีเสมือนใหม่แทนที่คำหลักสำหรับความหมายวิธีการ.” @Overrideยกเว้นมี
svick


3
คำอธิบายประกอบ @svick และคำหลักไม่เหมือนกัน :)
Anirban Nag 'tintinmj'

คำตอบ:


86

เนื่องจากคุณถามว่าเหตุใด C # จึงทำเช่นนี้จึงเป็นการดีที่สุดที่จะถามผู้สร้าง C # Anders Hejlsberg สถาปนิกนำสำหรับ C #, ตอบว่าทำไมพวกเขาเลือกที่จะไม่ไปกับเสมือนโดยค่าเริ่มต้น (เช่นใน Java) ในการสัมภาษณ์ , ตัวอย่างที่เกี่ยวข้องอยู่ด้านล่าง

โปรดทราบว่า Java มีเสมือนเป็นค่าเริ่มต้นด้วยคำหลักสุดท้ายเพื่อทำเครื่องหมายวิธีการที่ไม่ใช่เสมือน ยังคงเป็นสองแนวคิดในการเรียนรู้ แต่คนจำนวนมากไม่ทราบเกี่ยวกับคำหลักสุดท้ายหรือไม่ใช้เชิงรุก C # บังคับให้ใช้ virtual และ new / override เพื่อทำการตัดสินใจอย่างมีสติ

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

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

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

ทุกครั้งที่คุณพูดว่า virtual ใน API คุณกำลังสร้าง hook call back ในฐานะนักออกแบบระบบปฏิบัติการเฟรมเวิร์กหรือ API คุณต้องระวังให้ดี คุณไม่ต้องการให้ผู้ใช้เอาชนะและเชื่อมต่อกับจุดใดก็ได้ใน API เพราะคุณไม่สามารถทำตามสัญญาได้ และผู้คนอาจไม่เข้าใจอย่างเต็มที่ถึงคำสัญญาที่พวกเขาทำเมื่อพวกเขาทำสิ่งที่เสมือนจริง

สัมภาษณ์มีการอภิปรายเพิ่มเติมเกี่ยวกับวิธีที่นักพัฒนาคิดเกี่ยวกับการออกแบบการสืบทอดคลาสและวิธีการที่นำไปสู่การตัดสินใจ

ตอนนี้คำถามต่อไปนี้:

ฉันไม่สามารถเข้าใจว่าทำไมในโลกฉันจะเพิ่มวิธีการใน DerivedClass ของฉันที่มีชื่อเดียวกันและลายเซ็นเดียวกันกับ BaseClass และกำหนดพฤติกรรมใหม่ แต่ที่ polymorphism เวลาทำงานเมธอด BaseClass จะถูกเรียก! (ซึ่งไม่ใช่การเอาชนะ แต่ควรมีเหตุผล)

นี่จะเป็นเมื่อคลาสที่ได้รับต้องการประกาศว่าไม่ปฏิบัติตามสัญญาของคลาสพื้นฐาน แต่มีเมธอดที่มีชื่อเดียวกัน (สำหรับทุกคนที่ไม่ทราบความแตกต่างระหว่างnewและoverrideใน C # ดูหน้า MSDN นี้ )

สถานการณ์จริงมากคือ:

  • คุณสร้าง API Vehicleซึ่งมีระดับที่เรียกว่า
  • ฉันเริ่มใช้ API ของคุณและได้รับVehicleแล้ว
  • คุณชั้นไม่ได้มีวิธีการใดVehiclePerformEngineCheck()
  • ในของฉันเรียนฉันจะเพิ่มวิธีการCarPerformEngineCheck()
  • คุณเปิดตัวรุ่นใหม่ของ API PerformEngineCheck()ของคุณและเพิ่ม
  • ฉันไม่สามารถเปลี่ยนชื่อวิธีการของฉันเพราะลูกค้าของฉันขึ้นอยู่กับ API ของฉันและมันจะทำลายพวกเขา
  • ดังนั้นเมื่อฉันคอมไพล์ใหม่ API ของคุณ C # เตือนฉันเรื่องนี้เช่น

    หากฐานPerformEngineCheck()ไม่ใช่virtual:

    app2.cs(15,17): warning CS0108: 'Car.PerformEngineCheck()' hides inherited member 'Vehicle.PerformEngineCheck()'.
    Use the new keyword if hiding was intended.
    

    และถ้าฐานPerformEngineCheck()เป็นvirtual:

    app2.cs(15,17): warning CS0114: 'Car.PerformEngineCheck()' hides inherited member 'Vehicle.PerformEngineCheck()'.
    To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.
    
  • ตอนนี้ฉันต้องตัดสินใจอย่างชัดเจนว่าคลาสของฉันขยายสัญญาคลาสพื้นฐานจริง ๆ หรือว่าเป็นสัญญาอื่น แต่เกิดขึ้นเป็นชื่อเดียวกัน

  • ด้วยการทำให้มันnewฉันจะไม่ทำลายลูกค้าของฉันหากการทำงานของวิธีการพื้นฐานแตกต่างจากวิธีที่ได้รับ รหัสอ้างอิงใด ๆ ที่Vehicleจะไม่เห็นCar.PerformEngineCheck()เรียก แต่รหัสที่มีการอ้างอิงถึงจะยังคงเห็นการทำงานเดียวกันกับที่ผมได้นำเสนอในCarPerformEngineCheck()

ตัวอย่างที่คล้ายกันคือเมื่อเมธอดอื่นในคลาสฐานอาจเรียกใช้PerformEngineCheck()(โดยเฉพาะในเวอร์ชันที่ใหม่กว่า) วิธีหนึ่งจะป้องกันไม่ให้เรียกPerformEngineCheck()คลาสที่ได้รับมาได้อย่างไร ใน Java การตัดสินใจนั้นจะอยู่กับคลาสฐาน แต่ก็ไม่รู้อะไรเลยเกี่ยวกับคลาสที่ได้รับมา ใน C # การตัดสินใจนั้นขึ้นอยู่กับทั้งคลาสพื้นฐาน (ผ่านvirtualคำหลัก) และกับคลาสที่ได้รับ (ผ่านทางnewและoverrideคำหลัก)

แน่นอนข้อผิดพลาดที่คอมไพเลอร์โยนยังเป็นเครื่องมือที่มีประโยชน์สำหรับโปรแกรมเมอร์ที่จะไม่ทำให้เกิดข้อผิดพลาดโดยไม่คาดคิด (เช่นแทนที่หรือให้ฟังก์ชั่นใหม่โดยไม่ทราบ)

เช่นเดียวกับ Anders พูดโลกแห่งความจริงบังคับให้เราเข้าไปในประเด็นดังกล่าวซึ่งถ้าเราต้องเริ่มจากศูนย์เราจะไม่ต้องการเข้าไป

แก้ไข: เพิ่มตัวอย่างว่าnewจะต้องใช้ที่ใดเพื่อให้แน่ใจว่าสามารถใช้งานร่วมกับอินเตอร์เฟสได้

แก้ไข: ในขณะที่ผ่านความคิดเห็นฉันยังเจอบทความโดย Eric Lippert (จากนั้นหนึ่งในสมาชิกของคณะกรรมการออกแบบ C #) ในสถานการณ์ตัวอย่างอื่น ๆ (กล่าวถึงโดย Brian)


ส่วนที่ 2: อิงตามคำถามที่อัปเดต

แต่ในกรณีของ C # ถ้า SpaceShip ไม่ได้แทนที่ความเร็วของยานพาหนะคลาส 'เร่งความเร็วและใช้ใหม่แล้วตรรกะของรหัสของฉันจะถูกทำลาย นั่นเป็นข้อเสียหรือไม่?

ใครเป็นผู้ตัดสินใจว่าSpaceShipจริง ๆ แล้วจะเอาชนะVehicle.accelerate()หรือแตกต่างกันอย่างไร มันจะต้องมีการSpaceShipพัฒนา ดังนั้นหากSpaceShipนักพัฒนาตัดสินใจว่าพวกเขาไม่ได้รักษาสัญญาของคลาสพื้นฐานการโทรของคุณไปที่Vehicle.accelerate()ไม่ควรไปSpaceShip.accelerate()หรือควร? newนั่นคือเมื่อพวกเขาจะทำเครื่องหมายว่า overrideแต่ถ้าพวกเขาตัดสินใจว่ามันไม่แน่นอนให้สัญญาแล้วพวกเขาจะในความเป็นจริงทำเครื่องหมาย ในทั้งสองกรณีรหัสของคุณจะทำงานได้อย่างถูกต้องโดยการเรียกวิธีการที่ถูกต้องอยู่บนพื้นฐานของการทำสัญญา รหัสของคุณจะตัดสินใจได้อย่างไรว่าSpaceShip.accelerate()จริง ๆ แล้วมีการเอาชนะVehicle.accelerate()หรือเป็นชื่อที่ขัดแย้งกันหรือไม่ (ดูตัวอย่างของฉันด้านบน)

อย่างไรก็ตามในกรณีของมรดกนัยถึงแม้ว่าSpaceShip.accelerate()ไม่ได้ให้สัญญาของวิธีการเรียกยังจะไปVehicle.accelerate()SpaceShip.accelerate()


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

7
แน่นอนว่าอาจเป็นกรณี คำถามคือเมื่อ C # ตัดสินใจที่จะทำทำไมในเวลานั้นและทำไมคำตอบนี้จึงถูกต้อง หากคำถามคือว่ายังคงสมเหตุสมผลอยู่นั่นคือการสนทนาที่แตกต่างกัน IMHO
Omer Iqbal

1
ฉันเห็นด้วยกับคุณอย่างเต็มที่
maaartinus

2
IMHO ในขณะที่มีการใช้งานอย่างแน่นอนสำหรับการมีฟังก์ชั่นที่ไม่ใช่เสมือนความเสี่ยงของการผิดพลาดที่ไม่คาดคิดเกิดขึ้นเมื่อสิ่งที่ไม่คาดว่าจะแทนที่วิธีการระดับฐานหรือใช้อินเทอร์เฟซทำ
supercat

3
@Nilzor: ดังนั้นข้อโต้แย้งเกี่ยวกับการศึกษาและในทางปฏิบัติ ในทางปฏิบัติความรับผิดชอบในการทำลายบางสิ่งบางอย่างอยู่ในการเปลี่ยนแปลงครั้งสุดท้าย ถ้าคุณเปลี่ยนคลาสพื้นฐานของคุณและคลาสที่ได้รับมานั้นขึ้นอยู่กับว่ามันไม่เปลี่ยนแปลงนั่นไม่ใช่ปัญหาของพวกเขา ("พวกเขา" อาจไม่มีอยู่อีกต่อไป) ดังนั้นคลาสพื้นฐานจะถูกล็อคเข้ากับพฤติกรรมของคลาสที่ได้รับแม้ว่าคลาสนั้นจะไม่เป็นเสมือนจริงก็ตาม และอย่างที่คุณพูด RhinoMock มีอยู่จริง ใช่ถ้าคุณทำขั้นตอนวิธีสุดท้ายให้ถูกต้องทุกอย่างเท่ากัน ที่นี่เราชี้ไปที่ทุกระบบ Java ที่เคยสร้าง
deworde

94

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


7
การอนุญาตให้วิธีการใด ๆ ถูกเขียนทับนั้นผิด คุณควรจะสามารถแทนที่วิธีการเหล่านั้นที่ผู้ออกแบบซูเปอร์คลาสได้กำหนดไว้ว่าปลอดภัยสำหรับการแทนที่
Doval

21
เอริค Lippert กล่าวถึงในรายละเอียดในการโพสต์ของเขาวิธีเสมือนและฐานเรียนเปราะ
Brian

12
Java มีfinalตัวแก้ไขดังนั้นปัญหาคืออะไร
Sarge Borsch

13
@corsiKa "วัตถุควรจะเปิดเพื่อขยาย" - ทำไม? หากนักพัฒนาของคลาสพื้นฐานไม่เคยคิดเกี่ยวกับการสืบทอดมันเกือบจะรับประกันได้ว่ามีการอ้างอิงและการสันนิษฐานในรหัสที่จะนำไปสู่ข้อบกพร่อง (จำได้HashMapไหม?) ทั้งการออกแบบสำหรับการสืบทอดและทำให้สัญญาชัดเจนหรือไม่และตรวจสอบให้แน่ใจว่าไม่มีใครสามารถรับชั้นเรียนได้ @Overrideถูกเพิ่มเป็น bandaid เพราะมันสายเกินไปที่จะเปลี่ยนพฤติกรรมในตอนนั้น แต่แม้แต่ Java devs ดั้งเดิม (โดยเฉพาะ Josh Bloch) ก็เห็นด้วยว่านี่เป็นการตัดสินใจที่ไม่ดี
Voo

9
@ jwenting "ความเห็น" เป็นคำที่ตลก ผู้คนชอบที่จะแนบมันกับข้อเท็จจริงเพื่อที่พวกเขาจะไม่สนใจพวกเขา แต่สำหรับสิ่งที่มันคุ้มค่านอกจากนี้ยังเป็นโจชัวโบลช "ความคิดเห็น" (โปรดดูที่: ที่มีประสิทธิภาพ Java , รายการ 17 ), เจมส์กอสลิงของ "ความคิดเห็น" (ดูการสัมภาษณ์ครั้งนี้ ) และในขณะที่ชี้ให้เห็นในคำตอบโอเมอร์อิคบาลก็ยัง Anders Hejlsberg ของ "ความคิดเห็น" (และถึงแม้ว่าเขาไม่เคยพูดถึงการเลือกไม่เข้าร่วมกับการเลือกไม่รับ Eric Lippert เห็นด้วยอย่างชัดเจนว่าการรับมรดกนั้นเป็นอันตรายเช่นกัน) ดังนั้นใครที่อ้างถึง?
Doval

33

อย่างที่โรเบิร์ตฮาร์วีย์พูดมันทั้งหมดในสิ่งที่คุณคุ้นเคย ฉันพบว่า Java ขาดความยืดหยุ่นนี้แปลก

ที่กล่าวว่าทำไมมีสิ่งนี้ตั้งแต่แรก? สำหรับเหตุผลเดียวกันกับที่มี C # public, internal(ยัง "ไม่มีอะไร") protected, protected internalและprivateแต่ Java ก็มีpublic, ไม่มีอะไรและprotected privateมันให้การควบคุมที่ละเอียดยิ่งขึ้นเกี่ยวกับพฤติกรรมของสิ่งที่คุณกำลังเขียนโดยมีค่าใช้จ่ายในการมีคำศัพท์และคำหลักที่ต้องติดตามเพิ่มเติม

ในกรณีของnewvs. virtual+overrideมันมีลักษณะดังนี้:

  • หากคุณต้องการบังคับให้คลาสย่อยใช้วิธีการใช้abstractและoverrideในคลาสย่อย
  • หากคุณต้องการให้ฟังก์ชั่น แต่อนุญาตให้ subclass แทนที่มันให้ใช้virtualและoverrideใน subclass
  • หากคุณต้องการมอบฟังก์ชันการทำงานที่คลาสย่อยไม่ควรต้องการแทนที่อย่าใช้อะไรเลย
    • หากคุณมีคลาสย่อยกรณีพิเศษซึ่งไม่จำเป็นต้องทำงานแตกต่างกันให้ใช้newในคลาสย่อย
    • หากคุณต้องการให้แน่ใจว่าไม่มี subclass สามารถเคยแทนที่ลักษณะการทำงานที่ใช้sealedในชั้นฐาน

สำหรับตัวอย่างในโลกแห่งความจริง: โครงการที่ฉันทำงานเกี่ยวกับคำสั่งซื้ออีคอมเมิร์ซที่ประมวลผลจากหลาย ๆ แหล่ง มีฐานOrderProcessorที่มีเหตุผลส่วนใหญ่มีบางอย่างabstract/ virtualวิธีการสำหรับเด็กระดับของแหล่งที่มาเพื่อแทนที่ มันใช้งานได้ดีจนกระทั่งเราได้แหล่งใหม่ซึ่งมีวิธีประมวลผลคำสั่งที่แตกต่างไปจากเดิมอย่างสิ้นเชิงเช่นเราต้องเปลี่ยนฟังก์ชั่นหลัก เรามีสองทางเลือก ณ จุดนี้: 1) เพิ่มvirtualวิธีฐานและoverrideในเด็ก หรือ 2) เพิ่มnewให้กับเด็ก

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

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


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

@svick - อาจเป็นไปได้ ในสถานการณ์เฉพาะของเราที่จะไม่เกิดขึ้น แต่นั่นเป็นเหตุผลที่ฉันเพิ่มข้อแม้
Bobson

การใช้งานที่ยอดเยี่ยมอย่างหนึ่งnewคือใน WebViewPage <TModel> ในกรอบ MVC แต่ฉันก็ถูกโยนโดยข้อผิดพลาดที่เกี่ยวข้องnewเป็นเวลาหลายชั่วโมงดังนั้นฉันไม่คิดว่ามันเป็นคำถามที่ไม่มีเหตุผล
pdr

1
@ C.Champagne: เครื่องมือใด ๆ ที่สามารถใช้งานได้ไม่ดีและอันนี้เป็นเครื่องมือที่คมชัดเป็นพิเศษ - คุณสามารถตัดเองได้อย่างง่ายดาย แต่นั่นไม่ใช่เหตุผลที่จะลบเครื่องมือออกจากกล่องเครื่องมือและลบตัวเลือกจากนักออกแบบ API ที่มีความสามารถมากกว่า
pdr

1
@svick ให้ถูกต้องซึ่งเป็นเหตุผลว่าทำไมจึงเป็นคำเตือนของคอมไพเลอร์ แต่การมีความสามารถในการ "ใหม่" ครอบคลุมถึงเงื่อนไขของขอบ (เช่นที่ได้รับ) และยิ่งกว่านั้นทำให้เห็นได้ชัดว่าคุณกำลังทำอะไรแปลก ๆ เมื่อคุณมาเพื่อวินิจฉัยข้อผิดพลาดที่หลีกเลี่ยงไม่ได้ "ทำไมคลาสนี้และมีเพียงคลาสนี้เท่านั้น ... ah-hah," new ", ไปทดสอบที่ใช้ SuperClass"
deworde

7

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

สิ่งนี้ช่วยให้การทำงานของรันไทม์ง่ายขึ้น แต่อาจทำให้เกิดปัญหาที่ไม่คาดฝัน สมมติว่าใช้GrafBaseไม่ได้void DrawParallelogram(int x1,int y1, int x2,int y2, int x3,int y3)แต่GrafDerivedใช้มันเป็นวิธีการสาธารณะที่ดึงสี่เหลี่ยมด้านขนานที่คำนวณจุดที่สี่อยู่ตรงข้ามจุดแรก สมมติว่ารุ่นที่ใหม่กว่าGrafBaseใช้เมธอดสาธารณะที่มีลายเซ็นเดียวกัน แต่คำนวณจุดที่สี่ตรงข้ามกับวินาที ลูกค้าที่ได้รับการคาดหวังGrafBaseแต่ได้รับการอ้างอิงถึงความGrafDerivedคาดหวังที่DrawParallelogramจะคำนวณจุดเริ่มต้นในรูปแบบของGrafBaseวิธีการใหม่แต่ลูกค้าที่เคยใช้งานGrafDerived.DrawParallelogramก่อนที่จะเปลี่ยนวิธีการพื้นฐานจะคาดหวังพฤติกรรมที่GrafDerivedเริ่มดำเนินการ

ใน Java ไม่มีวิธีใดที่ผู้สร้างGrafDerivedจะสามารถอยู่ร่วมกับลูกค้าที่ใช้GrafBase.DrawParallelogramวิธีการใหม่(และอาจไม่ทราบว่าGrafDerivedมีอยู่) โดยไม่ทำลายความเข้ากันได้กับรหัสลูกค้าที่มีอยู่ซึ่งใช้GrafDerived.DrawParallelogramก่อนGrafBaseกำหนด เนื่องจากDrawParallelogramไม่สามารถบอกได้ว่าไคลเอ็นต์ประเภทใดที่เรียกใช้จึงต้องทำงานเหมือนกันเมื่อเรียกใช้โดยรหัสลูกค้าประเภทต่างๆ เนื่องจากรหัสลูกค้าสองประเภทมีความคาดหวังที่แตกต่างกันว่ามันควรทำอย่างไรจึงไม่มีทางที่GrafDerivedจะหลีกเลี่ยงการละเมิดความคาดหวังที่ถูกต้องตามกฎหมายที่หนึ่งในพวกเขา (เช่นทำลายรหัสลูกค้าที่ถูกกฎหมาย)

ใน C # ถ้าGrafDerivedไม่ได้คอมรันไทม์จะถือว่ารหัสที่ซึ่งเรียกDrawParallelogramวิธีเมื่ออ้างอิงจากประเภทนี้GrafDerivedจะได้รับการคาดหวังว่าพฤติกรรมGrafDerived.DrawParallelogram()มีเมื่อมันเป็นคอมไพล์แล้ว แต่รหัสซึ่งเรียกวิธีเมื่ออ้างอิงประเภทGrafBaseจะได้รับการคาดหวังว่าGrafBase.DrawParallelogram(ลักษณะการทำงานที่ ถูกเพิ่ม). ถ้าGrafDerivedคอมไพล์ใหม่ในภายหลังในการปรากฏตัวของการปรับปรุงGrafBaseคอมไพเลอร์จะ squawk จนกว่าโปรแกรมเมอร์ทั้งระบุว่าวิธีการของเขามีวัตถุประสงค์เพื่อทดแทนที่ถูกต้องสำหรับสมาชิกที่สืบทอดมาGrafBaseหรือว่าพฤติกรรมของมันจะต้องเชื่อมโยงกับการอ้างอิงประเภทGrafDerivedแต่ไม่ควร GrafBaseเปลี่ยนพฤติกรรมของการอ้างอิงประเภท

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


2
ฉันคิดว่ามีคำตอบที่ดีในที่นี่ แต่มันหายไปในกำแพงข้อความ โปรดแยกส่วนนี้ออกเป็นอีกสองสามย่อหน้า
Bobson

DerivedGraphics? A class using GrafDerived` คืออะไร
C.Champagne

@ C.Champagne: GrafDerivedผมหมายถึง ฉันเริ่มใช้DerivedGraphicsแต่รู้สึกว่ามันค่อนข้างยาว แม้GrafDerivedจะยังค่อนข้างยาว แต่ก็ไม่รู้ว่าจะดีที่สุดในการตั้งชื่อประเภทกราฟิก - เรนเดอร์ซึ่งควรมีความสัมพันธ์ที่ชัดเจน
supercat

1
@ Bobon: ดีกว่า
supercat

6

สถานการณ์มาตรฐาน:

คุณเป็นเจ้าของคลาสพื้นฐานที่ใช้โดยหลายโครงการ คุณต้องการเปลี่ยนแปลงคลาสพื้นฐานดังกล่าวที่จะแบ่งระหว่างคลาสที่ได้รับและนับไม่ถ้วนซึ่งอยู่ในโครงการที่ให้คุณค่าในโลกแห่งความเป็นจริง (กรอบงานให้คุณค่าที่ดีที่สุดในการลบครั้งเดียวโดยที่มนุษย์ไม่ต้องการกรอบ สิ่งที่ทำงานบน Framework) ขอให้โชคดีกับเจ้าของที่ยุ่งกับคลาสที่ได้รับ "ดีคุณต้องเปลี่ยนคุณไม่ควรลบล้างวิธีนั้น" โดยไม่ได้รับการยกย่องว่าเป็น "Framework: Delayer of Projects และ Causer of Bugs" กับคนที่ต้องอนุมัติการตัดสินใจ

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

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

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

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

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


0

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

ใน C # ผู้พัฒนาคลาสพื้นฐานไม่เชื่อถือนักพัฒนาคลาสย่อย ใน Java ผู้พัฒนาคลาสย่อยไม่เชื่อถือนักพัฒนาคลาสพื้นฐาน ผู้พัฒนาคลาสย่อยมีหน้าที่รับผิดชอบในคลาสย่อยและควร (imho) ได้รับอำนาจในการขยายคลาสฐานตามที่เห็นสมควร (ยกเว้นการปฏิเสธอย่างชัดเจนและในจาวาพวกเขาสามารถทำสิ่งนี้ผิด)

มันเป็นคุณสมบัติพื้นฐานของคำนิยามภาษา มันไม่ถูกหรือผิดมันเป็นอะไรและไม่สามารถเปลี่ยนแปลงได้


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