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 finally
khố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ạo và khô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 firstVariable
và secondVariable
trong 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 firstVariable
và secondVariable
.
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 Die
phương pháp với một message
tham 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.
try.. catch
là 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.