อะไรคือความแตกต่างระหว่าง Generics ใน C # และ Java ... และ Templates ใน C ++ [ปิด]


203

ฉันใช้จาวาและ generics เป็นส่วนใหญ่ค่อนข้างใหม่ ฉันอ่านต่อไปว่า Java ทำการตัดสินใจผิดหรือว่า. NET มีการใช้งานที่ดีขึ้น ฯลฯ

ดังนั้นอะไรคือความแตกต่างที่สำคัญระหว่าง C ++, C #, Java ใน generics? ข้อดี / ข้อเสียของแต่ละคน?

คำตอบ:


364

ฉันจะเพิ่มเสียงของฉันไปที่เสียงรบกวนและทำสิ่งที่ชัดเจน:

C # Generics ช่วยให้คุณสามารถประกาศสิ่งนี้

List<Person> foo = new List<Person>();

จากนั้นคอมไพเลอร์จะป้องกันไม่ให้คุณใส่สิ่งที่ไม่ได้Personอยู่ในรายการ
เบื้องหลังคอมไพเลอร์ C # เพิ่งใส่List<Person>ลงในไฟล์. dll dll แต่ในขณะรันไทม์คอมไพเลอร์ JIT จะไปและสร้างชุดรหัสใหม่ราวกับว่าคุณได้เขียนคลาสรายการพิเศษเพื่อบรรจุบุคคลListOfPersonสิ่งที่ชอบ

ประโยชน์ของมันคือทำให้มันเร็วจริงๆ ไม่มีการแคสต์หรือสิ่งอื่นใดและเนื่องจาก dll มีข้อมูลว่านี่คือ List of Personรหัสอื่นที่มองในภายหลังโดยใช้การสะท้อนสามารถบอกได้ว่ามันมีPersonวัตถุ (ดังนั้นคุณจะได้รับ Intellisense เป็นต้น)

ข้อเสียของสิ่งนี้คือรหัสเก่า C # 1.0 และ 1.1 (ก่อนที่พวกเขาจะเพิ่มชื่อสามัญ) ไม่เข้าใจสิ่งใหม่เหล่านี้List<something>ดังนั้นคุณต้องเปลี่ยนสิ่งต่าง ๆ กลับไปเป็นแบบเก่าด้วยตนเองListเพื่อทำงานร่วมกับพวกเขา นี่ไม่ใช่ปัญหาใหญ่เพราะรหัสไบนารี่ C # 2.0 ไม่รองรับการใช้งานย้อนหลัง ครั้งเดียวที่จะเกิดขึ้นคือถ้าคุณกำลังอัพเกรดรหัส C # 1.0 / 1.1 เก่าเป็น C # 2.0

Java Generics อนุญาตให้คุณประกาศสิ่งนี้

ArrayList<Person> foo = new ArrayList<Person>();

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

ความแตกต่างคือสิ่งที่เกิดขึ้นเบื้องหลัง แตกต่างจาก C #, Java ไม่ได้ไปและสร้างพิเศษListOfPerson- เพียงแค่ใช้เก่าธรรมดาArrayListที่ได้รับเสมอใน Java เมื่อคุณได้รับสิ่งต่าง ๆ จากแถวลำดับการPerson p = (Person)foo.get(1);ร่ายรำแบบปกติยังคงต้องทำ คอมไพเลอร์กำลังบันทึกคุณกดปุ่ม แต่ความเร็วในการตี / การหล่อยังคงเกิดขึ้นเหมือนเดิม
เมื่อมีคนพูดถึง "การลบประเภท" นี่คือสิ่งที่พวกเขากำลังพูดถึง คอมไพเลอร์แทรกการปลดเปลื้องสำหรับคุณแล้ว 'ลบ' ความจริงที่ว่ามันหมายถึงการเป็นรายการPersonไม่เพียงObject

ประโยชน์ของวิธีการนี้คือรหัสเก่าที่ไม่เข้าใจข้อมูลทั่วไปไม่จำเป็นต้องสนใจ มันยังคงติดต่อกับคนเก่าArrayListเหมือนที่เคยมีมา สิ่งนี้มีความสำคัญมากกว่าในโลกของจาวาเพราะพวกเขาต้องการที่จะรองรับการคอมไพล์โค้ดโดยใช้ Java 5 กับ generics และทำให้มันทำงานบน 1.4 หรือ JVM รุ่นก่อนหน้าของไมโครซอฟท์ซึ่งไมโครซอฟท์ได้ตัดสินใจแล้วว่าจะไม่สนใจ

ข้อเสียคือความเร็วที่ฉันกล่าวถึงก่อนหน้านี้และเนื่องจากไม่มีListOfPersonคลาสหลอกหรือสิ่งใดที่เหมือนกับการเข้าไปในไฟล์. class รหัสที่ดูในภายหลัง (พร้อมการสะท้อนกลับหรือถ้าคุณดึงมันออกมาจากคอลเล็กชันอื่น ที่มันถูกแปลงเป็นObjectหรืออื่น ๆ ) ไม่สามารถบอกได้ว่ามันหมายถึงว่าเป็นรายการที่มีเฉพาะPersonและไม่ใช่แค่รายการอาร์เรย์อื่น ๆ

เทมเพลต C ++ ช่วยให้คุณสามารถประกาศสิ่งนี้

std::list<Person>* foo = new std::list<Person>();

ดูเหมือนว่า C # และ Java generics และจะทำในสิ่งที่คุณคิดว่าควรทำ แต่เบื้องหลังจะมีสิ่งต่าง ๆ เกิดขึ้น

มันมีประโยชน์ร่วมกันมากที่สุดกับ C # generics เพราะมันสร้างเป็นพิเศษpseudo-classesมากกว่าเพียงแค่โยนข้อมูลประเภทออกไปอย่างที่จาวาใช้ แต่มันเป็นปลากาต้มน้ำที่แตกต่างกันโดยสิ้นเชิง

ทั้ง C # และ Java ผลิตเอาต์พุตที่ออกแบบมาสำหรับเครื่องเสมือน หากคุณเขียนโค้ดที่มีPersonคลาสในทั้งสองกรณีข้อมูลบางอย่างเกี่ยวกับPersonคลาสจะเข้าไปในไฟล์. dll หรือ. class และ JVM / CLR จะทำสิ่งนี้

C ++ สร้างรหัสไบนารี x86 แบบ raw ทุกอย่างไม่ใช่วัตถุและไม่มีเครื่องเสมือนที่ต้องรู้เกี่ยวกับPersonชั้นเรียน ไม่มีการชกมวยหรือ unboxing และฟังก์ชั่นไม่จำเป็นต้องเป็นของชั้นเรียนหรืออะไรก็ตาม

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

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

string addNames<T>( T first, T second ) { return first.Name() + second.Name(); }

รหัสนั้นจะไม่คอมไพล์ใน C # หรือ Java เพราะไม่ทราบว่าประเภทTนั้นมีวิธีการที่เรียกว่าชื่อ () คุณต้องบอก - ใน C # เช่นนี้:

interface IHasName{ string Name(); };
string addNames<T>( T first, T second ) where T : IHasName { .... }

จากนั้นคุณต้องตรวจสอบให้แน่ใจว่าสิ่งที่คุณส่งผ่านไปยัง addNames ใช้อินเทอร์เฟซ IHasName และอื่น ๆ ไวยากรณ์ของ java แตกต่างกัน ( <T extends IHasName>) แต่ได้รับปัญหาจากปัญหาเดียวกัน

กรณี 'คลาสสิค' สำหรับปัญหานี้กำลังพยายามเขียนฟังก์ชันที่ทำสิ่งนี้

string addNames<T>( T first, T second ) { return first + second; }

คุณไม่สามารถเขียนรหัสนี้ได้เนื่องจากไม่มีวิธีในการประกาศส่วนต่อประสานที่มี+วิธีการนั้น คุณล้มเหลว

C ++ ไม่ประสบปัญหาเหล่านี้ คอมไพเลอร์ไม่สนใจเกี่ยวกับการส่งประเภทลงไปยัง VM ของใด ๆ - ถ้าวัตถุของคุณทั้งสองมีฟังก์ชั่น. ชื่อ () มันจะรวบรวม หากพวกเขาทำไม่ได้มันจะไม่ ง่าย

ดังนั้นคุณมีมัน :-)


8
pseudoclasess ที่สร้างขึ้นสำหรับประเภทการอ้างอิงใน C # แบ่งปันการใช้งานเดียวกันดังนั้นคุณจะไม่ได้รับ ListOfPeople อย่างแน่นอน ตรวจสอบblogs.msdn.com/ericlippert/archive/2009/07/30/…
Piotr Czapla

4
ไม่คุณไม่สามารถรวบรวมรหัส Java 5 โดยใช้ generics และให้มันทำงานบน 1.4 VMs เก่า (อย่างน้อย Sun JDK ไม่ได้ใช้สิ่งนี้เครื่องมือของบุคคลที่สามทำ) สิ่งที่คุณสามารถทำได้คือใช้ 1.4 JARs ที่รวบรวมไว้ก่อนหน้านี้จาก รหัส 1.5 / 1.6
finnw

4
ฉันคัดค้านคำสั่งที่คุณไม่สามารถเขียนint addNames<T>( T first, T second ) { return first + second; }ใน C # ประเภททั่วไปสามารถถูก จำกัด คลาสได้แทนที่จะเป็นอินเตอร์เฟสและมีวิธีการประกาศคลาสที่มี+โอเปอเรเตอร์อยู่
Mashmagar

4
@AlexanderMalakhov ไม่ใช่สำนวนที่ตั้งใจ จุดไม่ได้ให้ความรู้เกี่ยวกับสำนวนของ C ++ แต่เพื่อแสดงให้เห็นว่าชิ้นส่วนของรหัสที่ดูคล้ายกันนั้นแตกต่างกันในแต่ละภาษา เป้าหมายนี้จะยากยิ่งกว่าที่จะได้รหัสที่ต่างออกไปมากขึ้น
Orion Edwards

3
@phresnel ฉันเห็นด้วยในหลักการ แต่ถ้าฉันเขียนตัวอย่างโค้ดในสำนวน C ++ มันจะเข้าใจนักพัฒนา C # / Java ได้น้อยลงดังนั้น (ฉันเชื่อว่า) จะทำงานได้แย่ลงเมื่ออธิบายความแตกต่าง เราตกลงที่จะไม่เห็นด้วยกับเรื่องนี้ :-)
Orion Edwards

61

C ++ ไม่ค่อยใช้คำศัพท์ "generics" แต่จะใช้คำว่า“ เทมเพลต” และมีความแม่นยำมากกว่า เทมเพลตอธิบายเทคนิคเดียวเพื่อให้ได้การออกแบบทั่วไป

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

template <unsigned int N>
struct product {
    static unsigned int const VALUE = N * product<N - 1>::VALUE;
};

template <>
struct product<1> {
    static unsigned int const VALUE = 1;
};

// Usage:
unsigned int const p5 = product<5>::VALUE;

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

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

เทมเพลต C ++ มีความจำเป็นสำหรับกระบวนทัศน์การเขียนโปรแกรมแบบอัลกอริทึม ตัวอย่างเช่นอัลกอริทึมเกือบทั้งหมดสำหรับคอนเทนเนอร์ถูกกำหนดเป็นฟังก์ชันที่ยอมรับประเภทคอนเทนเนอร์เป็นประเภทแม่แบบและปฏิบัติต่อพวกเขาอย่างสม่ำเสมอ ที่จริงแล้วมันไม่ถูกต้อง: C ++ ใช้งานไม่ได้กับคอนเทนเนอร์ แต่อยู่ในช่วงที่กำหนดโดยตัววนซ้ำสองตัวชี้ไปที่จุดเริ่มต้นและด้านหลังจุดสิ้นสุดของคอนเทนเนอร์ ดังนั้นเนื้อหาทั้งหมดจะถูก จำกัด โดยตัววนซ้ำ: เริ่มต้น <= องค์ประกอบ <สิ้นสุด

การใช้ตัววนซ้ำแทนคอนเทนเนอร์มีประโยชน์เพราะช่วยให้สามารถทำงานกับชิ้นส่วนของคอนเทนเนอร์แทนการใช้ทั้งหมด

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

template <typename T>
class Store {  }; // (1)

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

template <typename T>
class Store<T*> {  }; // (2)

ตอนนี้เมื่อใดก็ตามที่เราสร้างเทมเพลตคอนเทนเนอร์สำหรับประเภทเดียวเราจะใช้คำจำกัดความที่เหมาะสม:

Store<int> x; // Uses (1)
Store<int*> y; // Uses (2)
Store<string**> z; // Uses (2), with T = string*.

บางครั้งฉันอยากให้คุณสมบัติทั่วไปใน. net สามารถอนุญาตให้ใช้สิ่งอื่นนอกเหนือจากประเภทเป็นกุญแจได้ ถ้าอาร์เรย์ประเภทค่าเป็นส่วนหนึ่งของ Framework (ฉันแปลกใจที่พวกเขาไม่ได้มีความจำเป็นต้องโต้ตอบกับ API เก่าที่ฝังอาร์เรย์ขนาดคงที่ภายในโครงสร้าง) มันจะมีประโยชน์ในการประกาศ คลาสที่มีไอเท็มไม่กี่ไอเท็มจากนั้นอาร์เรย์ชนิดค่าที่ขนาดเป็นพารามิเตอร์ทั่วไป เนื่องจากมันเป็นสิ่งที่ใกล้เคียงที่สุดที่สามารถเกิดขึ้นได้คือมีคลาสวัตถุที่เก็บรายการแต่ละรายการแล้วยังมีการอ้างอิงไปยังวัตถุแยกต่างหากที่ถืออาร์เรย์
supercat

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

ฉันเดาว่าสิ่งที่ฉันสับสนคือมันดูเหมือนว่าการมีอาร์เรย์ชนิดค่าคงที่ขนาดไม่น่าจะยากและมันจะอนุญาตให้ข้อมูลหลายชนิดถูกจัดเรียงโดยอ้างอิงแทนที่จะเป็นค่า ถึงแม้ว่า marshal-by-value จะมีประโยชน์ในกรณีที่ไม่สามารถจัดการได้อย่างแท้จริง แต่ฉันคิดว่า marshal-by-ref มีความเหนือกว่าในเกือบทุกกรณีที่สามารถใช้งานได้ อาร์เรย์ขนาดเล็กดูเหมือนจะเป็นคุณสมบัติที่มีประโยชน์
supercat

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


18

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

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

ทางเลือกคืออะไร หากคุณไม่ได้ใช้งาน generics ในภาษาคุณจะใช้งานที่ไหน? และคำตอบคือ: ในเครื่องเสมือน ซึ่งแบ่งความเข้ากันได้ย้อนหลัง

ในทางกลับกันการลบประเภทช่วยให้คุณสามารถผสมไคลเอนต์ทั่วไปกับไลบรารีที่ไม่ใช่ทั่วไปได้ กล่าวอีกนัยหนึ่ง: รหัสที่รวบรวมบน Java 5 ยังคงสามารถปรับใช้กับ Java 1.4 ได้

อย่างไรก็ตาม Microsoft ตัดสินใจที่จะทำลายความเข้ากันได้ของข้อมูลทั่วไป นั่นเป็นเหตุผลที่. NET Generics "ดีกว่า" กว่า Java Generics

แน่นอนว่าซันไม่ใช่คนงี่เง่าหรือคนขี้ขลาด เหตุผลที่พวกเขา "chickened out" คือ Java นั้นมีอายุมากกว่าและแพร่หลายมากกว่า NET เมื่อพวกเขาแนะนำ generics (พวกเขาได้รับการแนะนำอย่างเกรี้ยวกราดในเวลาเดียวกันในทั้งสองโลก) การทำลายความเข้ากันได้แบบย้อนหลังจะเป็นความเจ็บปวดครั้งใหญ่

อีกวิธีหนึ่ง: ใน Java, Generics เป็นส่วนหนึ่งของภาษา (ซึ่งหมายความว่าพวกเขาใช้เฉพาะกับ Java ไม่ใช่ภาษาอื่น ๆ ) ใน. NET พวกเขาเป็นส่วนหนึ่งของVirtual Machine (ซึ่งหมายความว่าพวกเขาใช้กับทุกภาษาไม่ใช่ เพียงแค่ C # และ Visual Basic.NET)

เปรียบเทียบกับ .NET คุณสมบัติเช่น LINQ แสดงออกแลมบ์ดาท้องถิ่นชนิดตัวแปรอนุมานชนิดไม่ระบุชื่อและต้นไม้แสดงออกเหล่านี้เป็นทุกภาษาคุณสมบัติ นั่นเป็นเหตุผลที่มีความแตกต่างเล็กน้อยระหว่าง VB.NET และ C #: หากคุณสมบัติเหล่านั้นเป็นส่วนหนึ่งของ VM พวกเขาจะเหมือนกันในทุกภาษา แต่ CLR ไม่เปลี่ยนแปลง: ยังคงเหมือนเดิมใน. NET 3.5 SP1 เหมือนเดิมใน. NET 2.0 คุณสามารถคอมไพล์โปรแกรม C # ที่ใช้ LINQ กับ. NET 3.5 คอมไพเลอร์และยังคงรันมันบน. NET 2.0 โดยที่คุณไม่ต้องใช้ไลบรารี. NET 3.5 ที่จะไม่ทำงานกับ generics และ. NET 1.1 แต่จะทำงานกับ Java และ Java 1.4


3
LINQ เป็นคุณสมบัติห้องสมุดเป็นหลัก (แม้ว่า C # และ VB ยังได้เพิ่มซินแท็กซ์น้ำตาลข้างๆ) ภาษาใด ๆ ที่กำหนดเป้าหมาย 2.0 CLR สามารถใช้งาน LINQ ได้อย่างเต็มที่เพียงแค่โหลดแอสเซมบลี System.Core
Richard Berg

ใช่ขอโทษฉันควรจะชัดเจนมากขึ้น wrt LINQ ฉันอ้างถึงไวยากรณ์เคียวรีไม่ใช่ตัวดำเนินการเคียวรีมาตรฐาน monadic วิธีการขยาย LINQ หรืออินเตอร์เฟส IQueryable เห็นได้ชัดว่าคุณสามารถใช้จาก. NET ภาษาใด ๆ
Jörg W Mittag

1
ฉันคิดว่าตัวเลือกอื่นสำหรับ Java แม้ว่า Oracle ไม่ต้องการทำลายความเข้ากันได้แบบย้อนหลัง แต่ก็ยังสามารถทำเคล็ดลับคอมไพเลอร์เพื่อหลีกเลี่ยงข้อมูลประเภทที่ถูกลบได้ ตัวอย่างเช่นArrayList<T>สามารถปล่อยออกมาเป็นชนิดที่มีชื่อภายในใหม่พร้อมกับClass<T>ฟิลด์สแตติก (ซ่อน) ตราบใดที่เวอร์ชันใหม่ของ lib ทั่วไปได้รับการปรับใช้ด้วยรหัส 1.5+ ไบต์มันจะสามารถทำงานบน 1.4- JVM ได้
Earth Engine

14

ติดตามการโพสต์ก่อนหน้าของฉัน

เทมเพลตเป็นหนึ่งในสาเหตุหลักว่าทำไม C ++ จึงล้มเหลวอย่างมากในระบบ Intellisense โดยไม่คำนึงถึง IDE ที่ใช้ เนื่องจากความเชี่ยวชาญเทมเพลต IDE จึงไม่สามารถแน่ใจได้อย่างแน่นอนว่าสมาชิกที่ระบุนั้นมีอยู่หรือไม่ พิจารณา:

template <typename T>
struct X {
    void foo() { }
};

template <>
struct X<int> { };

typedef int my_int_type;

X<my_int_type> a;
a.|

ตอนนี้เคอร์เซอร์อยู่ในตำแหน่งที่ระบุและมันก็ยากสำหรับ IDE ที่จะพูด ณ จุดนั้นถ้าและสิ่งที่สมาชิกaมี สำหรับภาษาอื่น ๆ การแยกวิเคราะห์จะเรียบง่าย แต่สำหรับ C ++ จำเป็นต้องมีการประเมินล่วงหน้าเล็กน้อย

มันแย่ลงเรื่อย ๆ ถ้าหากmy_int_typeนิยามไว้ในเทมเพลตของชั้นเรียนด้วยล่ะ ตอนนี้ชนิดของมันจะขึ้นอยู่กับอาร์กิวเมนต์ประเภทอื่น และที่นี่แม้แต่คอมไพเลอร์ก็ล้มเหลว

template <typename T>
struct Y {
    typedef T my_type;
};

X<Y<int>::my_type> b;

หลังจากคิดไปเล็กน้อยโปรแกรมเมอร์จะสรุปว่ารหัสนี้เหมือนกับข้างต้น: Y<int>::my_typeแก้ไขintดังนั้นbควรเป็นประเภทเดียวกันaใช่ไหม?

ไม่ถูกต้อง. ณ จุดที่คอมไพเลอร์พยายามแก้ไขข้อความนี้มันยังไม่รู้จริง ๆY<int>::my_type! ดังนั้นจึงไม่ทราบว่าเป็นประเภทใด อาจเป็นอย่างอื่นเช่นฟังก์ชันสมาชิกหรือเขตข้อมูล สิ่งนี้อาจทำให้เกิดความกำกวม (แม้ว่าจะไม่ใช่ในกรณีปัจจุบัน) ดังนั้นคอมไพเลอร์จึงล้มเหลว เราต้องบอกอย่างชัดเจนว่าเราอ้างถึงชื่อประเภท:

X<typename Y<int>::my_type> b;

ตอนนี้รหัสรวบรวม เพื่อดูว่าเกิดความคลุมเครือจากสถานการณ์นี้อย่างไรให้พิจารณาโค้ดต่อไปนี้:

Y<int>::my_type(123);

คำสั่งนี้เป็นรหัสที่ถูกต้องสมบูรณ์และบอก c ++ Y<int>::my_typeที่จะดำเนินการเรียกใช้ฟังก์ชันเพื่อ อย่างไรก็ตามถ้าmy_typeไม่ใช่ฟังก์ชั่น แต่เป็นประเภทข้อความนี้จะยังคงใช้งานได้และทำการส่งแบบพิเศษ คอมไพเลอร์ไม่สามารถบอกได้ว่าเราหมายความว่าอย่างไรเราจึงต้องแก้ความกำกวมที่นี่


2
ฉันค่อนข้างเห็นด้วย มีความหวังอยู่บ้าง ระบบเติมข้อความอัตโนมัติและคอมไพเลอร์ C ++ ต้องโต้ตอบอย่างใกล้ชิด ฉันค่อนข้างมั่นใจว่า Visual Studio จะไม่มีคุณสมบัติดังกล่าว แต่สิ่งต่าง ๆ อาจเกิดขึ้นใน Eclipse / CDT หรือ IDE อื่น ๆ ที่ใช้ GCC หวังว่า! :)
Benoît

6

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

ยกตัวอย่างเช่นใน Java ที่มีอยู่ในคอลเลกชันกรอบถูกgenericised สมบูรณ์ Java ไม่มีทั้งคลาสทั่วไปและรุ่นที่ไม่ใช่รุ่นดั้งเดิมของคลาสคอลเล็กชัน ในบางวิธีการนี้จะสะอาดกว่ามาก - ถ้าคุณต้องการใช้คอลเล็กชันใน C # มีเหตุผลน้อยมากที่จะไปกับเวอร์ชันที่ไม่ใช่แบบทั่วไป แต่คลาสดั้งเดิมเหล่านั้นยังคงอยู่

ข้อแตกต่างที่น่าสังเกตอีกอย่างคือคลาส Enum ใน Java และ C # Enum ของ Java มีคำจำกัดความที่ดูค่อนข้างคดเคี้ยว:

//  java.lang.Enum Definition in Java
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {

(ดูคำอธิบายที่ชัดเจนของ Angelika Langer ว่าทำไมถึงเป็นเช่นนั้นโดยพื้นฐานแล้วนี่หมายถึง Java สามารถให้การเข้าถึงที่ปลอดภัยจากสตริงกับค่า Enum:

//  Parsing String to Enum in Java
Colour colour = Colour.valueOf("RED");

เปรียบเทียบกับรุ่น C #:

//  Parsing String to Enum in C#
Colour colour = (Colour)Enum.Parse(typeof(Colour), "RED");

เนื่องจาก Enum มีอยู่แล้วใน C # ก่อน generics ได้รับการแนะนำให้รู้จักกับภาษาความหมายไม่สามารถเปลี่ยนแปลงได้โดยไม่ทำลายรหัสที่มีอยู่ เช่นเดียวกับคอลเล็กชันมันยังคงอยู่ในไลบรารีหลักในสถานะดั้งเดิมนี้


แม้แต่ generics ของ C # ไม่ได้เป็นเพียงแค่คอมไพเลอร์เวทย์มนตร์คอมไพเลอร์ก็สามารถสร้างเวทย์มนตร์ต่อไปเพื่อสร้างไลบรารีที่มีอยู่ ไม่มีเหตุผลว่าทำไมพวกเขาต้องเปลี่ยนชื่อไม่เป็นArrayListไปList<T>และใส่ลงใน namespace ที่ใหม่ ความจริงก็คือถ้ามีคลาสปรากฏอยู่ในซอร์สโค้ดเพราะArrayList<T>มันจะกลายเป็นคอมไพเลอร์ที่สร้างชื่อคลาสที่แตกต่างในรหัส IL ดังนั้นจึงไม่มีความขัดแย้งของชื่อที่สามารถเกิดขึ้นได้
Earth Engine

4

11 เดือนสาย แต่ฉันคิดว่าคำถามนี้พร้อมสำหรับ Java Wildcard บางอย่าง

นี่คือคุณสมบัติการสร้างประโยคของ Java สมมติว่าคุณมีวิธี:

public <T> void Foo(Collection<T> thing)

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

public void Foo(Collection<?> thing)

เครื่องหมายคำถามขอให้คอมไพเลอร์ทำท่าว่าคุณประกาศพารามิเตอร์ประเภทที่มีชื่อปกติซึ่งจะต้องปรากฏเพียงครั้งเดียวในจุดนั้น

ไม่มีสิ่งใดที่คุณสามารถทำได้ด้วยอักขระตัวแทนที่คุณไม่สามารถทำได้ด้วยพารามิเตอร์ชนิดที่มีชื่อ (ซึ่งเป็นสิ่งที่ทำสิ่งเหล่านี้ใน C ++ และ C # เสมอ


2
ปลายอีก 11 เดือน ... มีหลายสิ่งที่คุณสามารถทำได้กับ Java wildcard ที่คุณไม่สามารถใช้พารามิเตอร์ประเภทที่มีชื่อ คุณสามารถทำได้ใน Java: class Foo<T extends List<?>>และใช้Foo<StringList>แต่ใน C # คุณต้องเพิ่มว่าพารามิเตอร์ชนิดพิเศษclass Foo<T, T2> where T : IList<T2>และใช้ Foo<StringList, String>clunky
R. Martinho Fernandes

2

Wikipedia มีการเขียนบทความที่ยอดเยี่ยมเมื่อเปรียบเทียบทั้งสองอย่าง Java / C # genericsและJava generics / C ++ เท็มเพลต บทความหลักใน Genericsดูเหมือนว่าบิตรก แต่จะมีข้อมูลที่ดีบางอย่างในนั้น


1

การร้องเรียนที่ใหญ่ที่สุดคือการลบประเภท ในนั้น generics ไม่ได้บังคับใช้ที่รันไทม์ นี่คือลิงค์ไปยังเอกสารของซันในหัวข้อนี้นี้

Generics ถูกนำมาใช้โดยการลบประเภท: ข้อมูลประเภททั่วไปจะปรากฏเฉพาะในเวลารวบรวมหลังจากนั้นจะถูกลบโดยคอมไพเลอร์


1

เทมเพลต C ++ นั้นมีประสิทธิภาพมากกว่า C # และจาวาของคู่หูอย่างมากเนื่องจากมีการประเมิน ณ เวลารวบรวมและความเชี่ยวชาญเฉพาะด้าน สิ่งนี้ทำให้เทมเพลต Meta-Programming และทำให้คอมไพเลอร์ C ++ เทียบเท่ากับเครื่องทัวริง (เช่นในระหว่างกระบวนการรวบรวมคุณสามารถคำนวณสิ่งที่คำนวณได้ด้วยเครื่องทัวริง)


1

ใน Java generics เป็นระดับคอมไพเลอร์เท่านั้นดังนั้นคุณจะได้รับ:

a = new ArrayList<String>()
a.getClass() => ArrayList

โปรดทราบว่าชนิดของ 'a' เป็นรายการอาร์เรย์ไม่ใช่รายการสตริง ดังนั้นประเภทของรายการกล้วยจะเท่ากับ () รายการลิง

ดังนั้นที่จะพูด


1

ดูเหมือนว่าในบรรดาข้อเสนอที่น่าสนใจอื่น ๆ มีสิ่งหนึ่งที่เกี่ยวกับการกลั่นยาสามัญและทำลายความเข้ากันได้ย้อนหลัง:

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

ที่บทความของ Alex Miller เกี่ยวกับข้อเสนอ Java 7


0

NB: ฉันไม่มีจุดพอที่จะแสดงความคิดเห็นดังนั้นอย่าลังเลที่จะย้ายสิ่งนี้เป็นความคิดเห็นเพื่อคำตอบที่เหมาะสม

ตรงกันข้ามกับความเชื่อที่ได้รับความนิยมซึ่งฉันไม่เคยเข้าใจมาก่อนว่ามันมาจากอะไร. net ได้นำเอายาสามัญมาใช้โดยไม่ต้องใช้ความเข้ากันได้แบบย้อนหลังและพวกเขาก็ใช้ความพยายามอย่างชัดเจน คุณไม่จำเป็นต้องเปลี่ยนรหัส. net 1.0 ที่ไม่ใช่แบบทั่วไปเป็น generics เพียงเพื่อใช้ใน. net 2.0 ทั้งรายการทั่วไปและไม่ใช่ทั่วไปยังคงมีอยู่ใน. NET Framework 2.0 แม้กระทั่ง 4.0 ไม่ว่าจะด้วยเหตุผลใด ๆ ดังนั้นรหัสเก่าที่ยังคงใช้ ArrayList ที่ไม่ใช่แบบทั่วไปจะยังคงใช้งานได้และใช้คลาส ArrayList เหมือนเดิม ความเข้ากันได้ของรหัสย้อนหลังนั้นได้รับการปรับปรุงตั้งแต่ 1.0 จนถึงปัจจุบัน ... ดังนั้นแม้ใน. net 4.0 คุณยังคงต้องเลือกใช้คลาสที่ไม่ใช่ชื่อสามัญจาก 1.0 BCL หากคุณเลือกที่จะทำ

ดังนั้นฉันจึงไม่คิดว่าจาวาต้องแยกความเข้ากันได้แบบย้อนหลังเพื่อรองรับข้อมูลทั่วไปที่แท้จริง


นั่นไม่ใช่ความเข้ากันได้แบบย้อนกลับที่คนพูดถึง แนวคิดคือความเข้ากันได้แบบย้อนหลังสำหรับรันไทม์ : โค้ดที่เขียนโดยใช้ generics ใน. NET 2.0 ไม่สามารถรันบน. NET Framework / CLR เวอร์ชันเก่าได้ ในทำนองเดียวกันถ้า Java แนะนำทั่วไป "จริง" รหัส Java รุ่นใหม่จะไม่สามารถทำงานบน JVM รุ่นเก่าได้
tzaman

นั่นคือ. net ไม่ใช่ generics ต้องมีการคอมไพล์ซ้ำเพื่อกำหนดเป้าหมาย CLR รุ่นที่ระบุเสมอ มีความเข้ากันได้ bytecode มีความเข้ากันได้ของรหัส และฉันก็ตอบกลับโดยเฉพาะเกี่ยวกับความต้องการในการแปลงรหัสเก่าที่ใช้รายการเก่าเพื่อใช้รายการทั่วไปใหม่ซึ่งไม่เป็นความจริง แต่อย่างใด
Sheepy

1
ผมคิดว่าผู้คนกำลังพูดเกี่ยวกับการเข้ากันได้ไปข้างหน้า เช่นโค้ด. net 2.0 ที่จะรันบน. net 1.1 ซึ่งจะแตกเพราะรันไทม์ 1.1 ไม่รู้อะไรเลยเกี่ยวกับ 2.0 "pseudo-class" ไม่เป็นอย่างนั้นหรือว่า "java ไม่ใช้จริงทั่วไปเพราะพวกเขาต้องการรักษาความเข้ากันได้ไปข้างหน้า"? (แทนที่จะย้อนหลัง)
Sheepy

ปัญหาความเข้ากันได้นั้นบอบบาง ฉันไม่คิดว่าปัญหาคือการเพิ่ม generics "ของจริง" ใน Java จะส่งผลกระทบต่อโปรแกรมใด ๆ ที่ใช้ Java รุ่นเก่า แต่รหัสที่ใช้ generics "ปรับปรุงใหม่" จะมีการแลกเปลี่ยนวัตถุดังกล่าวด้วยรหัสเก่าที่ยากกว่า ไม่รู้อะไรเกี่ยวกับรูปแบบใหม่ สมมติว่าตัวอย่างเช่นโปรแกรมที่มีArrayList<Foo>ที่มันต้องการที่จะส่งผ่านไปยังวิธีการเก่าซึ่งควรจะเติมกับกรณีของArrayList FooหากArrayList<foo>ไม่ใช่สิ่งArrayListใดสิ่งนั้นจะทำงานอย่างไร
supercat
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.