เหตุใด C # จึงไม่มีขอบเขตในบล็อกกรณี


38

ฉันกำลังเขียนรหัสนี้:

private static Expression<Func<Binding, bool>> ToExpression(BindingCriterion criterion)
{
    switch (criterion.ChangeAction)
    {
        case BindingType.Inherited:
            var action = (byte)ChangeAction.Inherit;
            return (x => x.Action == action);
        case BindingType.ExplicitValue:
            var action = (byte)ChangeAction.SetValue;
            return (x => x.Action == action);
        default:
            // TODO: Localize errors
            throw new InvalidOperationException("Invalid criterion.");
    }
}

และรู้สึกประหลาดใจที่พบข้อผิดพลาดในการคอมไพล์:

ตัวแปรท้องถิ่นชื่อ 'การกระทำ' ถูกกำหนดไว้แล้วในขอบเขตนี้

มันเป็นปัญหาที่แก้ไขได้ง่าย เพียงแค่กำจัดวินาทีที่varไม่ได้หลอกลวง

ตัวแปรที่ประกาศชัดเจนในcaseบล็อกมีขอบเขตของผู้ปกครองswitchแต่ฉันอยากรู้ว่าทำไมถึงเป็นเช่นนี้ ระบุว่า C # ไม่อนุญาตให้มีการดำเนินการที่จะตกผ่านกรณีอื่น ๆ (มันต้อง break , return, throwหรือgoto caseงบในตอนท้ายของทุกcaseบล็อก) มันดูเหมือนว่าค่อนข้างแปลกที่มันจะช่วยให้การประกาศตัวแปรภายในหนึ่งcaseที่จะใช้หรือขัดแย้งกับตัวแปรในอื่น ๆcase. ในคำอื่น ๆ ตัวแปรปรากฏอยู่ในcaseงบแม้ว่าการดำเนินการไม่สามารถ C # ใช้ความเจ็บปวดอย่างมากในการส่งเสริมการอ่านโดยการห้ามไม่ให้มีการสร้างภาษาอื่นที่สับสนหรือถูกทารุณกรรมได้ง่าย แต่ดูเหมือนว่ามันแค่ก่อให้เกิดความสับสน พิจารณาสถานการณ์สมมติต่อไปนี้:

  1. หากจะเปลี่ยนเป็น:

    case BindingType.Inherited:
        var action = (byte)ChangeAction.Inherit;
        return (x => x.Action == action);
    case BindingType.ExplicitValue:
        return (x => x.Action == action);
    

    ฉันได้รับ "การใช้งานตัวแปรท้องถิ่นที่ไม่ได้กำหนด 'การกระทำ' " นี่คือความสับสนเพราะในการสร้างอื่น ๆ ใน C # ที่ฉันสามารถคิดว่าvar action = ...จะเริ่มต้นตัวแปร แต่ที่นี่มันก็ประกาศ

  2. ถ้าฉันจะสลับกรณีเช่นนี้:

    case BindingType.ExplicitValue:
        action = (byte)ChangeAction.SetValue;
        return (x => x.Action == action);
    case BindingType.Inherited:
        var action = (byte)ChangeAction.Inherit;
        return (x => x.Action == action);
    

    ฉันได้รับ " ไม่สามารถใช้ 'การกระทำ' ตัวแปรท้องถิ่นก่อนที่จะมีการประกาศ " ดังนั้นลำดับของบล็อกเคสจึงมีความสำคัญที่นี่ในแบบที่ไม่ชัดเจน - โดยปกติฉันสามารถเขียนสิ่งเหล่านี้ในลำดับใดก็ได้ที่ฉันต้องการ แต่เนื่องจากvarต้องปรากฏในบล็อกแรกที่actionใช้ฉันจึงต้องปรับแต่งcaseบล็อก ตาม

  3. หากจะเปลี่ยนเป็น:

    case BindingType.Inherited:
        var action = (byte)ChangeAction.Inherit;
        return (x => x.Action == action);
    case BindingType.ExplicitValue:
        action = (byte)ChangeAction.SetValue;
        goto case BindingType.Inherited;
    

    จากนั้นฉันก็จะไม่เกิดข้อผิดพลาด แต่อย่างใดดูเหมือนว่าตัวแปรจะถูกกำหนดค่าก่อนที่จะมีการประกาศ
    (แม้ว่าฉันจะไม่สามารถนึกถึงเวลาที่คุณต้องการทำสิ่งนี้จริง ๆ - ฉันไม่เคยรู้goto caseมาก่อนว่าวันนี้)

ดังนั้นคำถามของฉันคือทำไมนักออกแบบของ C # ไม่caseบล็อกขอบเขตของตนเอง มีเหตุผลทางประวัติศาสตร์หรือทางเทคนิคสำหรับสิ่งนี้หรือไม่?


4
ประกาศactionตัวแปรของคุณก่อนswitchคำสั่งหรือใส่แต่ละกรณีไว้ในเครื่องหมายวงเล็บของตัวเองและคุณจะได้รับพฤติกรรมที่เหมาะสม
Robert Harvey

3
@ RobertHarvey ใช่และฉันสามารถไปให้ไกลขึ้นและเขียนสิ่งนี้โดยไม่ต้องใช้switchเลย - ฉันแค่อยากรู้เกี่ยวกับเหตุผลที่อยู่เบื้องหลังการออกแบบนี้
pswg

ถ้า c # ต้องการหยุดพักหลังจากทุกกรณีฉันคิดว่ามันจะต้องมีเหตุผลทางประวัติศาสตร์เช่นเดียวกับใน c / java ที่ไม่ใช่กรณี!
tgkprog

@tgkprog ฉันเชื่อว่ามันไม่ใช่เหตุผลทางประวัติศาสตร์และนักออกแบบของ C # ทำเพื่อจุดประสงค์เพื่อให้แน่ใจว่าข้อผิดพลาดทั่วไปของการลืม a breakไม่สามารถทำได้ใน C #
svick

12
ดูคำตอบของ Eric Lippert สำหรับคำถาม SO ที่เกี่ยวข้องนี้ stackoverflow.com/a/1076642/414076คุณอาจพอใจหรือไม่พอใจ มันอ่านว่า "เพราะนั่นเป็นวิธีที่พวกเขาเลือกที่จะทำในปี 1999"
Anthony Pegram

คำตอบ:


24

ฉันคิดว่าเหตุผลที่ดีคือในทุกกรณีขอบเขตของตัวแปรท้องถิ่น“ ปกติ” คือบล็อกที่คั่นด้วยเครื่องหมายวงเล็บ ( {}) ตัวแปรท้องถิ่นที่ไม่ปกติปรากฏในโครงสร้างพิเศษก่อนคำสั่ง (ซึ่งมักจะกระชาก) เช่นforตัวแปร loop usingหรือตัวแปรที่ประกาศใน

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

สำหรับการอ้างอิงกฎอยู่ในขอบเขต§3.7ของข้อมูลจำเพาะ C #:

  • ขอบเขตของตัวแปรโลคัลที่ประกาศในlocal-variable-declarationคือบล็อกที่มีการประกาศเกิดขึ้น

  • ขอบเขตของตัวแปรท้องถิ่นประกาศในสวิทช์บล็อกของswitchคำสั่งที่เป็นสวิทช์บล็อก

  • ขอบเขตของตัวแปรโลคัลที่ประกาศในfor-initializerของforคำสั่งคือfor-initializer , for-condition , for-iteratorและคำสั่งที่มีอยู่ของforคำสั่ง

  • ขอบเขตของตัวแปรที่ประกาศเป็นส่วนหนึ่งของคำสั่ง foreach , use-statement , lock-statementหรือquery-expressionถูกกำหนดโดยการขยายตัวของโครงสร้างที่กำหนด

(แม้ว่าฉันจะไม่แน่ใจว่าทำไมswitchบล็อกดังกล่าวอย่างชัดเจนเพราะมันไม่มีไวยากรณ์พิเศษสำหรับการลดลงของตัวแปรท้องถิ่นซึ่งแตกต่างจากการสร้างอื่น ๆ ที่กล่าวถึง)


1
+1 คุณสามารถระบุลิงก์ไปยังขอบเขตที่คุณต้องการได้หรือไม่
pswg

@pswg คุณสามารถค้นหาการเชื่อมโยงสำหรับการดาวน์โหลดข้อกำหนดใน MSDN (คลิกที่ลิงค์ที่ระบุว่า“ไมโครซอฟท์พัฒนาเครือข่าย (MSDN)” ที่หน้าที่.)
svick

ดังนั้นฉันจึงค้นหารายละเอียด Java และswitchesดูเหมือนจะทำงานเหมือนกันในเรื่องนี้ (ยกเว้นการกระโดดที่ต้องการในตอนท้ายของcase) ดูเหมือนว่าพฤติกรรมนี้ถูกคัดลอกมาจากที่นั่น ดังนั้นฉันเดาว่าคำตอบสั้น ๆ คือ - caseคำสั่งไม่สร้างบล็อกพวกเขาเพียงแค่กำหนดส่วนของswitchบล็อก - และดังนั้นจึงไม่มีขอบเขตด้วยตัวเอง
pswg

3
Re: ฉันไม่แน่ใจว่าทำไม switch block ถึงอย่างชัดเจน - ผู้เขียนสเป็คเพียงแค่จู้จี้จุกจิกโดยปริยายชี้ให้เห็นว่าswitch-blockมีไวยากรณ์ที่แตกต่างจากบล็อกปกติ
Eric Lippert

42

แต่มันก็เป็นเช่นนั้น คุณสามารถสร้างขอบเขตท้องถิ่นได้ทุกที่ด้วยการตัดบรรทัดด้วย{}

switch (criterion.ChangeAction)
{
  case BindingType.Inherited:
    {
      var action = (byte)ChangeAction.Inherit;
      return (x => x.Action == action);
    }
  case BindingType.ExplicitValue:
    {
      var action = (byte)ChangeAction.SetValue;
      return (x => x.Action == action);
    }
  default:
    // TODO: Localize errors
    throw new InvalidOperationException("Invalid criterion.");
}

Yup ใช้สิ่งนี้และทำงานตามที่อธิบายไว้
Mvision

1
+1 นี่เป็นกลลวงที่ดี แต่ฉันจะยอมรับคำตอบของsvickสำหรับการตอบคำถามเดิมของฉันให้ดีที่สุด
pswg

10

ฉันจะอ้างอิง Eric Lippert ซึ่งคำตอบนั้นค่อนข้างชัดเจนในเรื่อง:

คำถามที่สมเหตุสมผลคือ "ทำไมจึงไม่ถูกกฎหมาย" คำตอบที่สมเหตุสมผลคือ "ดีทำไมต้องเป็น" คุณสามารถมีหนึ่งในสองวิธี อาจเป็นสิ่งถูกกฎหมาย:

switch(y) 
{ 
    case 1:  int x = 123; ...  break; 
    case 2:  int x = 456; ...  break; 
}

หรือสิ่งนี้ถูกกฎหมาย:

switch(y) 
{
    case 1:  int x = 123; ... break; 
    case 2:  x = 456; ... break; 
}

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

การตัดสินใจครั้งนี้เกิดขึ้นเมื่อวันที่ 7 กรกฎาคม 1999 เพียงสิบปีที่ผ่านมา ความคิดเห็นในบันทึกย่อจากวันนั้นสั้นมากเพียงแค่ระบุ "กล่องสลับไม่ได้สร้างพื้นที่ประกาศของตัวเอง" จากนั้นให้ตัวอย่างรหัสบางอย่างที่แสดงให้เห็นว่าอะไรทำงานได้และอะไรไม่ได้

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

ในระยะสั้นไม่มีเหตุผลที่น่าสนใจโดยเฉพาะอย่างยิ่งในการเลือกทางเดียวหรืออื่น ๆ ; ทั้งสองมีข้อดี ทีมออกแบบภาษาเลือกทางเดียวเพราะต้องเลือก สิ่งที่พวกเขาเลือกนั้นสมเหตุสมผลกับฉัน

ดังนั้นหากคุณไม่ได้เป็นมิตรกับทีมนักพัฒนา C # ในปี 1999 มากกว่า Eric Lippert คุณจะไม่มีวันรู้ถึงเหตุผลที่แน่นอน!


ไม่ได้เป็น downvoter แต่นี่เป็นความคิดเห็นที่ทำกับคำถามเมื่อวานนี้
Jesse C. Slicer

4
ฉันเห็นว่า แต่เนื่องจากผู้แสดงความคิดเห็นไม่ได้โพสต์คำตอบฉันคิดว่ามันเป็นความคิดที่ดีที่จะสร้าง anwser อย่างชัดเจนโดยการอ้างอิงเนื้อหาของลิงก์ และฉันไม่สนใจผู้ลงคะแนนเลย! OP ถามถึงเหตุผลทางประวัติศาสตร์ไม่ใช่คำตอบ "ทำอย่างไร"
Cyril Gandon

5

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

(ตามที่ระบุไว้ในคำตอบอื่นนักพัฒนาของ C # ไม่มีเอกสารเกี่ยวกับวิธีการตัดสินใจเกี่ยวกับ case-scopes ทำ แต่หลักการ unstated สำหรับไวยากรณ์ C # คือว่าถ้าพวกเขามีเหตุผลที่น่าสนใจที่จะทำมันแตกต่างกันพวกเขาคัดลอก Java )

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

(พื้นฐานมากขึ้นไปที่ไม่มีโครงสร้าง - พวกเขาไม่ได้กำหนดหรือคั่นส่วนของรหัสพวกเขาเพียงกำหนดจุดกระโดดดังนั้นป้ายชื่อ goto ไม่สามารถแนะนำขอบเขต)

C # ยังคงมีไวยากรณ์ แต่แนะนำการป้องกันจาก "ตกผ่าน" โดยต้องการออกหลังจากแต่ละกรณี (ไม่ว่าง) ประโยค แต่สิ่งนี้เปลี่ยนวิธีคิดของสวิตช์! กรณีนี้เป็นสาขาอื่นเช่นสาขาใน if-else นี่หมายความว่าเราคาดหวังว่าแต่ละสาขาจะกำหนดขอบเขตของตัวเองเช่นเดียวกับ if-clauses หรือ clauses clause

ดังนั้นในระยะสั้น: คดีแบ่งขอบเขตเดียวกันเพราะนี่คือสิ่งที่อยู่ใน C. แต่ดูเหมือนว่าแปลกและไม่สอดคล้องกันใน C # เพราะเราคิดว่ากรณีเป็นสาขาอื่นมากกว่าเป้าหมาย goto


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

4

{}วิธีที่ง่ายในการมองขอบเขตคือการพิจารณาขอบเขตโดยบล็อก

เนื่องจากswitchไม่มีบล็อกใด ๆ จึงไม่มีขอบเขตที่แตกต่างกัน


3
เกี่ยวกับfor (int i = 0; i < n; i++) Write(i); /* legal */ Write(i); /* illegal */อะไร ไม่มีบล็อก แต่มีขอบเขตแตกต่างกัน
svick

@svick: อย่างที่ฉันพูดเรียบง่ายมีบล็อกที่สร้างขึ้นโดยforคำสั่ง switchไม่ได้สร้างบล็อกสำหรับแต่ละกรณีเพียงที่ระดับบนสุด ความคล้ายคลึงกันคือแต่ละคำสั่งสร้างหนึ่งบล็อก (นับswitchเป็นคำสั่ง)
Guvante

1
ถ้าอย่างนั้นทำไมคุณถึงนับcaseไม่ได้?
svick

1
@svick: เนื่องจากcaseไม่มีบล็อกเว้นแต่คุณจะตัดสินใจเลือกเพิ่มบล็อก forใช้เวลานานสำหรับคำสั่งถัดไปจนกว่าคุณจะเพิ่ม{}ดังนั้นมันจะมีบล็อกเสมอ แต่caseจะคงอยู่จนกว่าจะมีบางสิ่งที่ทำให้คุณออกจากบล็อกจริงswitchคำสั่งนั้น
Guvante
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.