Tại sao C # không có phạm vi cục bộ trong các khối trường hợp?


38

Tôi đã viết mã này:

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.");
    }
}

Và rất ngạc nhiên khi tìm thấy một lỗi biên dịch:

Một biến cục bộ có tên 'hành động' đã được xác định trong phạm vi này

Đó là một vấn đề khá dễ dàng để giải quyết; chỉ cần thoát khỏi thứ hai varđã lừa

Rõ ràng các biến được khai báo trong casecác khối có phạm vi của cha mẹ switch, nhưng tôi tò mò về lý do tại sao điều này là. Cho rằng C # không cho phép thực hiện giảm thông qua các trường hợp khác (nó đòi hỏi break , return, throw, hoặc goto casebáo cáo vào cuối mỗi casekhối), nó có vẻ khá kỳ lạ mà nó sẽ cho phép khai báo biến bên trong một caseđược sử dụng hoặc mâu thuẫn với các biến trong bất kỳ khác case. Nói cách khác, các biến dường như rơi vào các casecâu lệnh mặc dù thực thi không thể. C # rất đau đớn để thúc đẩy khả năng đọc bằng cách cấm một số cấu trúc của các ngôn ngữ khác gây nhầm lẫn hoặc dễ bị lạm dụng. Nhưng điều này có vẻ như nó chỉ bị ràng buộc để gây nhầm lẫn. Hãy xem xét các tình huống sau:

  1. Nếu được thay đổi nó thành này:

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

    Tôi nhận được " Sử dụng biến cục bộ chưa được gán 'hành động' ". Điều này gây nhầm lẫn bởi vì trong mọi cấu trúc khác trong C # mà tôi có thể nghĩ var action = ...sẽ khởi tạo biến, nhưng ở đây nó chỉ đơn giản là khai báo nó.

  2. Nếu tôi đã trao đổi các trường hợp như thế này:

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

    Tôi nhận được " Không thể sử dụng biến cục bộ 'hành động' trước khi nó được khai báo ". Vì vậy, thứ tự của các khối trường hợp dường như rất quan trọng ở đây theo cách không hoàn toàn rõ ràng - Thông thường tôi có thể viết chúng theo bất kỳ thứ tự nào tôi muốn, nhưng vì varphải xuất hiện trong khối đầu tiên actionđược sử dụng, tôi phải điều chỉnh casecác khối phù hợp.

  3. Nếu được thay đổi nó thành này:

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

    Sau đó, tôi không nhận được lỗi, nhưng theo một nghĩa nào đó, có vẻ như biến đang được gán một giá trị trước khi nó được khai báo.
    (Mặc dù tôi không thể nghĩ về bất cứ lúc nào bạn thực sự muốn làm điều này - tôi thậm chí không biết goto caseđã tồn tại trước ngày hôm nay)

Vì vậy, câu hỏi của tôi là, tại sao các nhà thiết kế của C # không đưa ra casephạm vi địa phương của riêng họ? Có bất kỳ lý do lịch sử hoặc kỹ thuật cho việc này?


4
Khai báo actionbiến của bạn trước switchcâu lệnh hoặc đặt từng trường hợp vào dấu ngoặc nhọn của chính nó và bạn sẽ có hành vi hợp lý.
Robert Harvey

3
@RobertHarvey có, và tôi có thể đi xa hơn và viết nó mà không cần sử dụng một switchchút nào - Tôi chỉ tò mò về lý do đằng sau thiết kế này.
pswg

Nếu c # cần nghỉ sau mỗi trường hợp thì tôi đoán nó phải vì lý do lịch sử như trong c / java không phải vậy!
tgkprog

@tgkprog Tôi tin rằng không phải vì lý do lịch sử và các nhà thiết kế của C # đã cố tình làm điều đó để đảm bảo rằng lỗi phổ biến của việc quên a breaklà không thể xảy ra trong C #.
Svick

12
Xem câu trả lời của Eric Lippert về câu hỏi SO liên quan này. stackoverflow.com/a/1076642/414076 Bạn có thể hoặc không thể hài lòng. Nó đọc là "bởi vì đó là cách họ chọn để làm điều đó vào năm 1999."
Anthony Pegram

Câu trả lời:


24

Tôi nghĩ rằng một lý do chính đáng là trong mọi trường hợp khác, phạm vi của biến cục bộ của bình thường là một khối được giới hạn bởi dấu ngoặc ( {}). Các biến cục bộ không bình thường xuất hiện trong một cấu trúc đặc biệt trước một câu lệnh (thường là một khối), giống như một forbiến vòng lặp hoặc một biến được khai báo using.

Một ngoại lệ nữa là các biến cục bộ trong các biểu thức truy vấn LINQ, nhưng các biến đó hoàn toàn khác với các khai báo biến cục bộ thông thường, vì vậy tôi không nghĩ rằng có thể có sự nhầm lẫn ở đó.

Để tham khảo, các quy tắc nằm trong §3.7 Phạm vi của thông số C #:

  • Phạm vi của một biến cục bộ được khai báo trong một khai báo biến cục bộ là khối trong đó khai báo xảy ra.

  • Phạm vi của một biến cục bộ được khai báo trong khối chuyển đổi của switchcâu lệnh là khối chuyển đổi .

  • Phạm vi của một biến cục bộ được khai báo trong bộ khởi tạo của forcâu lệnh là for-bộ khởi tạo , for-condition , for-iteratorcâu lệnh chứa của forcâu lệnh.

  • Phạm vi của một biến khai báo là một phần của một foreach-tuyên bố , sử dụng-tuyên bố , khóa tuyên bố hoặc truy vấn thể hiện được xác định bởi sự mở rộng của cấu trúc nhất định.

(Mặc dù tôi không hoàn toàn chắc chắn tại sao switchkhối được đề cập rõ ràng, vì nó không có bất kỳ cú pháp đặc biệt nào cho các suy giảm biến cục bộ, không giống như tất cả các cấu trúc được đề cập khác.)


1
+1 Bạn có thể cung cấp một liên kết đến phạm vi bạn trích dẫn không?
pswg

@pswg Bạn có thể tìm thấy một liên kết để tải xuống đặc tả trên MSDN . (Nhấp vào liên kết có nội dung mạng Microsoft Nhà phát triển Microsoft (MSDN) trên trang đó.)
svick

Vì vậy, tôi đã tra cứu các thông số kỹ thuật Java và switchesdường như hành xử giống hệt nhau về vấn đề này (ngoại trừ yêu cầu nhảy vào cuối của case). Có vẻ như hành vi này được sao chép đơn giản từ đó. Vì vậy, tôi đoán câu trả lời ngắn gọn là - các casecâu lệnh không tạo ra các khối, chúng chỉ đơn giản xác định các phân chia của một switchkhối - và do đó không có phạm vi riêng.
pswg

3
Re: Tôi không hoàn toàn chắc chắn tại sao khối chuyển đổi được đề cập rõ ràng - tác giả của thông số kỹ thuật chỉ là kén chọn, chỉ ra rằng khối chuyển đổi có ngữ pháp khác với khối thông thường.
Eric Lippert

42

Nhưng nó làm. Bạn có thể tạo phạm vi địa phương ở bất cứ đâu bằng cách gói các dòng với{}

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 đã sử dụng cái này và nó hoạt động như mô tả
Mvision

1
+1 Đây là một mẹo hay, nhưng tôi sẽ chấp nhận câu trả lời của Svick vì đã đến gần nhất để giải quyết câu hỏi ban đầu của tôi.
pswg

10

Tôi sẽ trích dẫn Eric Lippert, câu trả lời khá rõ ràng về chủ đề này:

Một câu hỏi hợp lý là "tại sao điều này không hợp pháp?" Một câu trả lời hợp lý là "tốt, tại sao nó phải như vậy"? Bạn có thể có một trong hai cách. Đây là hợp pháp:

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

hoặc điều này là hợp pháp:

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

nhưng bạn không thể có cả hai cách. Các nhà thiết kế của C # đã chọn cách thứ hai dường như là cách tự nhiên hơn để làm điều đó.

Quyết định này được đưa ra vào ngày 7 tháng 7 năm 1999, chỉ mới mười năm trước. Các ý kiến ​​trong các ghi chú từ ngày hôm đó cực kỳ ngắn gọn, chỉ cần nêu rõ "Trường hợp chuyển đổi không tạo không gian khai báo riêng" và sau đó đưa ra một số mã mẫu cho thấy những gì hoạt động và những gì không.

Để tìm hiểu thêm về những gì trong tâm trí của các nhà thiết kế vào ngày đặc biệt này, tôi phải nói với nhiều người về những gì họ đã nghĩ mười năm trước - và nói với họ về vấn đề cuối cùng là tầm thường; Tôi sẽ không làm điều đó.

Nói tóm lại, không có lý do đặc biệt thuyết phục để chọn cách này hay cách khác; Cả hai đều có công. Nhóm thiết kế ngôn ngữ đã chọn một cách vì họ phải chọn một cách; người mà họ chọn có vẻ hợp lý với tôi.

Vì vậy, trừ khi bạn thân thiết hơn với nhóm nhà phát triển C # 1999 so với Eric Lippert, bạn sẽ không bao giờ biết lý do chính xác!


Không phải là một downvoter, nhưng đây là một nhận xét về câu hỏi ngày hôm qua .
Jesse C. Choper

4
Tôi thấy điều đó, nhưng vì người bình luận không đăng câu trả lời, tôi nghĩ rằng có thể nên tạo một cách rõ ràng để tạo ra anwser bằng cách trích dẫn nội dung của liên kết. Và tôi không quan tâm đến downvote! OP hỏi lý do lịch sử, không phải là câu trả lời "Cách thực hiện".
Cyril Gandon

5

Giải thích rất đơn giản - đó là vì nó giống như trong C. Các ngôn ngữ như C ++, Java và C # đã sao chép cú pháp và phạm vi của câu lệnh chuyển đổi để làm quen.

. )

Trong C, các báo cáo trường hợp tương tự như nhãn goto. Câu lệnh switch thực sự là một cú pháp đẹp hơn cho một goto được tính toán . Các trường hợp xác định các điểm nhập cảnh vào khối chuyển đổi. Theo mặc định, phần còn lại của mã sẽ được thực thi, trừ khi có một lối thoát rõ ràng. Vì vậy, nó chỉ có ý nghĩa họ sử dụng cùng một phạm vi.

(Về cơ bản hơn. Goto không có cấu trúc - chúng không xác định hoặc phân định các phần của mã, chúng chỉ xác định các điểm nhảy. Do đó, nhãn goto không thể đưa ra phạm vi.)

C # giữ lại cú pháp, nhưng đưa ra một biện pháp bảo vệ chống lại "rơi qua" bằng cách yêu cầu thoát sau mỗi mệnh đề trường hợp (không trống). Nhưng điều này thay đổi cách chúng ta nghĩ về chuyển đổi! Các trường hợp bây giờ là các nhánh thay thế , giống như các nhánh trong if-other. Điều này có nghĩa là chúng ta sẽ mong đợi mỗi nhánh xác định phạm vi riêng của nó giống như mệnh đề if hoặc mệnh đề lặp.

Vì vậy, tóm lại: Các trường hợp chia sẻ cùng một phạm vi bởi vì đây là cách nó diễn ra trong C. Nhưng nó có vẻ kỳ lạ và không nhất quán trong C # vì chúng tôi nghĩ rằng các trường hợp là các nhánh thay vì các mục tiêu goto.


1
" Có vẻ kỳ lạ và không nhất quán trong C # vì chúng tôi nghĩ các trường hợp là các nhánh thay vì mục tiêu goto " Đó chính xác là sự nhầm lẫn mà tôi có. +1 để giải thích lý do thẩm mỹ đằng sau lý do tại sao nó cảm thấy sai trong C #.
pswg

4

Một cách đơn giản để xem xét phạm vi là xem xét phạm vi theo các khối {}.

switchkhông chứa bất kỳ khối nào, nó không thể có phạm vi khác nhau.


3
Thế còn for (int i = 0; i < n; i++) Write(i); /* legal */ Write(i); /* illegal */? Không có khối, nhưng có phạm vi khác nhau.
Svick

@svick: Như tôi đã nói, đơn giản hóa, có một khối, được tạo bởi forcâu lệnh. switchkhông tạo khối cho từng trường hợp, chỉ ở cấp cao nhất. Điểm giống nhau là mỗi câu lệnh tạo ra một khối (tính switchlà câu lệnh).
Guvante

1
Vậy thì tại sao bạn không thể đếm casethay?
Svick

1
@svick: Bởi vì casekhông chứa một khối, trừ khi bạn quyết định thêm một khối. forkéo dài cho câu lệnh tiếp theo trừ khi bạn thêm {}để nó luôn có một khối, nhưng casekéo dài cho đến khi một cái gì đó khiến bạn rời khỏi khối thực sự, switchcâu lệnh.
Guvante
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.