Tại sao câu lệnh chuyển đổi được thiết kế để cần nghỉ ngơi?


139

Đưa ra một tuyên bố chuyển đổi đơn giản

switch (int)
{
    case 1 :
    {
        printf("1\n");
        break;
    }

    case 2 : 
    {
        printf("2\n");
    }

    case 3 : 
    {
        printf("3\n");
    }
}

Việc không có tuyên bố phá vỡ trong trường hợp 2, ngụ ý rằng việc thực thi sẽ tiếp tục bên trong mã cho trường hợp 3. Đây không phải là một tai nạn; nó được thiết kế theo cách đó Tại sao quyết định này được đưa ra? Điều này mang lại lợi ích gì so với việc có một ngữ nghĩa ngắt tự động cho các khối? Lý do là gì?

Câu trả lời:


150

Nhiều câu trả lời dường như tập trung vào khả năng vượt qua là lý do để yêu cầu breaktuyên bố.

Tôi tin rằng đó đơn giản chỉ là một sai lầm, phần lớn là do khi C được thiết kế, gần như không có nhiều kinh nghiệm về cách các cấu trúc này sẽ được sử dụng.

Peter Van der Linden đưa ra trường hợp trong cuốn sách "Expert C Lập trình":

Chúng tôi đã phân tích các nguồn trình biên dịch Sun C để xem tần suất rơi mặc định được sử dụng. Mặt trước của trình biên dịch Sun ANSI C có 244 câu lệnh chuyển đổi, mỗi câu lệnh có trung bình bảy trường hợp. Rơi qua xảy ra chỉ trong 3% của tất cả các trường hợp này.

Nói cách khác, hành vi chuyển đổi bình thường là sai 97% thời gian. Nó không chỉ trong một trình biên dịch - ngược lại, trong đó phân tích được sử dụng trong phân tích này, nó thường dành cho các tình huống xảy ra thường xuyên hơn trong trình biên dịch so với các phần mềm khác, ví dụ, khi biên dịch các toán tử có thể có một hoặc hai toán hạng :

switch (operator->num_of_operands) {
    case 2: process_operand( operator->operand_2);
              /* FALLTHRU */

    case 1: process_operand( operator->operand_1);
    break;
}

Trường hợp rơi vào trường hợp được công nhận rộng rãi như là một khiếm khuyết mà thậm chí còn có một quy ước bình luận đặc biệt, được trình bày ở trên, nói với lint "đây thực sự là một trong 3% trường hợp rơi vào trường hợp mong muốn."

Tôi nghĩ rằng C # nên yêu cầu một câu lệnh nhảy rõ ràng ở cuối mỗi khối trường hợp (trong khi vẫn cho phép nhiều nhãn trường hợp được xếp chồng lên nhau - miễn là chỉ có một khối lệnh duy nhất). Trong C #, bạn vẫn có thể có một trường hợp rơi vào trường hợp khác - bạn chỉ cần làm cho cú ngã thông qua rõ ràng bằng cách chuyển sang trường hợp tiếp theo bằng cách sử dụng a goto.

Thật tệ khi Java không có cơ hội thoát khỏi ngữ nghĩa C.


Thật vậy, tôi nghĩ rằng họ đã đi để đơn giản thực hiện. Một số ngôn ngữ khác hỗ trợ các trường hợp phức tạp hơn (phạm vi, nhiều giá trị, chuỗi ...) với chi phí, có lẽ, về hiệu quả.
PhiLho

Java có lẽ đã không muốn phá vỡ thói quen và lan truyền sự nhầm lẫn. Đối với một hành vi khác nhau, họ sẽ phải sử dụng các ngữ nghĩa khác nhau. Dù sao, các nhà thiết kế Java đã mất một số cơ hội để thoát khỏi C, dù sao đi nữa.
PhiLho

@PhiLho - Tôi nghĩ có lẽ bạn gần với sự thật nhất với "sự đơn giản trong thực hiện".
Michael Burr

Nếu bạn đang sử dụng các bước nhảy rõ ràng thông qua GOTO, thì việc sử dụng một loạt các câu lệnh IF có hiệu quả không?
DevinB

3
thậm chí Pascal thực hiện chuyển đổi của họ mà không nghỉ. Làm thế nào mà trình biên dịch C có thể không nghĩ về nó @@
Chan Le

30

Trong rất nhiều cách, c chỉ là một giao diện sạch cho các thành ngữ lắp ráp tiêu chuẩn. Khi viết điều khiển luồng điều khiển bảng nhảy, lập trình viên có quyền lựa chọn rơi qua hoặc nhảy ra khỏi "cấu trúc điều khiển", và nhảy ra đòi hỏi một hướng dẫn rõ ràng.

Vì vậy, c làm điều tương tự ...


1
Tôi biết rất nhiều người nói vậy, nhưng tôi nghĩ đó không phải là bức tranh hoàn chỉnh; C cũng thường là một giao diện xấu xí để lắp ráp các antipotype. 1. Xấu xí: Báo cáo thay vì biểu thức. "Tuyên bố phản ánh việc sử dụng." Tôn vinh sao chép dán như các cơ chế cho mô đun và tính di động. Loại hệ thống cách quá phức tạp; khuyến khích lỗi. NULL. (Tiếp tục trong bình luận tiếp theo.)
Harrison

2. Antipotype: Không cung cấp các hiệp hội được gắn thẻ "sạch", một trong hai thành ngữ biểu diễn dữ liệu cơ bản của lắp ráp. (Knuth, vol 1, ch 2) Thay vào đó, "các hiệp hội không có thẻ", một hack không thành ngữ. Sự lựa chọn này đã thay đổi cách mọi người nghĩ về dữ liệu trong nhiều thập kỷ. NULchuỗi kết thúc chỉ là về ý tưởng tồi tệ nhất.
Harrison

@HarrisonKlaperman: Không có phương pháp lưu trữ chuỗi nào là hoàn hảo. Phần lớn các vấn đề liên quan đến chuỗi kết thúc null sẽ không xảy ra nếu các thường trình chấp nhận chuỗi cũng chấp nhận tham số độ dài tối đa và sẽ xảy ra với các thường trình lưu trữ chuỗi có độ dài vào bộ đệm có kích thước cố định mà không chấp nhận tham số kích thước bộ đệm . Thiết kế của câu lệnh chuyển đổi trong đó các trường hợp đơn giản là các nhãn có vẻ kỳ lạ đối với mắt hiện đại, nhưng nó không tệ hơn thiết kế của DOvòng lặp Fortran .
supercat

Nếu tôi quyết định viết một bảng nhảy trong cụm, tôi sẽ lấy giá trị trường hợp và biến nó thành bảng con của bảng nhảy, nhảy đến vị trí đó và thực thi mã. Sau đó tôi sẽ không nhảy vào trường hợp tiếp theo. Tôi sẽ nhảy đến một địa chỉ thống nhất là lối ra của tất cả các trường hợp. Ý tưởng rằng tôi sẽ nhảy hoặc rơi vào cơ thể của trường hợp tiếp theo là ngớ ngẩn. Không có trường hợp sử dụng cho mẫu đó.
EvilTeach

2
Mặc dù ngày nay mọi người được sử dụng nhiều hơn để làm sạch và tự bảo vệ các tính năng ngôn ngữ và ngôn ngữ ngăn không cho mình bắn vào chân, đây là một sự hồi tưởng từ một khu vực nơi các byte đã đắt tiền (C bắt đầu từ trước năm 1970). nếu mã của bạn cần vừa trong 1024 byte, bạn sẽ cảm thấy rất vui khi sử dụng lại các đoạn mã. Sử dụng lại mã bằng cách bắt đầu tại các điểm nhập khác nhau chia sẻ cùng một kết thúc là một cơ chế để đạt được điều này.
rpy

23

Để triển khai thiết bị của Duff, rõ ràng:

dsend(to, from, count)
char *to, *from;
int count;
{
    int n = (count + 7) / 8;
    switch (count % 8) {
    case 0: do { *to = *from++;
    case 7:      *to = *from++;
    case 6:      *to = *from++;
    case 5:      *to = *from++;
    case 4:      *to = *from++;
    case 3:      *to = *from++;
    case 2:      *to = *from++;
    case 1:      *to = *from++;
               } while (--n > 0);
    }
}

3
Tôi yêu thiết bị của Duff. Thật thanh lịch, và xấu xa nhanh chóng.
dicroce

7
ya, nhưng nó có phải xuất hiện mỗi khi có câu lệnh chuyển đổi về SO không?
billjamesdev

Bạn đã bỏ lỡ hai niềng răng đóng cửa ;-).
Toon Krijthe

18

Nếu các trường hợp được thiết kế để phá vỡ hoàn toàn thì bạn không thể có sự sụp đổ.

case 0:
case 1:
case 2:
    // all do the same thing.
    break;
case 3:
case 4:
    // do something different.
    break;
default:
    // something else entirely.

Nếu công tắc được thiết kế để thoát ra sau mỗi trường hợp, bạn sẽ không có lựa chọn nào về nó. Cấu trúc vỏ công tắc được thiết kế theo cách linh hoạt hơn.


12
Bạn có thể hình ảnh một công tắc bị hỏng hoàn toàn, nhưng có một từ khóa "thất bại". Lúng túng, nhưng khả thi.
dmckee --- ex-moderator mèo con

Làm thế nào sẽ tốt hơn? Tôi tưởng tượng một tuyên bố trường hợp hoạt động theo cách này thường xuyên hơn "khối mã cho mỗi trường hợp" ... đó là khối if / then / other.
billjamesdev

Tôi sẽ thêm rằng trong câu hỏi, các phạm vi bao vây {} trong mỗi khối trường hợp đang thêm vào sự nhầm lẫn vì nó trông giống như kiểu của câu lệnh "while".
Matthew Smith

1
@Bill: Tôi nghĩ sẽ tệ hơn, nhưng nó sẽ giải quyết khiếu nại do Mike B đưa ra: sự sụp đổ đó (không phải nhiều trường hợp giống nhau) là một sự kiện hiếm gặp và không nên là hành vi mặc định.
dmckee --- ex-moderator mèo con

1
Tôi rời khỏi niềng răng cho ngắn gọn, với nhiều tuyên bố họ nên ở đó. Tôi đồng ý rằng chỉ nên sử dụng dự phòng khi các trường hợp hoàn toàn giống nhau. Thật khó hiểu khi địa ngục xây dựng trên các trường hợp trước đó bằng cách sử dụng dự phòng.
Lập hóa đơn cho thằn lằn

15

Các báo cáo trường hợp trong một báo cáo chuyển đổi chỉ đơn giản là nhãn.

Khi bạn bật một giá trị, câu lệnh switch cơ bản thực hiện một goto đến nhãn với giá trị phù hợp.

Điều này có nghĩa là việc ngắt là cần thiết để tránh chuyển qua mã dưới nhãn tiếp theo.

Đối với lý do tại sao nó được thực hiện theo cách này - bản chất thông qua của một câu lệnh chuyển đổi có thể hữu ích trong một số tình huống. Ví dụ:

case optionA:
    // optionA needs to do its own thing, and also B's thing.
    // Fall-through to optionB afterwards.
    // Its behaviour is a superset of B's.
case optionB:
    // optionB needs to do its own thing
    // Its behaviour is a subset of A's.
    break;
case optionC:
    // optionC is quite independent so it does its own thing.
    break;

2
Có hai vấn đề lớn: 1) Quên đi breaknơi cần thiết. 2) Nếu các báo cáo trường hợp đã thay đổi thứ tự của chúng, sự sai lầm có thể dẫn đến trường hợp sai được chạy. Vì vậy, tôi thấy việc xử lý của C # tốt hơn nhiều (rõ ràng goto casecho sự sụp đổ, ngoại trừ nhãn trường hợp trống).
Daniel Rose

@DanielRose: 1) cũng có những cách để quên breaktrong C # - cách đơn giản nhất là khi bạn không muốn caselàm gì mà quên thêm break(có lẽ bạn đã bị cuốn vào nhận xét giải thích hoặc bị gọi đi một số nhiệm vụ khác): thực hiện sẽ chỉ rơi vào casebên dưới. 2) đáng khích lệ goto caselà khuyến khích mã hóa "spaghetti" không có cấu trúc - bạn có thể kết thúc bằng các chu kỳ tình cờ ( case A: ... goto case B; case B: ... ; goto case A;), đặc biệt khi các trường hợp được tách ra trong tệp và khó kết hợp. Trong C ++, dự phòng được bản địa hóa.
Tony Delroy

8

Để cho phép những thứ như:

switch(foo) {
case 1:
    /* stuff for case 1 only */
    if (0) {
case 2:
    /* stuff for case 2 only */
    }
    /* stuff for cases 1 and 2 */
case 3:
    /* stuff for cases 1, 2, and 3 */
}

Hãy nghĩ về casetừ khóa như một gotonhãn hiệu và nó xuất hiện tự nhiên hơn rất nhiều.


8
Rằng if(0)ở cuối trường hợp đầu tiên là xấu xa, và chỉ nên được sử dụng nếu mục tiêu là làm xáo trộn mã.
Daniel Rose

11
Toàn bộ câu trả lời này là một bài tập trong việc trở nên xấu xa, tôi nghĩ vậy. :-)
R .. GitHub DỪNG GIÚP ICE

3

Nó loại bỏ việc sao chép mã khi một số trường hợp cần thực thi cùng một mã (hoặc cùng mã theo trình tự).

Vì ở cấp độ ngôn ngữ lắp ráp, bạn không quan tâm đến việc bạn có phá vỡ giữa mỗi ngôn ngữ hay không, không có chi phí nào để vượt qua mọi trường hợp, vậy tại sao không cho phép chúng vì chúng mang lại lợi thế đáng kể trong một số trường hợp nhất định.


2

Tôi tình cờ gặp phải trường hợp gán giá trị trong vectơ cho các cấu trúc: nó phải được thực hiện theo cách nếu vectơ dữ liệu ngắn hơn số lượng thành viên dữ liệu trong cấu trúc, các thành viên còn lại sẽ ở lại giá trị mặc định của chúng. Trong trường hợp đó bỏ qua breaklà khá hữu ích.

switch (nShorts)
{
case 4: frame.leadV1    = shortArray[3];
case 3: frame.leadIII   = shortArray[2];
case 2: frame.leadII    = shortArray[1];
case 1: frame.leadI     = shortArray[0]; break;
default: TS_ASSERT(false);
}

0

Như nhiều người ở đây đã chỉ định, nó cho phép một khối mã duy nhất hoạt động cho nhiều trường hợp. Điều này nên là một sự xuất hiện phổ biến hơn cho các câu lệnh chuyển đổi của bạn so với "khối mã cho mỗi trường hợp" mà bạn chỉ định trong ví dụ của mình.

Nếu bạn có một khối mã cho mỗi trường hợp mà không bị rơi, có lẽ bạn nên cân nhắc sử dụng khối if-otherif-other, vì điều đó có vẻ phù hợp hơn.

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.