Tránh các 'goto` voodoo?


47

Tôi có một switchcấu trúc có một vài trường hợp để xử lý. Hoạt switchđộng trên một enumvấn đề đặt ra vấn đề về mã trùng lặp thông qua các giá trị kết hợp:

// 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.
}

Hiện tại switchcấu trúc xử lý từng giá trị riêng biệt:

// 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;
     ...
}

Bạn có ý tưởng ở đó. Hiện tại tôi đã chia nó thành một ifcấu trúc xếp chồng để xử lý các kết hợp với một dòng duy nhất:

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

Điều này đặt ra vấn đề đánh giá logic cực kỳ dài, khó hiểu và khó duy trì. Sau khi tái cấu trúc điều này, tôi bắt đầu nghĩ về các lựa chọn thay thế và nghĩ về ý tưởng về một switchcấu trúc với sự sụp đổ giữa các trường hợp.

Tôi phải sử dụng một gototrong trường hợp đó vì C#không cho phép rơi qua. Tuy nhiên, nó ngăn chặn các chuỗi logic dài vô cùng mặc dù nó nhảy xung quanh trong switchcấu trúc, và nó vẫn mang lại sự sao chép mã.

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;
}

Đây vẫn chưa phải là một giải pháp đủ sạch và có một sự kỳ thị liên quan đến gototừ khóa mà tôi muốn tránh. Tôi chắc chắn phải có một cách tốt hơn để làm sạch điều này.


Câu hỏi của tôi

Có cách nào tốt hơn để xử lý trường hợp cụ thể này mà không ảnh hưởng đến khả năng đọc và bảo trì không?


28
có vẻ như bạn muốn có một cuộc tranh luận lớn. Nhưng bạn sẽ cần một ví dụ tốt hơn về trường hợp tốt để sử dụng goto. cờ enum giải quyết gọn gàng cái này, ngay cả khi ví dụ câu lệnh if của bạn không được chấp nhận và nó có vẻ ổn với tôi
Ewan

5
@Ewan Không ai sử dụng goto. Lần cuối bạn xem mã nguồn nhân Linux là khi nào?
John Douma

6
Bạn sử dụng gotokhi cấu trúc cấp cao không tồn tại trong ngôn ngữ của bạn. Đôi khi tôi ước có một fallthru; từ khóa để thoát khỏi việc sử dụng cụ thể đó gotonhưng oh tốt.
Joshua

25
Nói chung, nếu bạn cảm thấy cần phải sử dụng gotongôn ngữ cấp cao như C #, thì có thể bạn đã bỏ qua nhiều lựa chọn thay thế và / hoặc thiết kế khác (và tốt hơn). Rất nản lòng.
code_dredd

11
Sử dụng "trường hợp goto" trong C # khác với sử dụng "goto" tổng quát hơn, bởi vì đó là cách bạn hoàn thành dự phòng rõ ràng.
Hammerite

Câu trả lời:


175

Tôi thấy mã khó đọc với các gototuyên bố. Tôi muốn giới thiệu cấu trúc enumkhác nhau của bạn . Ví dụ: nếu bạn enumlà một bitfield trong đó mỗi bit đại diện cho một trong các lựa chọn, thì nó có thể trông như thế này:

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

Thuộc tính Flags cho trình biên dịch biết rằng bạn đang thiết lập các giá trị không trùng nhau. Mã gọi mã này có thể thiết lập bit thích hợp. Sau đó, bạn có thể làm một cái gì đó như thế này để làm rõ những gì đang xảy ra:

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

Điều này đòi hỏi mã được thiết lập myEnumđể thiết lập các bitfield đúng và được đánh dấu bằng thuộc tính Flags. Nhưng bạn có thể làm điều đó bằng cách thay đổi các giá trị của enum trong ví dụ của bạn thành:

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

Khi bạn viết một số trong biểu mẫu 0bxxxx, bạn chỉ định nó ở dạng nhị phân. Vì vậy, bạn có thể thấy rằng chúng tôi đặt bit 1, 2 hoặc 3 (tốt, về mặt kỹ thuật 0, 1 hoặc 2, nhưng bạn có ý tưởng). Bạn cũng có thể đặt tên cho các kết hợp bằng cách sử dụng bit OR hoặc nếu các kết hợp có thể được đặt thường xuyên cùng nhau.


5
xuất hiện như vậy ! Nhưng nó phải là C # 7.0 trở lên, tôi đoán vậy.
dùng1118321

31
Dù sao, người ta cũng có thể làm điều đó như thế này : public enum ExampleEnum { One = 1 << 0, Two = 1 << 1, Three = 1 << 2, OneAndTwo = One | Two, OneAndThree = One | Three, TwoAndThree = Two | Three };. Không cần nhấn mạnh vào C # 7 +.
Ded repeatator

8
Bạn có thể sử dụng Enum.HasFlag
IMil

13
Các [Flags]thuộc tính không hiệu gì để trình biên dịch. Đây là lý do tại sao bạn vẫn phải tuyên bố rõ ràng các giá trị enum là quyền hạn của 2.
Joe Sewell

19
@UKMonkey Tôi kịch liệt không đồng ý. HasFlaglà một thuật ngữ mô tả và rõ ràng cho hoạt động đang được thực hiện và trừu tượng hóa việc thực hiện từ chức năng. Sử dụng &bởi vì nó phổ biến trên các ngôn ngữ khác không có ý nghĩa hơn so với sử dụng một byteloại thay vì khai báo một enum.
BJ Myers

136

IMO gốc rễ của vấn đề là đoạn mã này thậm chí không tồn tại.

Bạn rõ ràng có ba điều kiện độc lập và ba hành động độc lập phải thực hiện nếu những điều kiện đó là đúng. Vậy tại sao tất cả những gì được đưa vào một đoạn mã cần ba cờ Boolean để nói cho nó biết phải làm gì (dù bạn có làm xáo trộn chúng thành một enum) và sau đó kết hợp ba thứ độc lập? Nguyên tắc trách nhiệm duy nhất dường như có một ngày nghỉ ở đây.

Đặt các cuộc gọi đến ba chức năng trong đó thuộc về (tức là nơi bạn phát hiện ra nhu cầu thực hiện các hành động) và gửi mã trong các ví dụ này vào thùng rác.

Nếu có mười cờ và hành động không phải ba, bạn có mở rộng loại mã này để xử lý 1024 kết hợp khác nhau không? Tôi hy vọng không! Nếu 1024 là quá nhiều, 8 cũng là quá nhiều, vì lý do tương tự.


10
Thực tế, có rất nhiều API được thiết kế tốt có tất cả các loại cờ, tùy chọn và các loại tham số khác. Một số có thể được truyền lại, một số có tác dụng trực tiếp và một số khác có khả năng bị bỏ qua mà không phá vỡ SRP. Dù sao, chức năng thậm chí có thể được giải mã một số đầu vào bên ngoài, ai biết? Ngoài ra, reductio ad absurdum thường dẫn đến kết quả vô lý, đặc biệt là nếu điểm khởi đầu thậm chí không đủ vững chắc.
Ded repeatator

9
Nâng cao câu trả lời này. Nếu bạn chỉ muốn tranh luận về GOTO, đó là một câu hỏi khác với câu hỏi bạn đã hỏi. Dựa trên các bằng chứng được trình bày, bạn có ba yêu cầu rời rạc, không nên tổng hợp. Từ quan điểm SE, tôi gửi rằng alephzero là câu trả lời đúng.

8
@Ded repeatator Một cái nhìn về sự nhầm lẫn này của một số nhưng không phải tất cả các kết hợp 1,2 & 3 cho thấy đây không phải là "API được thiết kế tốt".
dùng949300

4
Rất nhiều điều này. Các câu trả lời khác không thực sự cải thiện những gì trong câu hỏi. Mã này cần viết lại. Không có gì sai khi viết nhiều chức năng hơn.
only_pro

3
Tôi thích câu trả lời này, đặc biệt là "Nếu 1024 quá nhiều, 8 cũng là quá nhiều". Nhưng thực chất nó là "sử dụng đa hình", phải không? Và câu trả lời (yếu hơn) của tôi đang nhận được rất nhiều điều tào lao khi nói điều đó. Tôi có thể thuê người PR của bạn không? :-)
user949300

29

Không bao giờ sử dụng gotos là một trong những khái niệm "dối trá cho trẻ em" của khoa học máy tính. Đó là lời khuyên đúng 99% thời gian và những lần nó không quá hiếm và chuyên biệt đến nỗi mọi người sẽ tốt hơn nếu nó chỉ giải thích cho các lập trình viên mới là "không sử dụng chúng".

Vậy khi nào nên sử dụng chúng? Có một vài tình huống , nhưng kịch bản cơ bản mà bạn dường như đang gặp phải là: khi bạn đang mã hóa một máy trạng thái . Nếu không có biểu thức có cấu trúc và cấu trúc tốt hơn của thuật toán của bạn so với máy trạng thái, thì biểu thức tự nhiên của nó trong mã liên quan đến các nhánh không có cấu trúc và không có nhiều điều có thể được thực hiện về việc không thể tạo ra cấu trúc của máy nhà nước riêng của mình hơn che khuất, chứ không phải là ít.

Người viết trình biên dịch biết điều này, đó là lý do tại sao mã nguồn cho hầu hết các trình biên dịch triển khai trình phân tích cú pháp LALR * có chứa gotos. Tuy nhiên, rất ít người sẽ thực sự viết mã các trình phân tích và phân tích từ vựng của riêng họ.

* - IIRC, có thể triển khai các ngữ pháp LALL hoàn toàn đệ quy mà không cần dùng đến các bảng nhảy hoặc các câu lệnh điều khiển không có cấu trúc khác, vì vậy nếu bạn thực sự chống goto, đây là một cách.


Bây giờ câu hỏi tiếp theo là "Đây có phải là một trong những trường hợp đó không?"

Điều tôi đang nhìn thấy là bạn có ba trạng thái tiếp theo khác nhau tùy thuộc vào việc xử lý trạng thái hiện tại. Vì một trong số chúng ("mặc định") chỉ là một dòng mã, về mặt kỹ thuật, bạn có thể thoát khỏi nó bằng cách xử lý dòng mã đó ở cuối các trạng thái mà nó áp dụng. Điều đó sẽ đưa bạn xuống 2 trạng thái tiếp theo có thể.

Một trong những cái còn lại ("Ba") chỉ được phân nhánh từ một nơi mà tôi có thể thấy. Vì vậy, bạn có thể thoát khỏi nó theo cùng một cách. Bạn sẽ kết thúc với mã trông như thế này:

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;
}

Tuy nhiên, một lần nữa đây là một ví dụ đồ chơi bạn cung cấp. Trong trường hợp "mặc định" có số lượng mã không tầm thường trong đó, "ba" được chuyển sang từ nhiều trạng thái hoặc (quan trọng nhất) khả năng duy trì thêm có thể sẽ thêm hoặc làm phức tạp các trạng thái , bạn thực sự sẽ tốt hơn sử dụng gotos (và thậm chí có thể thoát khỏi cấu trúc trường hợp enum che giấu bản chất máy móc nhà nước, trừ khi có một số lý do chính đáng để nó cần ở lại).


2
@PerpetualJ - Chà, tôi chắc chắn rất vui vì bạn đã tìm thấy thứ gì đó sạch hơn. Nó vẫn có thể thú vị, nếu đó thực sự là tình huống của bạn, hãy thử nó với gotos (không có trường hợp hoặc enum. Chỉ dán nhãn khối và gotos) và xem cách họ so sánh về khả năng đọc. Điều đó đang được nói, tùy chọn "goto" không nhất thiết phải dễ đọc hơn, nhưng đủ dễ đọc hơn để ngăn chặn các đối số và cố gắng "sửa chữa" từ các nhà phát triển khác, vì vậy có vẻ như bạn đã tìm thấy tùy chọn tốt nhất.
TED

4
Tôi không tin rằng bạn sẽ "tốt hơn khi sử dụng gotos" nếu "việc bảo trì thêm có khả năng thêm hoặc làm phức tạp các trạng thái". Bạn có thực sự tuyên bố rằng mã spaghetti dễ bảo trì hơn mã có cấu trúc? Điều này đi ngược lại ~ 40 năm thực hành.
dùng949300

5
@ user949300 - Không thực hành chống lại việc xây dựng các máy trạng thái, nhưng không. Bạn có thể chỉ cần đọc nhận xét trước đây của tôi về câu trả lời này cho một ví dụ thực tế. Nếu bạn cố gắng sử dụng các trường hợp rơi vào trường hợp C, áp đặt cấu trúc nhân tạo (thứ tự tuyến tính của chúng) vào thuật toán máy trạng thái vốn không có hạn chế như vậy, bạn sẽ thấy mình có độ phức tạp tăng dần về mặt hình học mỗi khi bạn phải sắp xếp lại tất cả thứ tự để phù hợp với một nhà nước mới. Nếu bạn chỉ viết mã trạng thái và chuyển tiếp, như thuật toán yêu cầu, điều đó sẽ không xảy ra. Nhà nước mới là tầm thường để thêm.
TED

8
@ user949300 - Một lần nữa, trình biên dịch xây dựng "~ 40 năm thực hành" cho thấy gotos thực sự là cách tốt nhất cho các phần của miền vấn đề đó, đó là lý do tại sao nếu bạn nhìn vào mã nguồn của trình biên dịch, hầu như bạn sẽ luôn tìm thấy gotos.
TED

3
@ user949300 Mã Spaghetti không chỉ xuất hiện khi sử dụng các chức năng hoặc quy ước cụ thể. Nó có thể xuất hiện trong bất kỳ ngôn ngữ, chức năng hoặc quy ước bất cứ lúc nào. Nguyên nhân là do sử dụng ngôn ngữ, chức năng hoặc quy ước đã nói trong một ứng dụng mà nó không dành cho và khiến nó bị cong về phía sau để kéo nó đi. Luôn luôn sử dụng công cụ phù hợp cho công việc, và vâng, đôi khi điều đó có nghĩa là sử dụng goto.
Abion47

26

Câu trả lời tốt nhất là sử dụng đa hình .

Một câu trả lời khác, IMO, làm cho công cụ if rõ ràng hơn và ngắn hơn :

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

goto có lẽ là lựa chọn thứ 58 của tôi ở đây ...


5
+1 để đề xuất tính đa hình, mặc dù lý tưởng có thể đơn giản hóa mã máy khách xuống chỉ là "Gọi ();", bằng cách sử dụng không hỏi - để đẩy logic quyết định ra khỏi máy khách đang sử dụng và vào cơ chế và phân cấp lớp.
Erik Eidt

12
Tuyên bố bất cứ điều gì cảnh tượng tốt nhất không nhìn thấy chắc chắn là rất dũng cảm. Nhưng không có thêm thông tin, tôi đề nghị một chút hoài nghi và giữ một tâm trí cởi mở. Có lẽ nó bị nổ tung tổ hợp?
Ded repeatator

Ồ, tôi nghĩ rằng đây là lần đầu tiên tôi bị hạ thấp vì đề xuất đa hình. :-)
user949300

25
Một số người, khi gặp vấn đề, nói "Tôi sẽ chỉ sử dụng đa hình". Bây giờ họ có hai vấn đề, và đa hình.
Cuộc đua nhẹ nhàng với Monica

8
Tôi thực sự đã làm luận án thạc sĩ của tôi về việc sử dụng đa hình thời gian chạy để loại bỏ các gotos trong các máy trạng thái của trình biên dịch. Điều này là hoàn toàn có thể làm được, nhưng tôi đã thấy trong thực tế nó không đáng để nỗ lực trong hầu hết các trường hợp. Nó đòi hỏi rất nhiều mã cài đặt OO, tất cả trong số đó tất nhiên có thể kết thúc với các lỗi (và câu trả lời này bỏ qua).
TED

10

Tại sao không phải là điều này:

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];
}

OK, tôi đồng ý, đây là hackish (Tôi không phải là nhà phát triển C # btw, vì vậy xin lỗi cho mã), nhưng từ quan điểm hiệu quả thì điều này là gì? Sử dụng enums làm chỉ số mảng là C # hợp lệ.


3
Đúng. Một ví dụ khác về việc thay thế mã bằng dữ liệu (thường được mong muốn, về tốc độ, cấu trúc và khả năng bảo trì).
Peter - Tái lập Monica

2
Đó là một ý tưởng tuyệt vời cho phiên bản gần đây nhất của câu hỏi, không còn nghi ngờ gì nữa. Và nó có thể được sử dụng để đi từ enum đầu tiên (một phạm vi giá trị dày đặc) đến các cờ của các hành động độc lập ít nhiều phải được thực hiện nói chung.
Ded repeatator

Tôi không có quyền truy cập vào trình biên dịch C #, nhưng có lẽ mã có thể được làm cho an toàn hơn bằng cách sử dụng loại mã này : int[ExempleEnum.length] COUNTS = { 1, 3, 4, 2, 5, 3 };?
Laurent Grégoire

7

Nếu bạn không thể hoặc không muốn sử dụng cờ, hãy sử dụng hàm đệ quy đuôi. Trong chế độ phát hành 64 bit, trình biên dịch sẽ tạo mã rất giống với gotocâu lệnh của bạn . Bạn không cần phải đối phó với nó.

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;
   }
}

Đó là một giải pháp độc đáo! +1 Tôi không nghĩ mình cần làm theo cách này, nhưng tuyệt vời cho những độc giả tương lai!
PerpetualJ

2

Các giải pháp được chấp nhận là tốt và là một giải pháp cụ thể cho vấn đề của bạn. Tuy nhiên, tôi muốn đưa ra một giải pháp thay thế, trừu tượng hơn.

Theo kinh nghiệm của tôi, việc sử dụng enum để xác định luồng logic là mùi mã vì nó thường là dấu hiệu của thiết kế lớp kém.

Tôi đã gặp một ví dụ thực tế về điều này xảy ra trong mã mà tôi đã làm việc vào năm ngoái. Nhà phát triển ban đầu đã tạo ra một lớp duy nhất thực hiện cả nhập và xuất logic, và chuyển đổi giữa hai lớp dựa trên một enum. Bây giờ mã tương tự và có một số mã trùng lặp, nhưng nó đủ khác nhau để làm như vậy làm cho mã khó đọc hơn và hầu như không thể kiểm tra. Cuối cùng tôi đã tái cấu trúc nó thành hai lớp riêng biệt, đơn giản hóa cả hai và thực sự cho phép tôi phát hiện và loại bỏ một số lỗi không được báo cáo.

Một lần nữa, tôi phải nói rằng việc sử dụng enums để điều khiển luồng logic thường là một vấn đề thiết kế. Trong trường hợp chung, Enums nên được sử dụng chủ yếu để cung cấp các giá trị an toàn, thân thiện với người tiêu dùng, trong đó các giá trị có thể được xác định rõ ràng. Chúng được sử dụng tốt hơn như một thuộc tính (ví dụ: làm ID cột trong bảng) hơn là cơ chế điều khiển logic.

Hãy xem xét vấn đề được trình bày trong câu hỏi. Tôi thực sự không biết bối cảnh ở đây, hoặc những gì enum này đại diện. Có phải là vẽ thẻ? Vẽ tranh? Vẽ máu? Là thứ tự quan trọng? Tôi cũng không biết hiệu suất quan trọng như thế nào. Nếu hiệu suất hoặc bộ nhớ là quan trọng, thì giải pháp này có thể sẽ không phải là giải pháp bạn muốn.

Trong mọi trường hợp, hãy xem xét enum:

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

Những gì chúng ta có ở đây là một số giá trị enum khác nhau đại diện cho các khái niệm kinh doanh khác nhau.

Những gì chúng ta có thể sử dụng thay vào đó là trừu tượng để đơn giản hóa mọi thứ.

Chúng ta hãy xem xét giao diện sau:

public interface IExample
{
  void Draw();
}

Sau đó chúng ta có thể thực hiện điều này như một lớp trừu tượng:

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

Chúng ta có thể có một lớp cụ thể để biểu diễn Vẽ một, hai và ba (vì mục đích tranh luận có logic khác nhau). Chúng có khả năng sử dụng lớp cơ sở được định nghĩa ở trên, nhưng tôi cho rằng khái niệm DrawOne khác với khái niệm được biểu thị bởi 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
  }
}

Và bây giờ chúng ta có ba lớp riêng biệt có thể được tạo để cung cấp logic cho các lớp khác.

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

Cách tiếp cận này dài dòng hơn nhiều. Nhưng nó có lợi thế.

Hãy xem xét các lớp sau, có chứa một lỗi:

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();
  }
}

Làm thế nào đơn giản hơn nhiều để phát hiện cuộc gọi drawThree.Draw () bị thiếu? Và nếu thứ tự là quan trọng, thứ tự của các cuộc gọi rút thăm cũng rất dễ dàng để xem và làm theo.

Nhược điểm của phương pháp này:

  • Mỗi một trong tám tùy chọn được trình bày yêu cầu một lớp riêng
  • Điều này sẽ sử dụng nhiều bộ nhớ hơn
  • Điều này sẽ làm cho mã của bạn lớn hơn bề ngoài
  • Đôi khi cách tiếp cận này là không thể, mặc dù một số biến thể trên nó có thể là

Ưu điểm của phương pháp này:

  • Mỗi lớp này là hoàn toàn có thể kiểm tra được; bởi vì
  • Độ phức tạp chu kỳ của các phương thức vẽ là thấp (và theo lý thuyết tôi có thể giả định các lớp DrawOne, DrawTwo hoặc DrawThree nếu cần thiết)
  • Các phương thức vẽ có thể hiểu được - một nhà phát triển không phải buộc bộ não của họ vào các nút thắt để tìm ra phương pháp làm gì
  • Lỗi rất dễ phát hiện và khó viết
  • Các lớp kết hợp thành các lớp cấp cao hơn, có nghĩa là việc xác định lớp ThreeThreeThree rất dễ thực hiện

Xem xét cách tiếp cận này (hoặc tương tự) bất cứ khi nào bạn cảm thấy cần phải có bất kỳ mã điều khiển logic phức tạp nào được viết vào các câu lệnh tình huống. Tương lai bạn sẽ hạnh phúc bạn đã làm.


Đây là một câu trả lời bằng văn bản rất tốt và tôi hoàn toàn đồng ý với cách tiếp cận. Trong trường hợp cụ thể của tôi, một cái gì đó dài dòng này sẽ quá mức đến mức vô hạn khi xem xét mã tầm thường đằng sau mỗi mã. Để đặt nó trong phối cảnh, hãy tưởng tượng mã chỉ đơn giản là thực hiện Console.WriteLine (n). Đó thực tế là tương đương với những gì mã tôi đang làm việc đang làm. Trong 90% tất cả các trường hợp khác, giải pháp của bạn chắc chắn là câu trả lời tốt nhất khi theo dõi OOP.
PerpetualJ

Vâng công bằng, cách tiếp cận này không hữu ích trong mọi tình huống. Tôi muốn đặt nó ở đây bởi vì thông thường các nhà phát triển sẽ được sửa chữa theo một cách tiếp cận cụ thể để giải quyết vấn đề và đôi khi phải trả tiền để lùi lại và tìm kiếm giải pháp thay thế.
Stephen

Tôi hoàn toàn đồng ý với điều đó, đó thực sự là lý do tôi kết thúc ở đây vì tôi đang cố gắng giải quyết vấn đề về khả năng mở rộng với điều mà một nhà phát triển khác đã viết theo thói quen.
PerpetualJ

0

Nếu bạn có ý định sử dụng một công tắc ở đây, mã của bạn sẽ thực sự nhanh hơn nếu bạn xử lý riêng từng trường hợp

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;
}

chỉ một thao tác số học duy nhất được thực hiện trong mỗi trường hợp

cũng như những người khác đã tuyên bố nếu bạn đang cân nhắc sử dụng gotos, có lẽ bạn nên suy nghĩ lại về thuật toán của mình (mặc dù tôi sẽ thừa nhận rằng việc thiếu trường hợp rơi của C # mặc dù có thể là một lý do để sử dụng goto). Xem bài viết nổi tiếng của Edgar Dijkstra "Đi đến tuyên bố được coi là có hại"


3
Tôi sẽ nói rằng đây là một ý tưởng tuyệt vời cho một người phải đối mặt với một vấn đề đơn giản hơn vì vậy cảm ơn bạn đã đăng nó. Trong trường hợp của tôi nó không đơn giản như vậy.
PerpetualJ

Ngoài ra, chúng ta không có chính xác những vấn đề mà Edgar gặp phải vào thời điểm viết bài báo của mình; ngày nay hầu hết mọi người thậm chí không có lý do chính đáng để ghét goto ngoài việc nó khiến mã khó đọc. Vâng, thành thật mà nói, nếu nó bị lạm dụng thì có, nếu không thì nó bị mắc kẹt với phương thức chứa để bạn thực sự không thể tạo mã spaghetti. Tại sao phải nỗ lực để làm việc xung quanh một tính năng của ngôn ngữ? Tôi sẽ nói rằng goto là cổ xưa và bạn nên nghĩ đến các giải pháp khác trong 99% trường hợp sử dụng; Nhưng nếu bạn cần nó, đó là những gì nó có.
PerpetualJ

Điều này sẽ không có quy mô tốt khi có nhiều booleans.
dùng949300

0

Đối với ví dụ cụ thể của bạn, vì tất cả những gì bạn thực sự muốn từ bảng liệt kê là chỉ báo không / không làm cho mỗi bước, giải pháp viết lại ba ifcâu của bạn là thích hợp hơn switchvà thật tốt khi bạn đưa ra câu trả lời được chấp nhận .

Nhưng nếu bạn có một số logic phức tạp hơn mà không được xử lý rõ ràng, thì tôi vẫn thấy các gotos trong switchcâu lệnh khó hiểu. Tôi thà thấy một cái gì đó như thế này:

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;
}

Điều này không hoàn hảo nhưng tôi nghĩ nó tốt hơn theo cách này so với gotos. Nếu các chuỗi sự kiện quá dài và trùng lặp với nhau đến mức thực sự không có ý nghĩa để đánh vần chuỗi hoàn chỉnh cho từng trường hợp, tôi thích chương trình con hơn là gotođể giảm trùng lặp mã.


0

Tôi không chắc chắn rằng bất cứ ai thực sự có lý do để ghét từ khóa goto những ngày này. Nó chắc chắn là cổ xưa và không cần thiết trong 99% trường hợp sử dụng, nhưng đó là một tính năng của ngôn ngữ vì một lý do.

Lý do để ghét gototừ khóa là mã như

if (someCondition) {
    goto label;
}

string message = "Hello World!";

label:
Console.WriteLine(message);

Úi! Đó rõ ràng là không đi làm. Các messagebiến không được định nghĩa trong con đường mã. Vì vậy, C # sẽ không vượt qua điều đó. Nhưng nó có thể là ẩn. Xem xét

object.setMessage("Hello World!");

label:
object.display();

Và giả sử rằng displaysau đó có WriteLinetuyên bố trong đó.

Loại lỗi này có thể khó tìm vì gotoche khuất đường dẫn mã.

Đây là một ví dụ đơn giản. Giả sử rằng một ví dụ thực tế sẽ không gần như rõ ràng. Có thể có năm mươi dòng mã giữa label:và việc sử dụng message.

Ngôn ngữ có thể giúp khắc phục điều này bằng cách giới hạn cách gotosử dụng, chỉ giảm dần khỏi các khối. Nhưng C # gotokhông bị giới hạn như thế. Nó có thể nhảy qua mã. Hơn nữa, nếu bạn định giới hạn goto, bạn cũng nên đổi tên. Các ngôn ngữ khác sử dụng breakđể giảm dần các khối, với một số (các khối cần thoát) hoặc nhãn (trên một trong các khối).

Khái niệm về gotomột hướng dẫn ngôn ngữ máy cấp thấp. Nhưng toàn bộ lý do tại sao chúng ta có ngôn ngữ cấp cao hơn là do chúng ta bị giới hạn ở mức trừu tượng cấp cao hơn, ví dụ phạm vi biến.

Tất cả những gì đã nói, nếu bạn sử dụng C # gotobên trong một switchcâu lệnh để chuyển từ trường hợp này sang trường hợp khác, thì thật vô hại. Mỗi trường hợp đã là một điểm nhập cảnh. Tôi vẫn nghĩ rằng việc gọi nó gotolà ngớ ngẩn trong tình huống đó, vì nó kết hợp việc sử dụng vô hại này gotovới các hình thức nguy hiểm hơn. Tôi muốn thay vì họ sử dụng một cái gì đó như thế continuecho điều đó. Nhưng vì một số lý do, họ quên hỏi tôi trước khi họ viết ngôn ngữ.


2
Trong C#ngôn ngữ, bạn không thể nhảy qua các khai báo biến. Để chứng minh, hãy khởi chạy một ứng dụng bảng điều khiển và nhập mã bạn đã cung cấp trong bài đăng của mình. Bạn sẽ gặp lỗi trình biên dịch trong nhãn của bạn nói rằng lỗi đó là sử dụng biến cục bộ 'thông báo' không được gán . Trong các ngôn ngữ được phép, đây là một mối quan tâm hợp lệ, nhưng không phải bằng ngôn ngữ C #.
PerpetualJ

0

Khi bạn có nhiều lựa chọn này (và thậm chí nhiều hơn, như bạn nói), thì có thể đó không phải là mã mà là dữ liệu.

Tạo một từ điển ánh xạ các giá trị enum thành các hành động, được biểu thị dưới dạng các hàm hoặc như một kiểu enum đơn giản hơn đại diện cho các hành động. Sau đó, mã của bạn có thể được đưa vào một tra cứu từ điển đơn giản, tiếp theo là gọi giá trị hàm hoặc chuyển đổi trên các lựa chọn đơn giản.


-7

Sử dụng một vòng lặp và sự khác biệt giữa nghỉ và tiếp tục.

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);
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.