หลีกเลี่ยงวูดู `goto 'หรือไม่


47

ฉันมีswitchโครงสร้างที่มีหลายกรณีที่ต้องจัดการ การswitchดำเนินการมากกว่าenumที่ poses ปัญหาของรหัสที่ซ้ำกันผ่านค่ารวม:

// All possible combinations of One - Eight.
public enum ExampleEnum {
    One,
    Two, TwoOne,
    Three, ThreeOne, ThreeTwo, ThreeOneTwo,
    Four, FourOne, FourTwo, FourThree, FourOneTwo, FourOneThree,
          FourTwoThree, FourOneTwoThree
    // ETC.
}

ขณะนี้switchโครงสร้างจัดการแต่ละค่าแยกกัน:

// All possible combinations of One - Eight.
switch (enumValue) {
    case One: DrawOne; break;
    case Two: DrawTwo; break;
    case TwoOne:
        DrawOne;
        DrawTwo;
        break;
     case Three: DrawThree; break;
     ...
}

คุณได้รับความคิดที่นั่น ขณะนี้ฉันได้แยกย่อยเป็นifโครงสร้างแบบซ้อนเพื่อจัดการชุดค่าผสมด้วยบรรทัดเดียวแทน:

// All possible combinations of One - Eight.
if (One || TwoOne || ThreeOne || ThreeOneTwo)
    DrawOne;
if (Two || TwoOne || ThreeTwo || ThreeOneTwo)
    DrawTwo;
if (Three || ThreeOne || ThreeTwo || ThreeOneTwo)
    DrawThree;

สิ่งนี้ทำให้เกิดปัญหาของการประเมินเชิงตรรกะที่ไม่น่าเชื่อมานานซึ่งทำให้สับสนในการอ่านและยากที่จะรักษา หลังจากเปลี่ยนสิ่งนี้ออกมาฉันเริ่มคิดถึงทางเลือกและคิดถึงความคิดของswitchโครงสร้างที่มีการตกลงระหว่างกรณี

ฉันต้องใช้ a gotoในกรณีนั้นเนื่องจากC#ไม่อนุญาตให้ล้มลง อย่างไรก็ตามมันจะป้องกันโซ่ลอจิกที่ยาวอย่างไม่น่าเชื่อแม้ว่ามันจะกระโดดไปมาในswitchโครงสร้าง

switch (enumVal) {
    case ThreeOneTwo: DrawThree; goto case TwoOne;
    case ThreeTwo: DrawThree; goto case Two;
    case ThreeOne: DrawThree; goto default;
    case TwoOne: DrawTwo; goto default;
    case Two: DrawTwo; break;
    default: DrawOne; break;
}

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


คำถามของฉัน

มีวิธีที่ดีกว่าในการจัดการกรณีนี้โดยไม่มีผลต่อการอ่านและการบำรุงรักษาหรือไม่?


28
ดูเหมือนคุณจะต้องมีการโต้วาทีครั้งใหญ่ แต่คุณจะต้องมีตัวอย่างที่ดีกว่าของกรณีที่ดีในการใช้ goto ธง enum เรียบร้อยแก้หนึ่งนี้แม้ว่าถ้าตัวอย่างเช่นคำสั่งของคุณไม่ได้รับการยอมรับและมันก็ดูดีกับฉัน
Ewan

5
@Ewan ไม่มีใครใช้ goto ครั้งสุดท้ายที่คุณดูซอร์สโค้ดเคอร์เนล Linux คืออะไร
John Douma

6
คุณใช้gotoเมื่อไม่มีโครงสร้างระดับสูงในภาษาของคุณ บางครั้งฉันหวังว่าจะมีfallthru; คำหลักเพื่อกำจัดการใช้งานนั้นโดยเฉพาะgotoแต่โอ้ดี
โยชูวา

25
โดยทั่วไปถ้าคุณรู้สึกว่าจำเป็นต้องใช้gotoภาษาในระดับสูงเช่น C # คุณอาจมองข้ามการออกแบบ (และดีกว่า) อื่น ๆ และ / หรือทางเลือกในการนำไปใช้ ท้อแท้อย่างมาก
code_dredd

11
การใช้ "กรณี goto" ใน C # นั้นแตกต่างจากการใช้ "goto" ทั่วไปมากขึ้นเพราะเป็นวิธีการที่คุณเข้าใจผิดอย่างชัดเจน
Hammerite

คำตอบ:


175

ฉันพบรหัสยากที่จะอ่านด้วยgotoคำสั่ง ฉันจะแนะนำโครงสร้างของคุณenumแตกต่าง ตัวอย่างเช่นหากคุณenumเป็นบิตฟิลด์ที่แต่ละบิตแสดงตัวเลือกอย่างใดอย่างหนึ่งอาจเป็นดังนี้:

[Flags]
public enum ExampleEnum {
    One = 0b0001,
    Two = 0b0010,
    Three = 0b0100
};

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

if (myEnum.HasFlag(ExampleEnum.One))
{
    CallOne();
}
if (myEnum.HasFlag(ExampleEnum.Two))
{
    CallTwo();
}
if (myEnum.HasFlag(ExampleEnum.Three))
{
    CallThree();
}

สิ่งนี้ต้องการรหัสที่ตั้งค่าmyEnumเพื่อตั้งค่าบิตฟิลด์อย่างถูกต้องและทำเครื่องหมายด้วยแอตทริบิวต์ Flags แต่คุณสามารถทำได้โดยเปลี่ยนค่าของ enums ในตัวอย่างของคุณเป็น:

[Flags]
public enum ExampleEnum {
    One = 0b0001,
    Two = 0b0010,
    Three = 0b0100,
    OneAndTwo = One | Two,
    OneAndThree = One | Three,
    TwoAndThree = Two | Three
};

เมื่อคุณเขียนตัวเลขลงในแบบฟอร์ม0bxxxxคุณจะระบุเป็นเลขฐานสอง ดังนั้นคุณจะเห็นได้ว่าเราตั้งค่าบิต 1, 2 หรือ 3 (ก็คือเทคนิค 0, 1 หรือ 2 แต่คุณได้แนวคิด) คุณยังสามารถตั้งชื่อชุดค่าผสมโดยใช้ค่าบิตหรือถ้าชุดค่าผสมอาจถูกตั้งค่าร่วมกันบ่อยครั้ง


5
มันจะปรากฏขึ้น ! แต่ต้องเป็น C # 7.0 หรือใหม่กว่าฉันเดา
user1118321

31
public enum ExampleEnum { One = 1 << 0, Two = 1 << 1, Three = 1 << 2, OneAndTwo = One | Two, OneAndThree = One | Three, TwoAndThree = Two | Three };อย่างไรก็ตามหนึ่งยังสามารถทำเช่นนี้ ไม่จำเป็นต้องยืนยันใน C # 7 +
Deduplicator

8
คุณสามารถใช้Enum.HasFlag
IMil

13
[Flags]แอตทริบิวต์ไม่ได้ส่งสัญญาณอะไรที่จะคอมไพเลอร์ นี่คือเหตุผลที่คุณยังต้องประกาศค่า enum อย่างชัดเจนว่าเป็นพลังของ 2
โจเซเวล

19
@UKMonkey ฉันไม่เห็นด้วยอย่างรุนแรง HasFlagเป็นคำที่สื่อความหมายและชัดเจนสำหรับการดำเนินการและบทคัดย่อการนำไปปฏิบัติจากการใช้งาน ใช้&เพราะมันเป็นเรื่องธรรมดาทั่วภาษาอื่น ๆ ทำให้รู้สึกไม่มีอะไรมากไปกว่าการใช้ชนิดแทนการประกาศตนเป็นbyte enum
BJ Myers

136

IMO รากเหง้าของปัญหาคือรหัสชิ้นนี้ไม่ควรมีอยู่

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

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

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


10
ตามจริงแล้วมี API ที่ออกแบบมาอย่างดีจำนวนมากซึ่งมีแฟล็กทุกประเภทตัวเลือกและพารามิเตอร์อื่น ๆ บางคนอาจถูกส่งต่อไปบางคนมีผลกระทบโดยตรงและอื่น ๆ อาจถูกละเว้นโดยไม่ทำลาย SRP อย่างไรก็ตามฟังก์ชั่นยังสามารถถอดรหัสอินพุตภายนอกใครจะรู้? นอกจากนี้ reductio ad absurdum มักจะนำไปสู่ผลลัพธ์ที่ไร้สาระโดยเฉพาะอย่างยิ่งหากจุดเริ่มต้นไม่ได้เป็นสิ่งที่แข็งแกร่ง
Deduplicator

9
โหวตคำตอบนี้ขึ้น หากคุณเพียงต้องการอภิปราย GOTO นั่นเป็นคำถามที่แตกต่างจากคำถามที่คุณถาม จากหลักฐานที่นำเสนอคุณมีข้อเรียกร้องไม่เข้าร่วมสามข้อซึ่งไม่ควรสรุปรวม จากมุมมองของ SE ฉันส่งของ alephzero ว่าเป็นคำตอบที่ถูกต้อง

8
@Dupuplicator หนึ่งดู mishmash ของบางส่วน แต่ไม่รวมทั้งหมดของ 1,2 และ 3 แสดงให้เห็นว่านี่ไม่ใช่ "API ที่ออกแบบมาอย่างดี"
949300

4
มากขนาดนี้ คำตอบอื่น ๆ ไม่ได้ปรับปรุงสิ่งที่อยู่ในคำถาม รหัสนี้ต้องการการเขียนใหม่ ไม่มีอะไรผิดปกติกับการเขียนฟังก์ชั่นเพิ่มเติม
only_pro

3
ฉันรักคำตอบนี้โดยเฉพาะ "ถ้า 1024 มากเกินไป 8 ก็มากเกินไป" แต่มันก็คือ "ใช้ความหลากหลาย" ใช่ไหม? และคำตอบของฉันก็อ่อนแอลงเรื่อย ๆ ฉันสามารถจ้างคนประชาสัมพันธ์ของคุณได้หรือไม่? :-)
user949300

29

อย่าใช้ gotosเป็นหนึ่งในแนวคิด"โกหกต่อเด็ก"ของวิทยาศาสตร์คอมพิวเตอร์ มันเป็นคำแนะนำที่ถูกต้อง 99% ของเวลาและเวลาที่มันไม่ได้หาได้ยากและมีความเชี่ยวชาญที่จะดีกว่าสำหรับทุกคนถ้ามันเพิ่งอธิบายให้ coders ใหม่ว่า "อย่าใช้มัน"

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

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

* - IIRC เป็นไปได้ที่จะใช้ไวยากรณ์ LALL ซ้ำทั้งหมดโดยไม่ต้องใช้วิธีกระโดดตารางหรือคำสั่งควบคุมที่ไม่มีโครงสร้างอื่น ๆ ดังนั้นหากคุณต่อต้านการข้ามไปอย่างแท้จริงนี่เป็นวิธีหนึ่ง


ตอนนี้คำถามต่อไปคือ "นี่เป็นตัวอย่างหนึ่งในกรณีเหล่านี้หรือไม่"

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

หนึ่งในสิ่งที่เหลืออยู่ ("สาม") แยกจากที่เดียวที่ฉันเห็น ดังนั้นคุณสามารถกำจัดมันด้วยวิธีเดียวกัน คุณจะได้รหัสที่มีลักษณะดังนี้:

switch (exampleValue) {
    case OneAndTwo: i += 3 break;
    case OneAndThree: i += 4 break;
    case Two: i += 2 break;
    case TwoAndThree: i += 5 break;
    case Three: i += 3 break;
    default: i++ break;
}

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


2
@PerpetualJ - ดีฉันดีใจที่คุณพบสิ่งที่สะอาด มันอาจจะยังน่าสนใจถ้าเป็นสถานการณ์ของคุณลองกับ gotos (ไม่มีกรณีหรือ enum เพียงแค่ติดป้ายบล็อกและ gotos) และดูว่าพวกเขาเปรียบเทียบอย่างไรในการอ่าน ดังที่กล่าวไว้ตัวเลือก "goto" ไม่เพียง แต่จะสามารถอ่านได้มากขึ้น แต่เพียงพอที่จะอ่านได้เพื่อป้องกันการโต้แย้งและการพยายาม "แก้ไข" จากแฮมนักพัฒนาคนอื่น ๆ ดังนั้นจึงดูเหมือนว่าคุณจะพบตัวเลือกที่ดีที่สุด
TED

4
ฉันไม่เชื่อว่าคุณจะ "ดีกว่าการใช้ gotos" ถ้า "การบำรุงรักษาเพิ่มเติมมีแนวโน้มที่จะเพิ่มหรือทำให้รัฐยุ่งยาก" คุณอ้างว่ารหัสสปาเก็ตตี้นั้นง่ายต่อการดูแลมากกว่าโค้ดที่มีโครงสร้างหรือไม่? สิ่งนี้ขัดต่อการฝึกฝนประมาณ 40 ปี
user949300

5
@ user949300 - ไม่ได้ฝึกฝนการสร้างเครื่องจักรสถานะมันไม่ได้ คุณสามารถอ่านความคิดเห็นก่อนหน้าของฉันในคำตอบนี้เพื่อเป็นตัวอย่างในโลกแห่งความจริง หากคุณพยายามใช้กรณีตัวพิมพ์ใหญ่ C ซึ่งกำหนดโครงสร้างเทียม (ลำดับเชิงเส้นของพวกเขา) บนอัลกอริทึมของเครื่องจักรที่ไม่มีข้อ จำกัด ดังกล่าวโดยเนื้อแท้คุณจะพบว่าตัวเองมีความซับซ้อนเพิ่มขึ้นทุกครั้งที่คุณต้องจัดเรียง orderings เพื่อรองรับสถานะใหม่ หากคุณเพียงแค่ระบุรหัสและการเปลี่ยนเช่นขั้นตอนวิธีที่ต้องการนั่นจะไม่เกิดขึ้น สถานะใหม่นั้นเพิ่มเล็กน้อย
TED

8
@ user949300 - อีกครั้ง "~ 40 ปีแห่งการฝึกฝน" คอมไพเลอร์การสร้างแสดงว่า gotos เป็นวิธีที่ดีที่สุดสำหรับส่วนต่าง ๆ ของโดเมนปัญหานั้นซึ่งเป็นเหตุผลว่าทำไมถ้าคุณดูซอร์สโค้ดคอมไพเลอร์คุณจะพบ gotos ได้เกือบทุกครั้ง
TED

3
@ user949300 รหัสสปาเก็ตตี้ไม่เพียง แต่ปรากฎโดยเนื้อแท้เมื่อใช้ฟังก์ชั่นหรืออนุสัญญา มันสามารถปรากฏในภาษาใด ๆ ฟังก์ชั่นหรือการประชุมในเวลาใดก็ได้ สาเหตุคือการใช้ภาษาฟังก์ชั่นหรือการประชุมดังกล่าวในแอปพลิเคชันที่ไม่ได้มีไว้สำหรับใช้และทำให้โค้งไปด้านหลังเพื่อดึงออก gotoมักจะใช้เครื่องมือที่เหมาะสมสำหรับงานและใช่ว่าบางครั้งหมายถึงการใช้
Abion47

26

คำตอบที่ดีที่สุดคือการใช้ความแตกต่าง

อีกคำตอบหนึ่งซึ่ง IMO ทำให้สิ่งที่ชัดเจนยิ่งขึ้นและสั้นลง :

if (One || OneAndTwo || OneAndThree)
  CallOne();
if (Two || OneAndTwo || TwoAndThree)
  CallTwo();
if (Three || OneAndThree || TwoAndThree)
  CallThree();

goto อาจเป็นทางเลือกที่ 58 ของฉันที่นี่ ...


5
+1 สำหรับการแนะนำ polymorphism แม้ว่าจะเป็นการง่ายที่โค้ดของไคลเอ็นต์จะลดลงเหลือเพียง "Call ();" โดยใช้ tell Don't ask - เพื่อผลักตรรกะการตัดสินใจออกจากไคลเอ็นต์ที่ใช้ไป
Erik Eidt

12
การประกาศสิ่งที่มองเห็นได้ดีที่สุดนั้นเป็นสิ่งที่กล้าหาญมาก แต่ถ้าไม่มีข้อมูลเพิ่มเติมฉันขอแนะนำให้สงสัยเล็กน้อยและเปิดใจให้กว้าง บางทีมันอาจจะได้รับผลกระทบจากการระเบิดแบบ combinatorial?
Deduplicator

ว้าวฉันคิดว่านี่เป็นครั้งแรกที่ฉันถูกลดระดับลงเพราะเสนอแนะความหลากหลาย :-)
user949300

25
บางคนเมื่อประสบกับปัญหาพูดว่า "ฉันจะใช้ความหลากหลาย" ตอนนี้พวกเขามีสองปัญหาและความแตกต่าง
การแข่งขัน Lightness กับโมนิก้า

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

10

ทำไมไม่ทำอย่างนี้:

public enum ExampleEnum {
    One = 0, // Why not?
    OneAndTwo,
    OneAndThree,
    Two,
    TwoAndThree,
    Three
}
int[] COUNTS = { 1, 3, 4, 2, 5, 3 }; // Whatever

int ComputeExampleValue(int i, ExampleEnum exampleValue) {
    return i + COUNTS[(int)exampleValue];
}

ตกลงฉันเห็นด้วยนี่เป็นเรื่องแฮ็ค (ฉันไม่ใช่นักพัฒนา C # btw ขอโทษสำหรับรหัส) แต่จากมุมมองของประสิทธิภาพนี้ต้องเป็นอย่างไร ใช้ enums เป็นดัชนีอาเรย์ที่ถูกต้อง C #


3
ใช่. อีกตัวอย่างของการแทนที่รหัสด้วยข้อมูล (ซึ่งมักเป็นที่ต้องการสำหรับความเร็วโครงสร้างและการบำรุงรักษา)
Peter - Reinstate Monica

2
นั่นเป็นแนวคิดที่ยอดเยี่ยมสำหรับคำถามรุ่นล่าสุดไม่ต้องสงสัยเลย และมันสามารถใช้ในการเปลี่ยนจากค่า enum แรก (ช่วงของค่าที่หนาแน่น) ไปจนถึงค่าสถานะของการกระทำอิสระที่มากหรือน้อยซึ่งต้องทำโดยทั่วไป
Deduplicator

ผมไม่ได้เข้าสู่เรียบเรียง C # แต่บางทีรหัสที่สามารถทำที่ปลอดภัยกว่าการใช้รหัสชนิดนี้int[ExempleEnum.length] COUNTS = { 1, 3, 4, 2, 5, 3 };?
Laurent Grégoire

7

หากคุณไม่สามารถหรือไม่ต้องการใช้ธงใช้ฟังก์ชันเรียกซ้ำหาง ในโหมดการเปิดตัว 64 บิตคอมไพเลอร์จะสร้างโค้ดที่คล้ายกับgotoคำสั่งของคุณ คุณไม่ต้องจัดการกับมัน

int ComputeExampleValue(int i, ExampleEnum exampleValue) {
    switch (exampleValue) {
        case One: return i + 1;
        case OneAndTwo: return ComputeExampleValue(i + 2, ExampleEnum.One);
        case OneAndThree: return ComputeExampleValue(i + 3, ExampleEnum.One);
        case Two: return i + 2;
        case TwoAndThree: return ComputeExampleValue(i + 2, ExampleEnum.Three);
        case Three: return i + 3;
   }
}

นั่นเป็นทางออกที่ไม่เหมือนใคร! +1 ฉันไม่คิดว่าฉันต้องทำแบบนี้ แต่เหมาะสำหรับผู้อ่านในอนาคต!
ตลอด

2

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

จากประสบการณ์ของฉันการใช้ enums เพื่อกำหนดการไหลของลอจิกเป็นกลิ่นรหัสเนื่องจากมักจะเป็นสัญญาณของการออกแบบระดับที่ไม่ดี

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

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

ลองพิจารณาปัญหาที่นำเสนอในคำถาม ฉันไม่รู้บริบทจริง ๆ ที่นี่หรือสิ่งที่ enum นำเสนอ มันวาดการ์ดหรือไม่ กำลังวาดภาพ? วาดเลือดหรือไม่ คำสั่งซื้อสำคัญหรือไม่ ฉันไม่ทราบด้วยว่าประสิทธิภาพสำคัญเพียงใด หากประสิทธิภาพหรือหน่วยความจำมีความสำคัญโซลูชันนี้อาจไม่ตรงกับที่คุณต้องการ

ไม่ว่าในกรณีใดลองพิจารณา enum:

// All possible combinations of One - Eight.
public enum ExampleEnum {
    One,
    Two,
    TwoOne,
    Three,
    ThreeOne,
    ThreeTwo,
    ThreeOneTwo
}

สิ่งที่เรามีที่นี่มีค่า enum ที่แตกต่างกันซึ่งแสดงถึงแนวคิดทางธุรกิจที่แตกต่างกัน

สิ่งที่เราสามารถใช้แทน abstractions เพื่อทำให้สิ่งต่าง ๆ ง่ายขึ้น

ขอให้เราพิจารณาอินเทอร์เฟซต่อไปนี้:

public interface IExample
{
  void Draw();
}

จากนั้นเราสามารถใช้สิ่งนี้เป็นคลาสนามธรรม:

public abstract class ExampleClassBase : IExample
{
  public abstract void Draw();
  // other common functionality defined here
}

เราสามารถมีคลาสที่เป็นรูปธรรมเพื่อเป็นตัวแทนของ Drawing One, สองและสาม (ซึ่งสำหรับเหตุผลของการโต้แย้งมีตรรกะที่แตกต่างกัน) สิ่งเหล่านี้อาจใช้คลาสพื้นฐานที่กำหนดไว้ด้านบน แต่ฉันสมมติว่าแนวคิด DrawOne แตกต่างจากแนวคิดที่แสดงโดย enum:

public class DrawOne
{
  public void Draw()
  {
    // Drawing logic here
  }
}

public class DrawTwo
{
  public void Draw()
  {
    // Drawing two logic here
  }
}

public class DrawThree
{
  public void Draw()
  {
    // Drawing three logic here
  }
}

และตอนนี้เรามีคลาสที่แยกกันสามคลาสซึ่งอาจประกอบด้วยตรรกะเพื่อให้คลาสอื่น

public class One : ExampleClassBase
{
  private DrawOne drawOne;

  public One(DrawOne drawOne)
  {
    this.drawOne = drawOne;
  }

  public void Draw()
  {
    this.drawOne.Draw();
  }
}

public class TwoOne : ExampleClassBase
{
  private DrawOne drawOne;
  private DrawTwo drawTwo;

  public One(DrawOne drawOne, DrawTwo drawTwo)
  {
    this.drawOne = drawOne;
    this.drawTwo = drawTwo;
  }

  public void Draw()
  {
    this.drawOne.Draw();
    this.drawTwo.Draw();
  }
}

// the other six classes here

วิธีนี้เป็นวิธีที่ละเอียดกว่ามาก แต่มันมีข้อดี

พิจารณาคลาสต่อไปนี้ซึ่งมีข้อบกพร่อง:

public class ThreeTwoOne : ExampleClassBase
{
  private DrawOne drawOne;
  private DrawTwo drawTwo;
  private DrawThree drawThree;

  public One(DrawOne drawOne, DrawTwo drawTwo, DrawThree drawThree)
  {
    this.drawOne = drawOne;
    this.drawTwo = drawTwo;
    this.drawThree = drawThree;
  }

  public void Draw()
  {
    this.drawOne.Draw();
    this.drawTwo.Draw();
  }
}

มันง่ายกว่าที่จะเห็นการเรียก drawThree.Draw () ที่หายไป? และถ้าลำดับเป็นสิ่งสำคัญลำดับของการเรียกเสมอก็ง่ายต่อการดูและติดตาม

ข้อเสียของวิธีนี้:

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

ข้อดีของวิธีนี้:

  • แต่ละคลาสเหล่านี้สามารถทดสอบได้อย่างสมบูรณ์ เพราะ
  • ความซับซ้อนของวงจรวิธีการวาดอยู่ในระดับต่ำ (และในทางทฤษฎีแล้วฉันสามารถล้อเลียนชั้นเรียน DrawOne, DrawTwo หรือ DrawThree ได้ถ้าจำเป็น)
  • วิธีการวาดเป็นที่เข้าใจได้ - นักพัฒนาไม่จำเป็นต้องผูกสมองของพวกเขาเป็นปมเพื่อหาวิธีที่จะทำ
  • จุดบกพร่องนั้นง่ายต่อการสังเกตและเขียนยาก
  • คลาสที่ประกอบเป็นคลาสระดับสูงมากขึ้นซึ่งหมายความว่าการกำหนดคลาส ThreeThreeThree ทำได้ง่าย

พิจารณาวิธีการนี้ (หรือคล้ายกัน) เมื่อใดก็ตามที่คุณรู้สึกว่าจำเป็นต้องมีรหัสควบคุมตรรกะที่ซับซ้อนใด ๆ ที่เขียนลงในคำสั่งกรณี ในอนาคตคุณจะมีความสุขที่คุณทำ


นี่เป็นคำตอบที่เขียนได้ดีมากและฉันเห็นด้วยกับวิธีการทั้งหมด ในกรณีของฉันบางสิ่งบางอย่าง verbose นี้จะ overkill ในระดับอนันต์พิจารณารหัสเล็กน้อยหลังแต่ละ หากต้องการวางไว้ในมุมมองให้จินตนาการว่าโค้ดนั้นกำลังใช้งาน Console.WriteLine (n) นั่นคือสิ่งที่เทียบเท่ากับสิ่งที่ฉันกำลังทำกับรหัส ใน 90% ของกรณีอื่น ๆ ทางออกของคุณคือคำตอบที่ดีที่สุดเมื่อติดตาม OOP
ตลอด

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

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

0

หากคุณต้องการใช้สวิตช์ที่นี่รหัสของคุณจะเร็วขึ้นหากคุณจัดการแต่ละกรณีแยกกัน

switch(exampleValue)
{
    case One:
        i++;
        break;
    case Two:
        i += 2;
        break;
    case OneAndTwo:
    case Three:
        i+=3;
        break;
    case OneAndThree:
        i+=4;
        break;
    case TwoAndThree:
        i+=5;
        break;
}

มีการดำเนินการทางคณิตศาสตร์เพียงอย่างเดียวในแต่ละกรณี

เช่นเดียวกับที่คนอื่น ๆ ได้กล่าวไว้หากคุณกำลังพิจารณาใช้ gotos คุณควรคิดใหม่อัลกอริทึมของคุณ (แม้ว่าฉันจะยอมรับว่ากรณีของ C # นั้นล้มเหลว แต่อาจเป็นเหตุผลที่ใช้ goto) ดูบทความที่มีชื่อเสียงของ Edgar Dijkstra "Go To Statement พิจารณาว่าเป็นอันตราย"


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

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

สิ่งนี้จะไม่ขยายได้ดีเมื่อมีบูลีนมากมาย
user949300

0

สำหรับตัวอย่างเฉพาะของคุณเนื่องจากทั้งหมดที่คุณต้องการจากการแจงนับเป็นตัวบ่งชี้ที่ต้องทำ / ไม่ต้องทำสำหรับแต่ละขั้นตอนวิธีการแก้ปัญหาที่เขียนใหม่สามifคำสั่งของคุณจะดีกว่าswitchและเป็นสิ่งที่ดีที่คุณทำให้เป็นคำตอบที่ยอมรับ .

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

switch (enumVal) {
    case ThreeOneTwo: DrawThree; DrawTwo; DrawOne; break;
    case ThreeTwo:    DrawThree; DrawTwo; break;
    case ThreeOne:    DrawThree; DrawOne; break;
    case TwoOne:      DrawTwo; DrawOne; break;
    case Two:         DrawTwo; break;
    default:          DrawOne; break;
}

นี้ไม่ได้สมบูรณ์แบบ แต่ฉันคิดว่ามันเป็นทางที่ดีกว่านี้กับgotos หากลำดับเหตุการณ์เกิดขึ้นนานและซ้ำซ้อนกันมากจนไม่เหมาะสมที่จะสะกดลำดับที่สมบูรณ์สำหรับแต่ละกรณีฉันต้องการรูทีนย่อยแทนgotoการลดรหัสซ้ำ


0

ฉันไม่แน่ใจว่าทุกคนมีเหตุผลที่จะเกลียดคำหลักข้ามไปเมื่อเร็ว ๆ นี้ เป็นของโบราณและไม่จำเป็นใน 99% ของกรณีการใช้งาน แต่มันเป็นคุณสมบัติของภาษาด้วยเหตุผล

เหตุผลที่เกลียดชังgotoคำหลักคือรหัสเช่น

if (someCondition) {
    goto label;
}

string message = "Hello World!";

label:
Console.WriteLine(message);

อ๊ะ! เห็นได้ชัดว่าไม่ไปทำงาน messageตัวแปรไม่ได้กำหนดไว้ในเส้นทางรหัสที่ ดังนั้น C # จะไม่ผ่านสิ่งนั้น แต่มันอาจจะเป็นนัย พิจารณา

object.setMessage("Hello World!");

label:
object.display();

และสมมติว่าdisplayมีWriteLineคำสั่งอยู่ในนั้น

ข้อผิดพลาดประเภทนี้หายากเพราะgotoบดบังเส้นทางโค้ด

นี่คือตัวอย่างที่ง่าย สมมติว่าตัวอย่างจริงอาจไม่ชัดเจน อาจจะมีห้าสิบบรรทัดของรหัสระหว่างและการใช้งานของ label:message

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

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

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


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

0

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

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


-7

ใช้การวนซ้ำและความแตกต่างระหว่างตัวแบ่งและต่อ

do {
  switch (exampleValue) {
    case OneAndTwo: i += 2; break;
    case OneAndThree: i += 3; break;
    case Two: i += 2; continue;
    case TwoAndThree: i += 2;
    // dropthrough
    case Three: i += 3; continue;
    default: break;
  }
  i++;
} while(0);
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.