ความแตกต่างระหว่าง C # และตัวดำเนินการ ternary ของ Java (? :)


94

ฉันเป็นมือใหม่ C # และฉันเพิ่งพบปัญหา มีความแตกต่างระหว่าง C # และ Java เมื่อจัดการกับตัวดำเนินการ ternary ( ? :)

ในส่วนรหัสต่อไปนี้เหตุใดบรรทัดที่ 4 จึงไม่ทำงาน คอมไพเลอร์แสดงข้อความแสดงข้อผิดพลาดของthere is no implicit conversion between 'int' and 'string'. บรรทัดที่ 5 ก็ใช้ไม่ได้เช่นกัน ทั้งสองListเป็นวัตถุไม่ใช่เหรอ?

int two = 2;
double six = 6.0;
Write(two > six ? two : six); //param: double
Write(two > six ? two : "6"); //param: not object
Write(two > six ? new List<int>() : new List<string>()); //param: not object

อย่างไรก็ตามรหัสเดียวกันทำงานใน Java:

int two = 2;
double six = 6.0;
System.out.println(two > six ? two : six); //param: double
System.out.println(two > six ? two : "6"); //param: Object
System.out.println(two > six ? new ArrayList<Integer>()
                   : new ArrayList<String>()); //param: Object

คุณลักษณะภาษาใดใน C # หายไป ถ้ามีทำไมถึงไม่เพิ่ม?


4
ตามmsdn.microsoft.com/en-us/library/ty67wk28.aspx "ประเภทของ first_expression และ second_expression ต้องเหมือนกันหรือต้องมีการแปลงโดยนัยจากประเภทหนึ่งไปยังอีกประเภทหนึ่ง"
Egor

2
ฉันไม่ได้ทำ C # มาสักพักแล้ว แต่ข้อความที่คุณอ้างนั้นไม่ได้พูดใช่ไหม สองสาขาของ ternary ต้องส่งคืนชนิดข้อมูลเดียวกัน คุณกำลังกำหนดให้เป็น int และ String C # ไม่รู้วิธีแปลง int เป็น String โดยอัตโนมัติ คุณต้องใส่การแปลงประเภทที่ชัดเจน
เจ

2
@ blackr1234 เพราะ an intไม่ใช่วัตถุ มันเป็นแบบดั้งเดิม
Code-Apprentice

3
@ Code-Apprentice ใช่มันแตกต่างกันมาก - C # มีการ reification รันไทม์ดังนั้นรายการจึงไม่เกี่ยวข้องกัน โอ้และคุณยังสามารถโยนความแปรปรวนร่วม / ความแตกต่างของอินเทอร์เฟซทั่วไปลงในส่วนผสมได้หากคุณต้องการเพื่อแนะนำระดับความสัมพันธ์;)
Lucas Trzesniewski

3
เพื่อประโยชน์ของ OP: type erasure ใน Java หมายความว่าArrayList<String>และArrayList<Integer>กลายเป็นเพียงArrayListใน bytecode ซึ่งหมายความว่าพวกเขาดูเหมือนจะเป็นประเภทเดียวกันในขณะรันไทม์ เห็นได้ชัดว่าใน C # เป็นประเภทต่างๆ
Code-Apprentice

คำตอบ:


106

เมื่อพิจารณาถึงข้อกำหนดภาษา C # 5 ส่วน 7.14: ตัวดำเนินการตามเงื่อนไขเราจะเห็นสิ่งต่อไปนี้:

  • ถ้า x มีประเภท X และ y มีประเภท Y แล้ว

    • หากการแปลงโดยนัย (§6.1) มีอยู่จาก X เป็น Y แต่ไม่ใช่จาก Y ถึง X ดังนั้น Y จะเป็นประเภทของนิพจน์เงื่อนไข

    • หากการแปลงโดยปริยาย (§6.1) มีอยู่จาก Y เป็น X แต่ไม่ใช่จาก X เป็น Y ดังนั้น X คือประเภทของนิพจน์เงื่อนไข

    • มิฉะนั้นจะไม่สามารถกำหนดประเภทนิพจน์ได้และเกิดข้อผิดพลาดเวลาคอมไพล์

กล่าวอีกนัยหนึ่ง: มันพยายามค้นหาว่า x และ y สามารถแปลงเป็นกันและกันได้หรือไม่และถ้าไม่เกิดข้อผิดพลาดในการคอมไพล์ ในกรณีของเราintและstringไม่มีการแปลงอย่างชัดเจนหรือโดยปริยายดังนั้นจะไม่รวบรวม

ตรงกันข้ามกับข้อกำหนดภาษา Java 7 ส่วนที่ 15.25: Conditional Operator :

  • ถ้าตัวถูกดำเนินการที่สองและสามมีชนิดเดียวกัน (ซึ่งอาจเป็นประเภทว่าง) นั่นคือชนิดของนิพจน์เงื่อนไข ( ไม่ )
  • ถ้าตัวถูกดำเนินการตัวที่สองและตัวที่สามเป็นแบบดั้งเดิม T และอีกประเภทหนึ่งเป็นผลมาจากการใช้การแปลงมวย (§5.1.7) เป็น T ประเภทของนิพจน์เงื่อนไขคือ T. ( NO )
  • ถ้าตัวถูกดำเนินการตัวที่สองและตัวที่สามเป็นประเภท null และอีกประเภทหนึ่งเป็นประเภทการอ้างอิงประเภทของนิพจน์เงื่อนไขคือประเภทอ้างอิงนั้น ( ไม่ )
  • มิฉะนั้นหากตัวถูกดำเนินการที่สองและสามมีประเภทที่สามารถแปลงได้ (§5.1.8) เป็นประเภทตัวเลขจะมีหลายกรณี: ( NO )
  • มิฉะนั้นตัวถูกดำเนินการที่สองและสามเป็นประเภท S1 และ S2 ตามลำดับ ให้ T1 เป็นประเภทที่เกิดจากการใช้การแปลงมวยเป็น S1 และให้ T2 เป็นประเภทที่เป็นผลมาจากการใช้การแปลงมวยเป็น S2
    ประเภทของนิพจน์เงื่อนไขเป็นผลมาจากการใช้การแปลงการดักจับ (§5.1.10) กับ lub (T1, T2) (§15.12.2.7) ( ใช่ )

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


1
ฉันอ่านข้อกำหนด C # ว่าต้องมีการแปลงโดยนัยอย่างน้อยหนึ่งทิศทาง ข้อผิดพลาดของคอมไพเลอร์เกิดขึ้นเฉพาะเมื่อไม่มีการแปลงโดยนัยในทิศทางใดทิศทางหนึ่ง
Code-Apprentice

1
แน่นอนว่านี่ยังคงเป็นคำตอบที่สมบูรณ์ที่สุดเนื่องจากคุณอ้างจากข้อกำหนดโดยตรง
Code-Apprentice

สิ่งนี้เปลี่ยนแปลงได้จริงกับ Java 5 เท่านั้น (ฉันคิดว่าอาจเป็น 6) ก่อนหน้านั้น Java มีลักษณะการทำงานเหมือนกับ C # เนื่องจากวิธีการใช้ enums อาจทำให้เกิดความประหลาดใจใน Java มากกว่า C # แม้ว่าจะเป็นการเปลี่ยนแปลงที่ดี
Voo

@Voo: FYI, Java 5 และ Java 6 มีเวอร์ชัน spec เดียวกัน ( The Java Language Specification , Third Edition) ดังนั้นจึงไม่เปลี่ยนแปลงใน Java 6 อย่างแน่นอน
ruakh

86

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

ในกรณีของตัวดำเนินการ ternary C # พยายามเลือกประเภท int และ string ที่กว้างกว่า แต่ไม่ใช่ประเภททั่วไปมากกว่า แทนที่จะเลือกประเภทที่ไม่ใช่ตัวเลือกในตอนแรกเช่นอ็อบเจกต์ C # ตัดสินใจว่าไม่มีประเภทใดที่สามารถอนุมานได้

ฉันทราบด้วยว่านี่เป็นไปตามหลักการออกแบบอื่นของ C #: หากมีบางอย่างผิดปกติโปรดแจ้งให้ผู้พัฒนาทราบ ภาษานี้ไม่ได้บอกว่า "ฉันจะเดาว่าคุณหมายถึงอะไรและสับสนไปหมดถ้าฉันทำได้" ภาษากล่าวว่า "ฉันคิดว่าคุณเขียนบางอย่างที่น่าสับสนที่นี่และฉันจะบอกคุณเกี่ยวกับเรื่องนั้น"

นอกจากนี้ฉันทราบว่า C # ไม่ได้ให้เหตุผลจากตัวแปรถึงค่าที่กำหนดแต่เป็นทิศทางอื่น C # ไม่ได้ระบุว่า "คุณกำลังกำหนดให้กับตัวแปรออบเจ็กต์ดังนั้นนิพจน์ต้องแปลงเป็นอ็อบเจ็กต์ได้ดังนั้นฉันจะตรวจสอบให้แน่ใจว่าเป็น" แต่ C # บอกว่า "นิพจน์นี้ต้องมีประเภทและฉันต้องสามารถอนุมานได้ว่าประเภทนั้นเข้ากันได้กับวัตถุ" เนื่องจากนิพจน์ไม่มีชนิดจึงเกิดข้อผิดพลาด


6
ฉันผ่านย่อหน้าแรกมาแล้วสงสัยว่าทำไมความแปรปรวนร่วม / ความแตกต่างอย่างกะทันหันฉันเริ่มเห็นด้วยกับย่อหน้าที่สองมากขึ้นจากนั้นฉันก็เห็นว่าใครเป็นคนเขียนคำตอบ ... น่าจะเดาได้เร็วกว่านี้ คุณเคยสังเกตไหมว่าคุณใช้ 'ยีราฟ' เป็นตัวอย่างมากแค่ไหน? คุณต้องชอบยีราฟแน่ ๆ
Pharap

21
ยีราฟสุดยอดมาก!
Eric Lippert

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

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

6
@ Eric int? b = (a != 0 ? a : (int?) null)ฉันเดิมวิ่งเข้าไปในปัญหานี้กับ นอกจากนี้ยังมีคำถามนี้พร้อมกับคำถามที่เชื่อมโยงทั้งหมดที่ด้านข้าง หากคุณดำเนินการตามลิงก์ต่อไปมีอยู่ไม่น้อย ในการเปรียบเทียบฉันไม่เคยได้ยินว่ามีใครประสบปัญหาในโลกแห่งความเป็นจริงกับวิธีที่ Java ทำ
BlueRaja - Danny Pflughoeft

24

เกี่ยวกับส่วนยาสามัญ:

two > six ? new List<int>() : new List<string>()

ใน C # คอมไพลเลอร์พยายามแปลงส่วนนิพจน์ทางขวามือเป็นประเภททั่วไป เนื่องจากList<int>และList<string>เป็นประเภทที่สร้างขึ้นสองประเภทที่แตกต่างกันประเภทหนึ่งไม่สามารถแปลงเป็นประเภทอื่นได้

ใน Java คอมไพเลอร์พยายามที่จะหา supertype ทั่วไปแทนการแปลงเพื่อรวบรวมของรหัสที่เกี่ยวข้องกับการใช้งานโดยนัยของสัญลักษณ์และลบออกประเภท ;

two > six ? new ArrayList<Integer>() : new ArrayList<String>()

มีประเภทการคอมไพล์เป็นArrayList<?>(อันที่จริงอาจเป็นได้เช่นกันArrayList<? extends Serializable>หรือArrayList<? extends Comparable<?>>ขึ้นอยู่กับบริบทการใช้งานเนื่องจากทั้งสองเป็นซูเปอร์ไทป์ทั่วไปทั่วไป) และประเภทรันไทม์ของดิบArrayList(เนื่องจากเป็นซุปเปอร์ไทป์ดิบทั่วไป)

ยกตัวอย่างเช่น(ทดสอบด้วยตัวคุณเอง) ,

void test( List<?> list ) {
    System.out.println("foo");
}

void test( ArrayList<Integer> list ) { // note: can't use List<Integer> here
                                 // since both test() methods would clash after the erasure
    System.out.println("bar");
}

void test() {
    test( true ? new ArrayList<Object>() : new ArrayList<Object>() ); // foo
    test( true ? new ArrayList<Integer>() : new ArrayList<Object>() ); // foo 
    test( true ? new ArrayList<Integer>() : new ArrayList<Integer>() ); // bar
} // compiler automagically binds the correct generic QED

จริงๆแล้วใช้Write(two > six ? new List<object>() : new List<string>());ไม่ได้ทั้ง
blackr1234

2
@ blackr1234 เป๊ะ: ฉันไม่ต้องการพูดซ้ำสิ่งที่คำตอบอื่น ๆ พูดไปแล้ว แต่นิพจน์ด้านท้ายจำเป็นต้องประเมินเป็นประเภทและคอมไพเลอร์จะไม่พยายามค้นหา "ตัวส่วนร่วมต่ำสุด" ของทั้งสอง
Mathieu Guindon

2
อันที่จริงนี่ไม่ได้เกี่ยวกับการลบประเภท แต่เกี่ยวกับประเภทสัญลักษณ์แทน การลบArrayList<String>และArrayList<Integer>จะเป็นArrayList(ชนิดดิบ) แต่ประเภทที่อนุมานสำหรับตัวดำเนินการที่เกี่ยวข้องนี้คือArrayList<?>(ประเภทสัญลักษณ์แทน) โดยทั่วไปแล้วว่ารันไทม์ Java ใช้ generics ผ่านการลบประเภทไม่มีผลต่อประเภทเวลาคอมไพล์ของนิพจน์
ทำบุญ

1
@vaxquis ขอบคุณ! ฉันเป็นคน C # ชื่อสามัญ Java ทำให้ฉันสับสน ;-)
Mathieu Guindon

2
จริงๆแล้วประเภทของ two > six ? new ArrayList<Integer>() : new ArrayList<String>()คือArrayList<? extends Serializable&Comparable<?>>ซึ่งหมายความว่าคุณอาจกำหนดให้กับตัวแปรประเภทArrayList<? extends Serializable>และตัวแปรประเภทArrayList<? extends Comparable<?>>แต่แน่นอนArrayList<?>ว่าเทียบเท่ากับArrayList<? extends Object>ได้เช่นกัน
Holger

6

ทั้งใน Java และ C # (และภาษาอื่น ๆ ส่วนใหญ่) ผลลัพธ์ของนิพจน์จะมีประเภท ในกรณีของตัวดำเนินการ ternary จะมีนิพจน์ย่อยที่เป็นไปได้สองรายการที่ประเมินสำหรับผลลัพธ์และทั้งสองต้องมีชนิดเดียวกัน ในกรณีของ Java intสามารถแปลงตัวแปรเป็นออIntegerโต้บ็อกซ์ได้ ตอนนี้เนื่องจากทั้งสองIntegerและStringสืบทอดจากObjectพวกเขาสามารถแปลงเป็นประเภทเดียวกันได้โดยการ จำกัด การแปลงอย่างง่าย

บนมืออื่น ๆ , ใน C # ความintเป็นดั้งเดิมและไม่มีการแปลงส่อไปหรืออื่นstringobject


ขอบคุณสำหรับคำตอบ. Autoboxing คือสิ่งที่ฉันกำลังคิดอยู่ C # ไม่อนุญาตให้ทำกล่องอัตโนมัติหรือไม่
blackr1234

2
ฉันไม่คิดว่าสิ่งนี้ถูกต้องเนื่องจากการชกมวย int ไปยังวัตถุนั้นเป็นไปได้อย่างสมบูรณ์แบบใน C # ความแตกต่างก็คือฉันเชื่อว่า C # ใช้วิธีการที่ผ่อนคลายมากในการพิจารณาว่าจะได้รับอนุญาตหรือไม่
Jeroen Vannevel

9
สิ่งนี้ไม่เกี่ยวข้องกับการชกมวยอัตโนมัติซึ่งจะเกิดขึ้นได้มากใน C # ความแตกต่างคือ C # ไม่พยายามค้นหา supertype ทั่วไปในขณะที่ Java (ตั้งแต่ Java 5 เท่านั้น!) คุณสามารถทดสอบได้อย่างง่ายดายโดยพยายามดูว่าจะเกิดอะไรขึ้นถ้าคุณลองด้วยคลาสที่กำหนดเอง 2 คลาส (ซึ่งทั้งสองสืบทอดมาจากวัตถุอย่างชัดเจน) นอกจากนี้ยังมีการแปลงนัยจากการint object
Voo

3
และเพื่อ nitpick เพียงเล็กน้อยเพิ่มเติมได้ที่: การแปลงจากInteger/StringการObjectไม่ได้เป็นแปลงกวดขันก็ตรงข้ามแน่นอน :-)
Voo

1
IntegerและStringยังนำไปใช้SerializableและการComparableกำหนดให้กับอย่างใดอย่างหนึ่งก็จะได้ผลเช่นกันComparable<?> c=condition? 6: "6";หรือList<? extends Serializable> l = condition? new ArrayList<Integer>(): new ArrayList<String>();เป็นรหัส Java ที่ถูกกฎหมาย
Holger

5

นี่ค่อนข้างตรงไปตรงมา ไม่มีการแปลงโดยนัยระหว่างสตริงและ int ตัวดำเนินการ ternary ต้องการตัวถูกดำเนินการสองตัวสุดท้ายเพื่อให้มีชนิดเดียวกัน

ลอง:

Write(two > six ? two.ToString() : "6");

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