เหตุใดฉันจึงสามารถกำหนด 0.0 ให้กับค่าการแจงนับได้ แต่ไม่ใช่ 1.0


90

ด้วยความอยากรู้อยากเห็น: เหตุใดฉันจึงสามารถกำหนด 0.0 ให้กับตัวแปรที่เป็นประเภทการแจงนับ แต่ไม่ใช่ 1.0 ดูรหัสต่อไปนี้:

public enum Foo
{
    Bar,
    Baz
}

class Program
{
    static void Main()
    {
        Foo value1 = 0.0;
        Foo value2 = 1.0;   // This line does not compile
        Foo value3 = 4.2;   // This line does not compile
    }
}

ฉันคิดว่าการแปลงระหว่างประเภทตัวเลขและค่าการนับจะได้รับอนุญาตผ่านการร่ายเท่านั้น? นั่นคือฉันสามารถเขียน Foo value2 = (Foo) 1.0;เพื่อให้บรรทัด 2 ในMainคอมไพล์ได้ เหตุใดจึงมีข้อยกเว้นสำหรับค่า0.0ใน C #


17
สำหรับฉันมันแปลกที่คุณสามารถกำหนด 0.0 สองเท่าให้กับ enum ที่กำหนดเองได้ ไม่ใช่ว่าคุณไม่สามารถกำหนด1.0ตัวอักษรให้กับ enum ที่กำหนดเองได้
Ilya Ivanov

2
ฉันสงสัยว่าคอมไพเลอร์กำลังปฏิบัติต่อมัน0แทน ผมมีคำถามที่คล้ายกันครั้งเดียวและ Rawling โพสต์คำตอบที่ดีที่นี่
Amicable

2
IdeOneไม่รวบรวมมัน
Johnny Mopp

คำตอบ:


98

เป็นบั๊กที่คุณสามารถใช้ 0.0 คอมไพเลอร์ถือว่านิพจน์คงที่ทั้งหมดโดยปริยายโดยมีค่าเป็นศูนย์เป็นเพียง 0

ตอนนี้ถูกต้องสำหรับคอมไพลเลอร์ที่อนุญาตให้มีการแปลงโดยนัยจากintนิพจน์คงที่เป็น0 เป็น enum ของคุณตามส่วน 6.1.3 ของข้อกำหนด C # 5:

การแปลงการแจงนับโดยปริยายอนุญาตให้แปลงทศนิยม - จำนวนเต็ม - ลิเทอรัล 0 เป็นชนิด enum ใด ๆ และเป็นประเภทที่ไม่สามารถระบุได้ซึ่งมีประเภทที่อยู่ภายใต้การระบุประเภท ในกรณีหลังการแปลงจะได้รับการประเมินโดยการแปลงเป็น enum-type ที่อยู่ภายใต้และตัดผลลัพธ์ (§4.1.10)

ฉันเคยพูดคุยกับทีม C # เกี่ยวกับเรื่องนี้มาก่อน: พวกเขาชอบที่จะลบการแปลงโดยไม่ได้ตั้งใจจาก 0.0 (และ 0.0m และ 0.0f) เป็นค่า enum แต่น่าเสียดายที่ฉันรวบรวมรหัสมากเกินไป - แม้ว่า ไม่ควรได้รับอนุญาตตั้งแต่แรก

mcsคอมไพเลอร์Mono ห้ามการแปลงทศนิยมเหล่านี้ทั้งหมดแม้ว่าจะอนุญาต:

const int Zero = 0;
...

SomeEnum x = Zero;

แม้ว่าZeroจะเป็นนิพจน์คงที่ แต่ไม่ใช่ทศนิยม - จำนวนเต็ม - ลิเทอรัล

ฉันคงไม่แปลกใจที่เห็นการเปลี่ยนแปลงข้อกำหนด C # ในอนาคตเพื่ออนุญาตให้นิพจน์คงที่จำนวนเต็มใด ๆ ที่มีค่าเป็น 0 (เช่นเพื่อเลียนแบบmcs) แต่ฉันไม่คาดหวังว่าการแปลงทศนิยมจะถูกต้องอย่างเป็นทางการ (ฉันเคยคิดผิดเกี่ยวกับการทำนายอนาคตของ C # แน่นอน ... )


3
ต่อสเปคก็หมายถึงเฉพาะที่จะเป็นตัวอักษร 0. ดังนั้นจึงควรปฏิเสธ1-1- คงที่การแสดงออกที่มีค่าของint 0แต่เท่าที่สังเกตคอมไพเลอร์ไม่ตรงกับสเป็คตรงนี้
Damien_The_Unbeliever

4
it broke too much code- มันยากมากที่จะจินตนาการถึงเหตุผลในการเขียนโค้ดดังกล่าว
Ilya Ivanov

1
@ObsidianPhoenix: ฉันไม่แน่ใจว่าคุณหมายถึงอะไร เทียบเท่ากับ: SomeEnum x = (SomeEnum) 0;. นั่นคือกรณีที่มีชื่อศูนย์หรือไม่
Jon Skeet

2
@ObsidianPhoenix: ไม่เพราะค่าของTest.Fooคือ 1 ไม่ใช่ 0 ... อีกครั้งนั่นเหมือนกับที่คุณเขียนTest v1 = (Test) 0;- และพฤติกรรมนั้นมีค่าใด ๆที่ไม่ใช่ค่าที่มีชื่อใน enum
Jon Skeet

2
@JonSkeet จะได้รับการแก้ไขใน Roslyn หรือไม่?
สูงสุด

98

คำตอบของจอนถูกต้อง ฉันจะเพิ่มประเด็นต่อไปนี้

  • ฉันทำให้เกิดข้อผิดพลาดที่โง่และน่าอายนี้ ขอโทษมากมาย.

  • จุดบกพร่องเกิดจากการที่ฉันเข้าใจความหมายของเพรดิเคต "นิพจน์เป็นศูนย์" ผิดพลาดในคอมไพเลอร์ ฉันเชื่อว่ามันกำลังตรวจสอบความเท่าเทียมกันของจำนวนเต็มศูนย์เท่านั้นเมื่อในความเป็นจริงมันกำลังตรวจสอบเพิ่มเติมตามบรรทัดของ "นี่เป็นค่าเริ่มต้นของประเภทนี้หรือไม่" ในความเป็นจริงในเวอร์ชันก่อนหน้าของบั๊กนั้นเป็นไปได้ที่จะกำหนดค่าเริ่มต้นของประเภทใด ๆ ให้กับ enum! ตอนนี้เป็นเพียงค่าเริ่มต้นของตัวเลขเท่านั้น (บทเรียน: ตั้งชื่อผู้ช่วยเหลือของคุณอย่างระมัดระวัง)

  • พฤติกรรมที่ฉันพยายามนำไปใช้ที่ฉันทำผิดนั้นเป็นวิธีแก้ปัญหาสำหรับจุดบกพร่องที่แตกต่างกันเล็กน้อย คุณสามารถอ่านเรื่องราวเลวร้ายทั้งหมดได้ที่นี่: https://docs.microsoft.com/en-us/archive/blogs/ericlippert/the-root-of-all-evil-part-oneและ https://docs.microsoft .com / en-us / archive / blogs / ericlippert / the-root-of-all-evil-part-สอง (บทเรียน: การแนะนำจุดบกพร่องใหม่ที่แย่กว่าเดิมเป็นเรื่องง่ายมากในขณะที่แก้ไขข้อบกพร่องเก่า)

  • ทีม C # ตัดสินใจที่จะสร้างพฤติกรรมที่ผิดพลาดนี้แทนที่จะแก้ไขเนื่องจากความเสี่ยงในการทำลายรหัสที่มีอยู่โดยไม่มีประโยชน์ที่น่าสนใจนั้นสูงเกินไป (บทเรียน: ทำให้ถูกต้องในครั้งแรก!)

  • รหัสที่ฉันเขียนใน Roslyn เพื่อรักษาพฤติกรรมนี้สามารถพบได้ในวิธีการIsConstantNumericZeroในhttps://github.com/dotnet/roslyn/blob/master/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionsBase.cs - ดูรายละเอียดเพิ่มเติมว่าพฤติกรรมของ Roslyn คืออะไร ฉันเขียนโค้ดเกือบทั้งหมดในไดเร็กทอรี Conversions ฉันขอแนะนำให้คุณอ่านทั้งหมดเนื่องจากมีข้อเท็จจริงที่น่าสนใจมากมายเกี่ยวกับการที่ C # แตกต่างจากข้อกำหนดในความคิดเห็น ฉันตกแต่งแต่ละชิ้นด้วย SPEC VIOLATION เพื่อให้ง่ายต่อการค้นหา

อีกหนึ่งจุดที่น่าสนใจ: C # ยังอนุญาตให้ใช้ค่า enum ใด ๆในตัวเริ่มต้น enum โดยไม่คำนึงถึงความชัดเจน:

enum E { A = 1 }
enum F { B = E.A }  // ???

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


10
มันเจ๋งมากในที่สุดฉันก็ได้เห็นโค้ดที่คุณเขียน น่ากลัวที่ซอร์สโค้ด Roslyn เป็นโอเพ่นซอร์ส ตอนนี้ฉันเข้าใจอย่างสมบูรณ์แล้วว่ามีเหตุผลที่ถูกต้อง (ทางเทคนิค / กฎหมาย) ที่จะไม่ให้ประวัติการเปลี่ยนแปลง แต่การดูประวัติการเปลี่ยนแปลงจะดีมากเพื่อดูว่าโค้ดมีการพัฒนาอย่างไร
SolutionYogi

The C# team decided to enshrine this buggy behaviour rather than fixing it because the risk of breaking existing code for no compelling benefit was too high.ฉันไม่คิดว่าจะมีคนมากมายที่ต้องพึ่งพาพฤติกรรมนี้และมันเป็นหนึ่งในความแปลกประหลาดเหล่านี้ที่อาจดีกว่าที่จะแก้ไข อย่างไรก็ตามมันไม่ได้ทำอันตรายอะไรมากนัก (ยกเว้นโครงการที่ใช้ข้อมูลจำเพาะ)
Aidiakapi

5
@Aidiakapi: อันที่จริงจำนวนคนที่ได้รับผลกระทบน่าจะไม่มาก มันไม่ใช่ศูนย์ ทีม C # ให้ความสำคัญกับการเปลี่ยนแปลงอย่างจริงจัง เป็นเรื่องง่ายสำหรับคุณที่จะบอกว่าการแก้ไขจะดีกว่า คุณไม่จำเป็นต้องรับมือกับลูกค้าที่โกรธแค้นที่โทรหารองประธานของคุณเพื่อบ่นว่าการเปลี่ยนแปลงเล็กน้อยของคุณซึ่งไม่ได้เพิ่มประโยชน์ใด ๆ เลยก็ตามที่ทำให้การรวมระบบของพวกเขาล่าช้าในแต่ละวัน
Eric Lippert

3
มันแย่ลง การเปลี่ยนแปลงทั้งหมดดังกล่าวจะแสดงรายการในคู่มือการย้าย Framework ของ Microsoft ยิ่งรายการนี้มีความยาวมากเท่าไหร่ผู้ใช้ก็ยิ่งลังเลที่จะย้ายแอปพลิเคชันของตน ดังนั้นแม้แต่การเปลี่ยนแปลงเล็กน้อยก็ทำให้เกิด: 1. มีแอปพลิเคชั่นจำนวนน้อยที่พัง 2. ผู้ใช้จำนวนน้อยที่ปฏิเสธที่จะอัปเกรด (แม้ว่าปัญหาจะไม่ส่งผลกระทบก็ตาม) 3. ผู้ใช้จำนวนไม่น้อยที่เสียทรัพยากรในการประเมินว่าการเปลี่ยนแปลงครั้งใหญ่ส่งผลกระทบต่อพวกเขาหรือไม่ 4. ผู้ใช้จาก # 1 # 2 และ # 3 บ่นกับคนอื่น ๆ
Brian

@EricLippert หาก "ข้อมูลจำเพาะค่อนข้างคลุมเครือ" การอัปเดตสเป็คจะไม่สมเหตุสมผลหรือไม่ (คำถามจริง!)
ยากอบ

10

การแจงนับใน C # มาจากค่าอินทิกรัลของนิยาม เพื่อความสอดคล้อง C # ไม่ควรยอมรับการมอบหมายอย่างใดอย่างหนึ่งเหล่านี้ แต่0.0จะถือว่าเป็นอินทิกรั0ล นี่อาจเป็นการยึดจาก C โดยที่ลิเทอรัล0ได้รับการปฏิบัติเป็นพิเศษและโดยพื้นฐานแล้วสามารถใช้ประเภทใดก็ได้ - จำนวนเต็มเลขทศนิยมตัวชี้โมฆะ ... คุณตั้งชื่อมัน


3
คำถามคือทำไม ? ถ้าคุณไปที่IL- มันกำลังส่งค่าจำนวนเต็มไปยังสแต็กIL_0001: ldc.i4.0
Ilya Ivanov

@IlyaIvanov ดูการอัปเดต แต่พูดตามตรงคำตอบคือ“ ไม่มีเหตุผลที่ดี”
Konrad Rudolph

2
ฉันคิดว่านี่เป็นหนึ่งในกรณีเหล่านั้นที่ถ้าคุณดูที่ข้อมูลจำเพาะ C # มันไม่ถูกกฎหมาย แต่ถ้าคุณดูคอมไพเลอร์ C # ใด ๆ ที่ MS ผลิตขึ้นมันก็ทำเช่นนี้
Damien_The_Unbeliever

3

enumมีวัตถุประสงค์อย่างแท้จริง (ในทุกภาษาที่รองรับ) เพื่อเป็นวิธีการทำงานกับสตริง (ป้ายกำกับ) ที่มีความหมายและไม่ซ้ำใครแทนที่จะเป็นค่าตัวเลข ดังนั้นในตัวอย่างของคุณคุณควรใช้BarและBazเมื่อจัดการกับประเภทข้อมูลที่แจกแจงFooเท่านั้น คุณไม่ควรใช้ (เปรียบเทียบหรือกำหนด) จำนวนเต็มแม้ว่าคอมไพเลอร์จำนวนมากจะช่วยให้คุณหลีกเลี่ยงมันได้ (enums มักจะเป็นจำนวนเต็มภายใน) และในกรณีนี้คอมไพเลอร์จะถือว่า 0.0 โดยไม่ใส่ใจเป็น 0

ตามแนวคิดแล้วควรจะเพิ่มจำนวนเต็มnให้กับค่าที่แจกแจงได้ถูกต้องเพื่อให้ได้ค่าnเพิ่มเติมลงไปในบรรทัดหรือใช้val2 - val1เพื่อดูว่าพวกมันอยู่ห่างกันแค่ไหน แต่เว้นแต่ข้อกำหนดภาษาจะอนุญาตอย่างชัดเจน หลีกเลี่ยงมัน (คิดว่าค่าที่แจกแจงเป็นเหมือนตัวชี้ C ในแบบที่คุณสามารถใช้ได้) ไม่มีเหตุผลที่ไม่สามารถใช้ enums กับตัวเลขทศนิยมและการเพิ่มที่คงที่ระหว่างค่าเหล่านี้ แต่ฉันไม่เคยได้ยิน สิ่งนี้ทำในภาษาใดก็ได้


ฉันรู้ว่าฉันไม่ได้ใช้ enums แบบนั้นใน C # - แต่ฉันพบเครื่องมือพัฒนาสมองนี้และอยากรู้ว่าทำไม 0.0 ถึงใช้งานได้ แต่ 1.0 ไม่ใช่ ฉันรู้ว่ามันต้องมีบางอย่างกับคอมไพเลอร์ C # เพราะคุณจะเห็นว่ารหัส IL สำหรับFoo v1 = 0.0;นั้นเหมือนกับสำหรับFoo v2 = Foo.Bar.
feO2x
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.