Tuyên bố chuyển đổi: phải mặc định là trường hợp cuối cùng?


178

Hãy xem xét các switchtuyên bố sau :

switch( value )
{
  case 1:
    return 1;
  default:
    value++;
    // fall-through
  case 2:
    return value * 2;
}

Mã này biên dịch, nhưng nó có hợp lệ (= hành vi được xác định) cho C90 / C99 không? Tôi chưa bao giờ thấy mã trong đó trường hợp mặc định không phải là trường hợp cuối cùng.

EDIT:
Như Jon CageKillianDS viết: đây thực sự là mã xấu xí và khó hiểu và tôi nhận thức rõ về nó. Tôi chỉ quan tâm đến cú pháp chung (được xác định?) Và đầu ra dự kiến.


19
+1 Thậm chí chưa bao giờ xem xét hành vi đó
Jamie Wong

@ Péter Török: ý bạn là nếu value == 2 nó sẽ trả về 6?
Alexandre C.

4
@ Péter Török không, thứ tự không quan trọng - nếu giá trị khớp với hằng số trong bất kỳ nhãn trường hợp nào, thì điều khiển sẽ chuyển đến câu lệnh đó theo nhãn, nếu không, điều khiển sẽ chuyển sang câu lệnh theo nhãn mặc định nếu có.
Pete Kirkham

11
@Jon Lồng gotokhông ác. Tín đồ sùng bái là! Bạn không thể tưởng tượng được những gì cực đoan mà mọi người có thể tránh để tránh gotobởi vì nó hoàn toàn xấu xa, tạo ra một mớ hỗn độn thực sự không thể đọc được của mã của họ.
Patrick Schlüter

3
Tôi sử dụng gotochủ yếu để mô phỏng một cái gì đó giống như một finallymệnh đề trong các hàm, trong đó các nguồn (tệp, bộ nhớ) phải được giải phóng khi dừng và lặp lại cho mọi trường hợp lỗi một danh sách freeclosekhông giúp ích cho khả năng đọc. Mặc dù có một cách sử dụng gotomà tôi muốn tránh nhưng không thể, là khi tôi muốn thoát ra khỏi vòng lặp và tôi switchở trong vòng lặp đó.
Patrick Schlüter

Câu trả lời:


83

Tiêu chuẩn C99 không rõ ràng về điều này, nhưng kết hợp tất cả các sự kiện lại với nhau, nó hoàn toàn hợp lệ.

A casedefaultnhãn tương đương với gotonhãn. Xem 6.8.1 Báo cáo được dán nhãn. Đặc biệt thú vị là 6.8.1.4, cho phép Thiết bị của Duff đã đề cập:

Bất kỳ câu lệnh nào cũng có thể được bắt đầu bằng tiền tố khai báo định danh là tên nhãn. Bản thân các nhãn không làm thay đổi dòng kiểm soát, mà tiếp tục không bị cản trở trên chúng.

Chỉnh sửa : Mã trong một công tắc không có gì đặc biệt; nó là một khối mã bình thường như trong một phân đoạn if, với các nhãn nhảy bổ sung. Điều này giải thích hành vi rơi xuống và tại sao breakcần thiết.

6.8.4.2.7 thậm chí còn đưa ra một ví dụ:

switch (expr) 
{ 
    int i = 4; 
    f(i); 
case 0: 
    i=17; 
    /*falls through into default code */ 
default: 
    printf("%d\n", i); 
} 

Trong đoạn chương trình nhân tạo, đối tượng có mã định danh i tồn tại với thời lượng lưu trữ tự động (trong khối) nhưng không bao giờ được khởi tạo và do đó, nếu biểu thức điều khiển có giá trị khác 0, lệnh gọi hàm printf sẽ truy cập giá trị không xác định. Tương tự, không thể gọi được hàm f.

Các hằng số trường hợp phải là duy nhất trong câu lệnh switch:

6.8.4.2.3 Biểu thức của mỗi nhãn trường hợp phải là biểu thức hằng số nguyên và không có hai biểu thức hằng số trường hợp nào trong cùng một câu lệnh chuyển đổi sẽ có cùng giá trị sau khi chuyển đổi. Có thể có nhiều nhất một nhãn mặc định trong câu lệnh switch.

Tất cả các trường hợp được đánh giá, sau đó nó nhảy đến nhãn mặc định, nếu được đưa ra:

6.8.4.2.5 Các khuyến mãi số nguyên được thực hiện trên biểu thức kiểm soát. Biểu thức không đổi trong mỗi nhãn trường hợp được chuyển đổi thành loại được tăng cường của biểu thức kiểm soát. Nếu một giá trị được chuyển đổi khớp với biểu thức điều khiển được thăng cấp, điều khiển sẽ nhảy tới câu lệnh theo nhãn trường hợp khớp. Mặt khác, nếu có nhãn mặc định, điều khiển nhảy tới câu lệnh được gắn nhãn. Nếu không có biểu thức hằng số trường hợp chuyển đổi khớp và không có nhãn mặc định, thì không có phần nào của thân công tắc được thực thi.


6
@HeathHunnicutt Bạn rõ ràng không hiểu mục đích của ví dụ. Mã này không được tạo bởi áp phích này, nhưng được lấy thẳng từ tiêu chuẩn C, như một minh họa về cách các câu lệnh chuyển đổi kỳ lạ và cách thực hành tồi sẽ dẫn đến lỗi. Nếu bạn đã bận tâm đọc văn bản bên dưới mã, bạn sẽ nhận ra càng nhiều.
Lundin

2
+1 để bù cho downvote. Việc hạ thấp ai đó từ việc trích dẫn tiêu chuẩn C có vẻ khá khắc nghiệt.
Lundin

2
@Lundin Tôi không bỏ phiếu tiêu chuẩn C và tôi đã không bỏ qua bất cứ điều gì như bạn đề xuất. Tôi đã bỏ phiếu cho phương pháp sư phạm xấu khi sử dụng một ví dụ xấu và không cần thiết. Cụ thể, ví dụ đó liên quan đến một tình huống hoàn toàn khác so với được hỏi về. Tôi có thể tiếp tục, nhưng "cảm ơn phản hồi của bạn."
Heath Hunnicutt

12
Intel yêu cầu bạn đặt mã thường xuyên nhất trước tiên trong câu lệnh chuyển đổi tại Chi nhánh và sắp xếp lại vòng lặp để ngăn chặn các sai phạm . Tôi ở đây bởi vì tôi có một defaulttrường hợp thống trị các trường hợp khác khoảng 100: 1, và tôi không biết liệu nó có hợp lệ hay không xác định để đưa ra defaulttrường hợp đầu tiên.
jww

@jww Tôi không chắc ý của bạn về Intel. Nếu bạn có nghĩa là thông minh tôi sẽ gọi nó là giả thuyết. Tôi có cùng suy nghĩ, nhưng sau đó đọc các trạng thái không giống như câu lệnh if, câu lệnh switch là truy cập ngẫu nhiên. Vì vậy, trường hợp cuối cùng không chậm hơn so với trường hợp đầu tiên. Điều này được thực hiện bằng cách băm các giá trị trường hợp không đổi. Đó là lý do tại sao các câu lệnh chuyển đổi nhanh hơn so với câu lệnh if khi các nhánh rất nhiều.

91

Các câu lệnh tình huống và câu lệnh mặc định có thể xảy ra theo bất kỳ thứ tự nào trong câu lệnh switch. Mệnh đề mặc định là mệnh đề tùy chọn được khớp nếu không có hằng số nào trong các câu lệnh tình huống có thể được khớp.

Ví dụ tốt :-

switch(5) {
  case 1:
    echo "1";
    break;
  case 2:
  default:
    echo "2, default";
    break;
  case 3;
    echo "3";
    break;
}


Outputs '2,default'

rất hữu ích nếu bạn muốn các trường hợp của bạn được trình bày theo thứ tự hợp lý trong mã (như trong trường hợp không nói trường hợp 1, trường hợp 3, trường hợp 2 / mặc định) và các trường hợp của bạn rất dài nên bạn không muốn lặp lại toàn bộ trường hợp mã ở dưới cùng cho mặc định


7
Đây chính xác là kịch bản mà tôi thường đặt mặc định ở đâu đó ngoài cuối ... có thứ tự logic cho các trường hợp rõ ràng (1, 2, 3) và tôi muốn mặc định hoạt động giống hệt như một trong những trường hợp rõ ràng không phải là cái cuối cùng
ArtOfWarfare

51

Nó hợp lệ và rất hữu ích trong một số trường hợp.

Hãy xem xét các mã sau đây:

switch(poll(fds, 1, 1000000)){
   default:
    // here goes the normal case : some events occured
   break;
   case 0:
    // here goes the timeout case
   break;
   case -1:
     // some error occurred, you have to check errno
}

Điểm đáng chú ý là đoạn mã trên dễ đọc và hiệu quả hơn so với xếp tầng if. Bạn có thể đặt defaultở cuối, nhưng nó là vô nghĩa vì nó sẽ tập trung sự chú ý của bạn vào các trường hợp lỗi thay vì các trường hợp thông thường (đây là defaulttrường hợp).

Trên thực tế, đó không phải là một ví dụ tốt, trong pollbạn biết có bao nhiêu sự kiện có thể xảy ra nhiều nhất. Quan điểm thực tế của tôi là có những trường hợp với một tập hợp các giá trị đầu vào được xác định trong đó có 'trường hợp ngoại lệ' và trường hợp bình thường. Nếu tốt hơn là đặt ngoại lệ hoặc trường hợp bình thường ở phía trước là vấn đề lựa chọn.

Trong lĩnh vực phần mềm tôi nghĩ đến một trường hợp rất thông thường khác: thu hồi với một số giá trị đầu cuối. Nếu bạn có thể biểu thị nó bằng cách sử dụng một công tắc, defaultsẽ là giá trị thông thường chứa lệnh gọi đệ quy và các phần tử phân biệt (trường hợp riêng) các giá trị đầu cuối. Thường không cần phải tập trung vào các giá trị đầu cuối.

Một lý do khác là thứ tự của các trường hợp có thể thay đổi hành vi mã được biên dịch và điều đó quan trọng đối với các màn trình diễn. Hầu hết các trình biên dịch sẽ tạo mã lắp ráp được biên dịch theo cùng thứ tự như mã xuất hiện trong công tắc. Điều đó làm cho trường hợp đầu tiên rất khác so với các trường hợp khác: tất cả các trường hợp ngoại trừ trường hợp đầu tiên sẽ liên quan đến bước nhảy và điều đó sẽ làm trống các đường ống của bộ xử lý. Bạn có thể hiểu nó giống như dự báo nhánh mặc định để chạy trường hợp xuất hiện đầu tiên trong công tắc. Nếu một trường hợp nếu phổ biến hơn nhiều so với những người khác thì bạn có lý do rất tốt để đặt nó như là trường hợp đầu tiên.

Đọc bình luận đó là lý do cụ thể tại sao người đăng ban đầu hỏi câu hỏi đó sau khi đọc sắp xếp lại Trình biên dịch Intel Loop về tối ưu hóa mã.

Sau đó, nó sẽ trở thành một số trọng tài giữa khả năng đọc mã và hiệu suất mã. Có lẽ tốt hơn để đặt một bình luận để giải thích cho người đọc trong tương lai tại sao một trường hợp xuất hiện đầu tiên.


6
+1 để đưa ra một ví dụ (tốt) mà không có hành vi ngụy biện.
KillianDS

1
... Nghĩ về nó mặc dù, tôi không tin rằng mặc định ở trên cùng là tốt bởi vì rất ít người sẽ tìm kiếm nó ở đó. Có thể tốt hơn để gán trả về cho một biến và xử lý thành công ở một bên của if và lỗi ở phía bên kia với một câu lệnh tình huống.
Jon Lồng

@Jon: cứ viết đi. Bạn thêm tiếng ồn cú pháp mà không có bất kỳ lợi ích dễ đọc. Và, nếu mặc định là trên cùng, thực sự không cần phải nhìn vào nó, nó thực sự rõ ràng (nó có thể khó khăn hơn nếu bạn đặt nó ở giữa).
kriss

Bằng cách này, tôi không thực sự thích cú pháp chuyển đổi / trường hợp C. Tôi rất muốn có thể đặt một số nhãn sau một trường hợp thay vì bắt buộc phải đặt nhiều nhãn liên tiếp case. Điều đáng buồn là nó trông giống như đường cú pháp và sẽ không phá vỡ bất kỳ mã hiện có nào nếu được hỗ trợ.
kriss

1
@kriss: Tôi đã bị cám dỗ một nửa khi nói "Tôi cũng không phải là một lập trình viên trăn!" :)
Andrew Grimm

16

vâng, điều này là hợp lệ, và trong một số trường hợp, nó thậm chí còn hữu ích. Nói chung, nếu bạn không cần nó, đừng làm điều đó.


-1: Điều này có mùi ác với tôi. Sẽ tốt hơn nếu chia mã thành một cặp câu lệnh chuyển đổi.
Jon Lồng

25
@ John John: đặt tôi -1 ở đây thật khó chịu. Đây không phải là lỗi của tôi rằng đây là mã hợp lệ.
Jens Gustyt

Chỉ tò mò, tôi muốn biết trong trường hợp nào nó hữu ích?
Salil

1
-1 là nhằm mục đích khẳng định của bạn về việc nó hữu ích. Tôi sẽ thay đổi thành +1 nếu bạn có thể cung cấp một ví dụ hợp lệ để sao lưu khiếu nại của mình.
Jon Lồng

4
Đôi khi, khi chuyển đổi thành một lỗi mà chúng ta đã nhận được từ một số chức năng hệ thống. Giả sử chúng ta có một trường hợp mà chúng ta biết rằng chúng ta phải thực hiện một lối thoát sạch, nhưng lối thoát sạch này có thể yêu cầu một số dòng mã hóa mà chúng ta không muốn lặp lại. Nhưng giả sử, chúng tôi cũng có rất nhiều mã lỗi kỳ lạ khác mà chúng tôi không muốn xử lý riêng lẻ. Tôi sẽ xem xét chỉ cần đặt một perror trong trường hợp mặc định và để nó chạy qua trường hợp khác và thoát ra một cách sạch sẽ. Tôi không nói bạn nên làm như thế. Nó chỉ là một vấn đề của hương vị.
Jens Gustyt

8

Không có thứ tự xác định trong một tuyên bố chuyển đổi. Bạn có thể xem các trường hợp như một cái gì đó giống như một nhãn có tên, giống như một gotonhãn. Trái với những gì mọi người dường như nghĩ ở đây, trong trường hợp giá trị 2, nhãn mặc định không được nhảy tới. Để minh họa bằng một ví dụ cổ điển, đây là thiết bị của Duff , là con đẻ của các thái cực switch/casetrong C.

send(to, from, count)
register short *to, *from;
register count;
{
  register 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);
  }
}

4
Và đối với bất kỳ ai không quen thuộc với thiết bị của Duff, mã này hoàn toàn không thể đọc được ...
KillianDS

7

Một kịch bản trong đó tôi cho rằng nó phù hợp để đặt 'mặc định' ở đâu đó ngoài phần cuối của câu lệnh tình huống trong một máy trạng thái nơi trạng thái không hợp lệ sẽ đặt lại máy và tiếp tục như thể đó là trạng thái ban đầu. Ví dụ:

chuyển đổi (widget_state)
{
  mặc định: / * Đã tắt đường ray - đặt lại và tiếp tục * /
    widget_state = WIDGET_START;
    / * Rơi qua * /
  trường hợp WIDGET_START:
    ...
    phá vỡ;
  trường hợp WIDGET_WHATEVER:
    ...
    phá vỡ;
}

một sự sắp xếp khác, nếu trạng thái không hợp lệ không nên đặt lại máy mà nên dễ dàng xác định là trạng thái không hợp lệ:

chuyển đổi (widget_state) { trường hợp WIDGET_IDLE: widget_ yet = 0; widget_hardware_off (); phá vỡ; trường hợp WIDGET_START: ... phá vỡ; trường hợp WIDGET_WHATEVER: ... phá vỡ; mặc định: widget_state = WIDGET_INVALID_STATE; / * Rơi qua * / trường hợp WIDGET_INVALID_STATE: widget_ yet = 0; widget_hardware_off (); ... làm bất cứ điều gì khác là cần thiết để thiết lập một điều kiện "an toàn" }

Mã ở nơi khác sau đó có thể kiểm tra (widget_state == WIDGET_INVALID_STATE) và cung cấp bất kỳ hành vi báo cáo lỗi hoặc thiết lập lại trạng thái nào có vẻ phù hợp. Ví dụ: mã vạch trạng thái có thể hiển thị biểu tượng lỗi và tùy chọn trình đơn "tiện ích bắt đầu" bị vô hiệu hóa ở hầu hết các trạng thái không sử dụng có thể được bật cho WIDGET_INVALID_STATE cũng như WIDGET_IDLE.


6

Thay đổi bằng một ví dụ khác: Điều này có thể hữu ích nếu "mặc định" là trường hợp không mong muốn và bạn muốn đăng nhập lỗi nhưng cũng làm điều gì đó hợp lý. Ví dụ từ một số mã của riêng tôi:

  switch (style)
  {
  default:
    MSPUB_DEBUG_MSG(("Couldn't match dash style, using solid line.\n"));
  case SOLID:
    return Dash(0, RECT_DOT);
  case DASH_SYS:
  {
    Dash ret(shapeLineWidth, dotStyle);
    ret.m_dots.push_back(Dot(1, 3 * shapeLineWidth));
    return ret;
  }
  // more cases follow
  }

5

Có những trường hợp khi bạn chuyển đổi ENUM thành một chuỗi hoặc chuyển đổi chuỗi thành enum trong trường hợp bạn đang viết / đọc thành / từ một tệp.

Đôi khi bạn cần đặt một trong các giá trị mặc định để khắc phục các lỗi được thực hiện bằng cách chỉnh sửa các tệp theo cách thủ công.

switch(textureMode)
{
case ModeTiled:
default:
    // write to a file "tiled"
    break;

case ModeStretched:
    // write to a file "stretched"
    break;
}

2

Điều defaultkiện có thể là bất cứ nơi nào trong chuyển đổi mà một mệnh đề trường hợp có thể tồn tại. Nó không bắt buộc phải là mệnh đề cuối cùng. Tôi đã thấy mã đặt mặc định là mệnh đề đầu tiên. Việc case 2:được thực thi bình thường, mặc dù mệnh đề mặc định ở trên nó.

Để thử nghiệm, tôi đặt mã mẫu vào một hàm, được gọi test(int value){}và chạy:

  printf("0=%d\n", test(0));
  printf("1=%d\n", test(1));
  printf("2=%d\n", test(2));
  printf("3=%d\n", test(3));
  printf("4=%d\n", test(4));

Đầu ra là:

0=2
1=1
2=4
3=8
4=10

1

Nó hợp lệ, nhưng khá khó chịu. Tôi sẽ đề nghị nói chung là không tốt khi cho phép thông qua vì nó có thể dẫn đến một số mã spaghetti rất lộn xộn.

Gần như chắc chắn tốt hơn là chia những trường hợp này thành một số câu lệnh chuyển đổi hoặc các hàm nhỏ hơn.

[sửa] @Tristopia: Ví dụ của bạn:

Example from UCS-2 to UTF-8 conversion 

r is the destination array, 
wc is the input wchar_t  

switch(utf8_length) 
{ 
    /* Note: code falls through cases! */ 
    case 3: r[2] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0x800; 
    case 2: r[1] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0x0c0; 
    case 1: r[0] = wc;
}

sẽ rõ ràng hơn theo ý định của nó (tôi nghĩ) nếu nó được viết như thế này:

if( utf8_length >= 1 )
{
    r[0] = wc;

    if( utf8_length >= 2 )
    {
        r[1] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0x0c0; 

        if( utf8_length == 3 )
        {
            r[2] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0x800; 
        }
    }
}   

[edit2] @Tristopia: Ví dụ thứ hai của bạn có lẽ là ví dụ rõ ràng nhất về cách sử dụng tốt để theo dõi:

for(i=0; s[i]; i++)
{
    switch(s[i])
    {
    case '"': 
    case '\'': 
    case '\\': 
        d[dlen++] = '\\'; 
        /* fall through */ 
    default: 
        d[dlen++] = s[i]; 
    } 
}

.. nhưng cá nhân tôi sẽ chia nhận dạng nhận xét thành chức năng riêng của nó:

bool isComment(char charInQuestion)
{   
    bool charIsComment = false;
    switch(charInQuestion)
    {
    case '"': 
    case '\'': 
    case '\\': 
        charIsComment = true; 
    default: 
        charIsComment = false; 
    } 
    return charIsComment;
}

for(i=0; s[i]; i++)
{
    if( isComment(s[i]) )
    {
        d[dlen++] = '\\'; 
    }
    d[dlen++] = s[i]; 
}

2
Có những trường hợp rơi qua thực sự, thực sự là một ý tưởng tốt.
Patrick Schlüter

Ví dụ từ chuyển đổi UCS-2 sang UTF-8 rlà mảng đích, wclà công wchar_t tắc đầu vào (utf8_length) {/ * Lưu ý: mã rơi vào các trường hợp! * / trường hợp 3: r [2] = 0x80 | (wc & 0x3f); wc >> = 6; wc | = 0x800; trường hợp 2: r [1] = 0x80 | (wc & 0x3f); wc >> = 6; wc | = 0xc0; trường hợp 1: r [0] = wc; }
Patrick Schlüter

Ở đây, một thói quen sao chép chuỗi với ký tự thoát: for(i=0; s[i]; i++) { switch(s[i]) { case '"': case '\'': case '\\': d[dlen++] = '\\'; /* fall through */ default: d[dlen++] = s[i]; } }
Patrick Schlüter

Có, nhưng thói quen này là một trong những điểm nóng của chúng tôi, đây là cách nhanh nhất, di động (chúng tôi sẽ không lắp ráp) để thực hiện nó. Nó chỉ có 1 thử nghiệm cho bất kỳ độ dài UTF nào, của bạn có 2 hoặc thậm chí 3. Ngoài ra, tôi đã không nghĩ ra nó, tôi đã lấy nó từ BSD.
Patrick Schlüter

1
Có, đặc biệt là trong các chuyển đổi bằng tiếng Bulgaria và tiếng Hy Lạp (trên Solaris SPARC) và văn bản với đánh dấu nội bộ của chúng tôi (đó là 3 byte UTF8). Phải thừa nhận rằng, trong toto nó không nhiều và đã trở nên không liên quan kể từ lần cập nhật phần cứng cuối cùng của chúng tôi, nhưng tại thời điểm nó được viết, nó đã tạo ra một số khác biệt.
Patrick Schlüter
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.