Trong C #, tại sao các biến được khai báo bên trong khối thử bị giới hạn phạm vi?


23

Tôi muốn thêm xử lý lỗi vào:

var firstVariable = 1;
var secondVariable = firstVariable;

Dưới đây sẽ không biên dịch:

try
{
   var firstVariable = 1;
}
catch {}

try
{
   var secondVariable = firstVariable;
}
catch {}

Tại sao cần phải có một khối bắt thử ảnh hưởng đến phạm vi của các biến như các khối mã khác làm? Bỏ sự nhất quán sang một bên, sẽ không có nghĩa gì nếu chúng ta có thể bọc mã của mình bằng cách xử lý lỗi mà không cần phải cấu trúc lại?


14
A try.. catchlà một loại khối mã cụ thể và theo như tất cả các khối mã, bạn không thể khai báo một biến trong một và sử dụng cùng một biến đó trong một biến khác như một vấn đề về phạm vi.
Neil

"Là một loại khối mã cụ thể". Cụ thể theo cách nào? Cảm ơn
JMᴇᴇ

7
Tôi có nghĩa là bất cứ điều gì giữa dấu ngoặc nhọn là một khối mã. Bạn thấy nó theo một câu lệnh if và theo một câu lệnh for, mặc dù khái niệm này giống nhau. Các nội dung là trong một phạm vi nâng cao đối với phạm vi cha mẹ của nó. Tôi khá chắc chắn rằng điều này sẽ có vấn đề nếu bạn chỉ sử dụng dấu ngoặc nhọn {}mà không thử.
Neil

Mẹo: Lưu ý rằng việc sử dụng (IDis Dùng) {} và chỉ {} cũng áp dụng tương tự. Khi bạn sử dụng sử dụng với IDis Dùng một lần, nó sẽ tự động dọn sạch các tài nguyên bất kể thành công hay thất bại. Có một vài trường hợp ngoại lệ, như không phải tất cả các lớp bạn mong muốn triển khai IDis Dùng ...
Julia McGuigan

1
Rất nhiều cuộc thảo luận về cùng một câu hỏi trên StackOverflow, tại đây: stackoverflow.com/questions/94977/iêu
Jon Schneider

Câu trả lời:


90

Nếu mã của bạn là:

try
{
   MethodThatMightThrow();
   var firstVariable = 1;
}
catch {}

try
{
   var secondVariable = firstVariable;
}
catch {}

Bây giờ bạn sẽ cố gắng sử dụng một biến không được khai báo ( firstVariable) nếu cuộc gọi phương thức của bạn ném.

Lưu ý : Ví dụ trên trả lời cụ thể câu hỏi ban đầu, trong đó nêu rõ "tính nhất quán sang một bên". Điều này chứng tỏ rằng có những lý do khác hơn là tính nhất quán. Nhưng như câu trả lời của Peter cho thấy, cũng có một lập luận mạnh mẽ từ tính nhất quán, chắc chắn đó sẽ là một yếu tố rất quan trọng trong quyết định.


Ahh, đây chính xác là những gì tôi đã theo đuổi. Tôi biết có một số tính năng ngôn ngữ khiến những gì tôi cho là không thể, nhưng tôi không thể đưa ra bất kỳ kịch bản nào. Cảm ơn rất nhiều.
JMᴇᴇ

1
"Bây giờ bạn sẽ cố gắng sử dụng một biến không được khai báo nếu cuộc gọi phương thức của bạn ném." Hơn nữa, giả sử điều này được tránh bằng cách xử lý biến như thể nó đã được khai báo, nhưng không được khởi tạo, trước khi mã có thể ném. Sau đó, nó sẽ không được khai báo, nhưng nó vẫn có khả năng chưa được chỉ định và phân tích phân công xác định sẽ cấm đọc giá trị của nó (không có sự phân công can thiệp mà nó có thể chứng minh được).
Eliah Kagan

3
Đối với ngôn ngữ tĩnh như C #, khai báo chỉ thực sự có liên quan tại thời gian biên dịch. Trình biên dịch có thể dễ dàng di chuyển khai báo trước đó trong phạm vi. Thực tế quan trọng hơn trong thời gian chạy là biến có thể không được khởi tạo .
jpmc26

3
Tôi không đồng ý với câu trả lời này. C # đã có quy tắc rằng các biến chưa được khởi tạo không thể được đọc từ đó, với một số nhận thức về dataflow. (Hãy thử khai báo các biến trong trường hợp a switchvà truy cập chúng trong các biến khác.) Quy tắc này có thể dễ dàng áp dụng ở đây và ngăn không cho mã này biên dịch. Tôi nghĩ rằng câu trả lời của Peter dưới đây là hợp lý hơn.
Sebastian Redl

2
Có một sự khác biệt giữa chưa được khai báo và C # theo dõi chúng riêng rẽ. Nếu bạn được phép sử dụng một biến bên ngoài khối nơi nó được khai báo, điều đó có nghĩa là bạn sẽ có thể gán cho nó trong catchkhối đầu tiên và sau đó nó sẽ được gán chắc chắn trong trykhối thứ hai .
Svick

64

Tôi biết rằng điều này đã được Ben trả lời tốt, nhưng tôi muốn giải quyết POV nhất quán được thuận tiện đẩy sang một bên. Giả sử rằng try/catchcác khối không ảnh hưởng đến phạm vi, thì bạn sẽ kết thúc bằng:

{
    // new scope here
}

try
{
   // Not new scope
}

Và với tôi điều này đâm đầu vào Nguyên tắc ít gây ngạc nhiên nhất (Pola) bởi vì bây giờ bạn có {}thực hiện nhiệm vụ kép tùy thuộc vào bối cảnh của những gì trước chúng.

Cách duy nhất để thoát khỏi mớ hỗn độn này là chỉ định một số điểm đánh dấu khác để phân định try/catchcác khối. Mà bắt đầu thêm một mùi mã. Vì vậy, vào thời điểm bạn không biết gì try/catchvề ngôn ngữ, nó sẽ trở nên lộn xộn đến mức bạn sẽ tốt hơn với phiên bản có phạm vi.


Một câu trả lời tuyệt vời khác. Và tôi chưa bao giờ nghe nói về Pola, rất hay đọc thêm. Cảm ơn rất nhiều bạn đời.
JMᴇᴇ

"Cách duy nhất để thoát khỏi mớ hỗn độn này là chỉ định một số điểm đánh dấu khác để phân định try/ catchkhối." - ý bạn là : try { { // scope } }? :)
CompuChip

@CompuChip sẽ có {}nhiệm vụ kéo theo phạm vi kép và không tạo phạm vi tùy thuộc vào ngữ cảnh. try^ //no-scope ^sẽ là một ví dụ về một điểm đánh dấu khác nhau.
Leliel

1
Theo tôi, đây là lý do cơ bản hơn nhiều, và gần hơn với câu trả lời "thực sự".
Jack Aidley

@JackAidley đặc biệt vì bạn đã có thể viết mã nơi bạn sử dụng một biến có thể không được chỉ định. Vì vậy, trong khi câu trả lời của Ben có quan điểm về việc đây là hành vi hữu ích như thế nào, tôi không xem đó là lý do tại sao hành vi đó tồn tại. Câu trả lời của Ben lưu ý OP nói rằng "sự nhất quán sang một bên", nhưng tính nhất quán là một lý do hoàn toàn tốt! Phạm vi hẹp có tất cả các loại lợi ích khác.
Kat

21

Bỏ sự nhất quán sang một bên, sẽ không có nghĩa gì nếu chúng ta có thể bọc mã của mình bằng cách xử lý lỗi mà không cần phải cấu trúc lại?

Để trả lời điều này, cần phải xem xét nhiều hơn là phạm vi của một biến .

Ngay cả khi biến vẫn nằm trong phạm vi, nó sẽ không được gán chắc chắn .

Khai báo biến trong khối thử biểu thị - cho trình biên dịch và cho người đọc con người - rằng nó chỉ có ý nghĩa bên trong khối đó. Nó rất hữu ích cho trình biên dịch để thực thi điều đó.

Nếu bạn muốn biến nằm trong phạm vi sau khối thử, bạn có thể khai báo nó bên ngoài khối:

var zerothVariable = 1_000_000_000_000L;
int firstVariable;

try {
    // Change checked to unchecked to allow the overflow without throwing.
    firstVariable = checked((int)zerothVariable);
}
catch (OverflowException e) {
    Console.Error.WriteLine(e.Message);
    Environment.Exit(1);
}

Điều đó thể hiện rằng biến có thể có ý nghĩa bên ngoài khối thử. Trình biên dịch sẽ cho phép điều này.

Nhưng nó cũng cho thấy một lý do khác thường không hữu ích để giữ các biến trong phạm vi sau khi giới thiệu chúng trong một khối thử. Trình biên dịch C # thực hiện phân tích gán xác định và cấm đọc giá trị của biến mà nó chưa được chứng minh đã được đưa ra một giá trị. Vì vậy, bạn vẫn không thể đọc từ biến.

Giả sử tôi cố đọc từ biến sau khối thử:

Console.WriteLine(firstVariable);

Điều đó sẽ đưa ra một lỗi thời gian biên dịch :

CS0165 Sử dụng biến cục bộ chưa được gán 'FirstVariable'

Tôi đã gọi là môi trường.Exit trong khối bắt, vì vậy tôi biết biến đã được chỉ định trước khi gọi đến Console.WriteLine. Nhưng trình biên dịch không suy ra điều này.

Tại sao trình biên dịch rất nghiêm ngặt?

Tôi thậm chí không thể làm điều này:

int n;

try {
    n = 10; // I know this won't throw an IOException.
}
catch (IOException) {
}

Console.WriteLine(n);

Một cách để xem xét hạn chế này là nói phân tích phân công xác định trong C # không phức tạp lắm. Nhưng một cách khác để xem xét nó là, khi bạn viết mã trong một khối thử với các mệnh đề bắt, bạn đang nói với cả trình biên dịch và bất kỳ độc giả nào của con người rằng nó nên được xử lý như thể nó không thể chạy được.

Để minh họa điều tôi muốn nói, hãy tưởng tượng nếu trình biên dịch cho phép mã ở trên, nhưng sau đó bạn đã thêm một cuộc gọi trong khối thử vào một chức năng mà cá nhân bạn biết sẽ không ném ngoại lệ . Không thể đảm bảo rằng hàm được gọi không ném IOException, trình biên dịch không thể biết rằng nó nđã được gán, và sau đó bạn sẽ phải cấu trúc lại.

Điều này có nghĩa là, bằng cách đưa ra phân tích rất tinh vi trong việc xác định xem một biến được gán trong khối thử với mệnh đề bắt đã được gán chắc chắn sau đó, trình biên dịch giúp bạn tránh viết mã có khả năng bị hỏng sau này. (Rốt cuộc, bắt một ngoại lệ thường có nghĩa là bạn nghĩ người ta có thể bị ném.)

Bạn có thể đảm bảo biến được gán thông qua tất cả các đường dẫn mã.

Bạn có thể tạo mã biên dịch bằng cách cho biến một giá trị trước khối thử hoặc trong khối bắt. Theo cách đó, nó vẫn sẽ được khởi tạo hoặc được chỉ định, ngay cả khi việc gán trong khối thử không diễn ra. Ví dụ:

var n = 0; // But is this meaningful, or just covering a bug?

try {
    n = 10;
}
catch (IOException) {
}

Console.WriteLine(n);

Hoặc là:

int n;

try {
    n = 10;
}
catch (IOException) {
    n = 0; // But is this meaningful, or just covering a bug?
}

Console.WriteLine(n);

Những người biên dịch. Nhưng tốt nhất là chỉ làm điều gì đó tương tự nếu giá trị mặc định bạn cung cấp có ý nghĩa * và tạo ra hành vi chính xác.

Lưu ý rằng, trong trường hợp thứ hai này khi bạn gán biến trong khối thử và trong tất cả các khối bắt, mặc dù bạn có thể đọc biến sau khi thử bắt, bạn vẫn không thể đọc biến trong finallykhối đính kèm , bởi vì thực thi có thể để lại một khối thử trong nhiều tình huống hơn chúng ta thường nghĩ .

* Nhân tiện , một số ngôn ngữ, như C và C ++, cả hai đều cho phép các biến chưa được khởi tạokhông có phân tích gán xác định để ngăn việc đọc từ chúng. Bởi vì đọc bộ nhớ uninitialized gây các chương trình để cư xử trong một không xác định và không ổn định thời trang, nó thường được khuyến cáo để tránh giới thiệu các biến trong những ngôn ngữ mà không cần cung cấp một initializer. Trong các ngôn ngữ có phân tích gán xác định như C # và Java, trình biên dịch giúp bạn không đọc các biến chưa được khởi tạo và cũng không phải là ít tệ hơn khi khởi tạo chúng với các giá trị vô nghĩa mà sau này có thể bị hiểu sai là có ý nghĩa.

Bạn có thể làm cho nó sao cho các đường dẫn mã trong đó biến không được chỉ định đưa ra một ngoại lệ (hoặc trả về).

Nếu bạn có kế hoạch thực hiện một số hành động (như ghi nhật ký) và lấy lại ngoại lệ hoặc ném ngoại lệ khác, và điều này xảy ra trong bất kỳ mệnh đề bắt nào khi biến không được gán, thì trình biên dịch sẽ biết biến đã được gán:

int n;

try {
    n = 10;
}
catch (IOException e) {
    Console.Error.WriteLine(e.Message);
    throw;
}

Console.WriteLine(n);

Điều đó biên dịch, và cũng có thể là một lựa chọn hợp lý. Tuy nhiên, trong một ứng dụng thực tế, trừ khi ngoại lệ chỉ bị ném trong các tình huống thậm chí không có ý nghĩa để cố gắng khôi phục * , bạn nên đảm bảo rằng bạn vẫn đang nắm bắt và xử lý đúng cách ở đâu đó .

(Bạn cũng không thể đọc biến trong một khối cuối cùng trong tình huống này, nhưng không có vẻ như bạn sẽ có thể - sau tất cả, cuối cùng, các khối về cơ bản luôn luôn chạy và trong trường hợp này, biến không phải luôn luôn được chỉ định .)

* Ví dụ: nhiều ứng dụng không có mệnh đề bắt xử lý OutOfMemoryException bởi vì bất kỳ điều gì chúng có thể làm về nó đều có thể ít nhất là tồi tệ như sự cố .

Có lẽ bạn thực sự làm muốn Refactor mã.

Trong ví dụ của bạn, bạn giới thiệu firstVariablesecondVariabletrong các khối thử. Như tôi đã nói, bạn có thể xác định chúng trước các khối thử được gán trong đó chúng sẽ vẫn ở trong phạm vi sau đó và bạn có thể thỏa mãn / lừa trình biên dịch cho phép bạn đọc từ chúng bằng cách đảm bảo chúng luôn được gán.

Nhưng mã xuất hiện sau các khối đó có lẽ phụ thuộc vào chúng đã được gán chính xác. Nếu đó là trường hợp, thì mã của bạn sẽ phản ánh và đảm bảo rằng.

Đầu tiên, bạn có thể (và nên) thực sự xử lý lỗi ở đó không? Một trong những lý do xử lý ngoại lệ tồn tại là để giúp xử lý các lỗi dễ dàng hơn khi chúng có thể xử lý hiệu quả , ngay cả khi đó không ở gần nơi chúng xảy ra.

Nếu bạn thực sự không thể xử lý lỗi trong hàm đã khởi tạo và sử dụng các biến đó, thì có lẽ khối thử không nên ở hàm đó, mà thay vào đó, ở đâu đó cao hơn (ví dụ, trong mã gọi hàm đó hoặc mã đó gọi mã đó ). Chỉ cần đảm bảo rằng bạn không vô tình bắt gặp một ngoại lệ bị ném ở một nơi khác và giả định sai rằng nó đã bị ném trong khi khởi tạo firstVariablesecondVariable.

Một cách tiếp cận khác là đặt mã sử dụng các biến trong khối thử. Điều này thường hợp lý. Một lần nữa, nếu các trường hợp ngoại lệ tương tự mà bạn bắt được từ trình khởi tạo của chúng cũng có thể bị ném từ mã xung quanh, bạn nên đảm bảo rằng bạn không bỏ qua khả năng đó khi xử lý chúng.

(Tôi giả sử bạn đang khởi tạo các biến có biểu thức phức tạp hơn trong ví dụ của bạn, để chúng thực sự có thể đưa ra một ngoại lệ, và bạn cũng không thực sự có kế hoạch nắm bắt tất cả các ngoại lệ có thể , nhưng chỉ để nắm bắt bất kỳ ngoại lệ cụ thể nào bạn có thể dự đoán và xử lý một cách có ý nghĩa . Đúng là thế giới thực không phải lúc nào cũng đẹp và mã sản xuất đôi khi làm điều này , nhưng vì mục tiêu của bạn ở đây là xử lý các lỗi xảy ra trong khi khởi tạo hai biến cụ thể, bất kỳ mệnh đề nào bạn viết cho cụ thể đó mục đích nên cụ thể cho bất kỳ lỗi nào.)

Cách thứ ba là trích xuất mã có thể thất bại và thử bắt xử lý nó, vào phương thức riêng của nó. Điều này hữu ích nếu bạn có thể muốn xử lý lỗi hoàn toàn trước, và sau đó không lo lắng về việc vô tình bắt một ngoại lệ phải được xử lý ở một nơi khác thay thế.

Ví dụ, giả sử rằng bạn muốn thoát khỏi ứng dụng ngay lập tức khi không thể gán một trong hai biến. (Rõ ràng không phải tất cả xử lý ngoại lệ đều là lỗi nghiêm trọng; đây chỉ là một ví dụ và có thể hoặc không phải là cách bạn muốn ứng dụng của mình phản ứng với vấn đề.) Bạn có thể như vậy:

// In real life, this should be named more descriptively.
private static (int firstValue, int secondValue) GetFirstAndSecondValues()
{
    try {
        // This code is contrived. The idea here is that obtaining the values
        // could actually fail, and throw a SomeSpecificException.
        var firstVariable = 1;
        var secondVariable = firstVariable;
        return (firstVariable, secondVariable);
    }
    catch (SomeSpecificException e) {
        Console.Error.WriteLine(e.Message);
        Environment.Exit(1);
        throw new InvalidOperationException(); // unreachable
    }
}

// ...and of course so should this.
internal static void MethodThatUsesTheValues()
{
    var (firstVariable, secondVariable) = GetFirstAndSecondValues();

    // Code that does something with them...
}

Mã đó trả về và giải mã một ValueTuple với cú pháp của C # 7.0 để trả về nhiều giá trị, nhưng nếu bạn vẫn sử dụng phiên bản C # trước đó, bạn vẫn có thể sử dụng kỹ thuật này; ví dụ: bạn có thể sử dụng các tham số hoặc trả về một đối tượng tùy chỉnh cung cấp cả hai giá trị . Hơn nữa, nếu hai biến không thực sự liên quan chặt chẽ với nhau, có lẽ tốt hơn là nên có hai phương thức riêng biệt.

Đặc biệt nếu bạn có nhiều phương pháp như vậy, bạn nên xem xét việc tập trung mã của mình để thông báo cho người dùng về các lỗi nghiêm trọng và bỏ việc. (Ví dụ, bạn có thể viết một Diephương pháp với một messagetham số.) Các throw new InvalidOperationException();dòng là không bao giờ được thực thi , do đó bạn không cần phải (và không nên) viết một điều khoản bắt buộc với nó.

Ngoài việc thoát khi xảy ra lỗi cụ thể, đôi khi bạn có thể viết mã trông như thế này nếu bạn ném ngoại lệ của loại khác bao bọc ngoại lệ ban đầu . (Trong tình huống đó, bạn sẽ không cần một biểu hiện ném thứ hai, không thể truy cập.)

Kết luận: Phạm vi chỉ là một phần của bức tranh.

Bạn có thể đạt được hiệu quả của việc bọc mã bằng xử lý lỗi mà không cần cấu trúc lại (hoặc, nếu bạn thích, hầu như không có bất kỳ cấu trúc lại nào), chỉ bằng cách tách các khai báo của biến khỏi các phép gán của chúng. Trình biên dịch cho phép điều này nếu bạn thỏa mãn các quy tắc gán xác định của C # và việc khai báo một biến trước khối thử làm cho phạm vi lớn hơn của nó rõ ràng. Nhưng tái cấu trúc hơn nữa vẫn có thể là lựa chọn tốt nhất của bạn.


"khi bạn viết mã trong một khối thử với các mệnh đề bắt, bạn đang nói với cả trình biên dịch và bất kỳ độc giả nào của con người rằng nó nên được xử lý như nó có thể không chạy được." Điều mà trình biên dịch quan tâm là điều khiển đó có thể đạt được các câu lệnh sau ngay cả khi các câu lệnh trước đó có ngoại lệ. Trình biên dịch thường giả định rằng nếu một câu lệnh đưa ra một ngoại lệ, thì câu lệnh tiếp theo sẽ không được thực thi, vì vậy một biến không được gán sẽ không được đọc. Thêm một 'bắt' sẽ cho phép kiểm soát đạt được các câu lệnh sau - đó là vấn đề bắt, không phải là liệu mã trong khối thử có ném hay không.
Pete Kirkham
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.