CHỈNH SỬA 20/11/2009 :
Tôi vừa mới đọc bài viết MSDN này về cải thiện hiệu suất mã được quản lý và phần này nhắc tôi về câu hỏi này:
Chi phí thực hiện của việc ném một ngoại lệ là đáng kể. Mặc dù xử lý ngoại lệ có cấu trúc là cách được khuyến nghị để xử lý các điều kiện lỗi, hãy đảm bảo rằng bạn chỉ sử dụng ngoại lệ trong các trường hợp ngoại lệ khi điều kiện lỗi xảy ra. Không sử dụng ngoại lệ cho luồng điều khiển thường xuyên.
Tất nhiên, điều này chỉ dành cho .NET và nó cũng hướng đặc biệt vào những người đang phát triển các ứng dụng hiệu suất cao (như tôi); vì vậy nó rõ ràng không phải là một sự thật phổ quát. Tuy nhiên, có rất nhiều nhà phát triển .NET của chúng tôi ngoài kia, vì vậy tôi cảm thấy điều đó đáng chú ý.
CHỈNH SỬA :
Được rồi, trước hết, hãy nói thẳng một điều: Tôi không có ý định gây gổ với bất kỳ ai về câu hỏi hiệu suất. Nói chung, trên thực tế, tôi có xu hướng đồng ý với những người tin rằng tối ưu hóa quá sớm là một tội lỗi. Tuy nhiên, hãy để tôi chỉ ra hai điểm:
Người đăng đang yêu cầu một lý do khách quan đằng sau sự khôn ngoan thông thường rằng các ngoại lệ nên được sử dụng một cách tiết kiệm. Chúng ta có thể thảo luận về khả năng đọc và thiết kế phù hợp tất cả những gì chúng ta muốn; nhưng đây là những vấn đề chủ quan với những người sẵn sàng tranh luận ở cả hai bên. Tôi nghĩ rằng người đăng nhận thức được điều này. Thực tế là việc sử dụng các ngoại lệ để kiểm soát luồng chương trình thường là một cách làm không hiệu quả. Không, không phải luôn luôn , nhưng thường xuyên . Đây là lý do tại sao lời khuyên hợp lý là sử dụng các trường hợp ngoại lệ một cách tiết kiệm, giống như lời khuyên hữu ích là ăn thịt đỏ hoặc uống rượu vang một cách tiết kiệm.
Có sự khác biệt giữa tối ưu hóa không có lý do chính đáng và viết mã hiệu quả. Hệ quả của điều này là có sự khác biệt giữa việc viết một thứ gì đó mạnh mẽ, nếu không được tối ưu hóa và một thứ gì đó đơn giản là không hiệu quả. Đôi khi tôi nghĩ khi mọi người tranh luận về những thứ như xử lý ngoại lệ, họ thực sự chỉ đang nói chuyện với nhau, bởi vì họ đang thảo luận về những điều cơ bản khác nhau.
Để minh họa quan điểm của tôi, hãy xem xét các ví dụ mã C # sau đây.
Ví dụ 1: Phát hiện đầu vào của người dùng không hợp lệ
Đây là một ví dụ về những gì tôi muốn gọi là lạm dụng ngoại lệ .
int value = -1;
string input = GetInput();
bool inputChecksOut = false;
while (!inputChecksOut) {
try {
value = int.Parse(input);
inputChecksOut = true;
} catch (FormatException) {
input = GetInput();
}
}
Mã này, đối với tôi, thật nực cười. Tất nhiên là nó hoạt động . Không ai tranh cãi với điều đó. Nhưng nó phải là một cái gì đó như:
int value = -1;
string input = GetInput();
while (!int.TryParse(input, out value)) {
input = GetInput();
}
Ví dụ 2: Kiểm tra sự tồn tại của một tệp
Tôi nghĩ kịch bản này thực sự rất phổ biến. Nó chắc chắn có vẻ "dễ chấp nhận" hơn đối với nhiều người, vì nó xử lý I / O tệp:
string text = null;
string path = GetInput();
bool inputChecksOut = false;
while (!inputChecksOut) {
try {
using (FileStream fs = new FileStream(path, FileMode.Open)) {
using (StreamReader sr = new StreamReader(fs)) {
text = sr.ReadToEnd();
}
}
inputChecksOut = true;
} catch (FileNotFoundException) {
path = GetInput();
}
}
Điều này có vẻ đủ hợp lý, phải không? Chúng tôi đang cố gắng mở một tệp; nếu nó không có ở đó, chúng tôi bắt ngoại lệ đó và cố gắng mở một tệp khác ... Điều đó có gì sai?
Không có gì thực sự. Nhưng hãy xem xét sự thay thế này, điều này không gây ra bất kỳ ngoại lệ nào:
string text = null;
string path = GetInput();
while (!File.Exists(path)) path = GetInput();
using (FileStream fs = new FileStream(path, FileMode.Open)) {
using (StreamReader sr = new StreamReader(fs)) {
text = sr.ReadToEnd();
}
}
Tất nhiên, nếu hiệu suất của hai cách tiếp cận này thực sự giống nhau, thì đây thực sự sẽ là một vấn đề hoàn toàn mang tính học thuyết. Vì vậy, chúng ta hãy xem xét. Đối với ví dụ mã đầu tiên, tôi đã tạo một danh sách gồm 10000 chuỗi ngẫu nhiên, không có chuỗi nào đại diện cho một số nguyên thích hợp, và sau đó thêm một chuỗi số nguyên hợp lệ vào cuối. Sử dụng cả hai phương pháp trên, đây là kết quả của tôi:
Sử dụng try
/ catch
khối: 25,455 giây
Sử dụng int.TryParse
: 1,637 mili giây
Đối với ví dụ thứ hai, về cơ bản tôi đã làm điều tương tự: tạo danh sách 10000 chuỗi ngẫu nhiên, không có chuỗi nào là đường dẫn hợp lệ, sau đó thêm đường dẫn hợp lệ vào cuối. Đây là kết quả:
Sử dụng try
/ catch
khối: 29,989 giây
Sử dụng File.Exists
: 22,820 mili giây
Rất nhiều người sẽ trả lời điều này bằng cách nói, "Vâng, tốt, ném và bắt 10.000 trường hợp ngoại lệ là cực kỳ phi thực tế; điều này phóng đại kết quả." Tất nhiên là thế. Sự khác biệt giữa việc đưa ra một ngoại lệ và việc tự xử lý đầu vào không hợp lệ sẽ không gây chú ý cho người dùng. Thực tế vẫn là việc sử dụng các ngoại lệ, trong hai trường hợp này, chậm hơn từ 1.000 đến hơn 10.000 lần so với các phương pháp thay thế có thể đọc được - nếu không muốn nói là hơn.
Đó là lý do tại sao tôi bao gồm ví dụ của GetNine()
phương pháp bên dưới. Nó không phải là nó intolerably chậm hoặc không thể chấp nhận chậm; nó chậm hơn mức đáng lẽ ... không vì lý do chính đáng .
Một lần nữa, đây chỉ là hai ví dụ. Tất nhiên sẽ có những lúc hiệu quả hoạt động của việc sử dụng các ngoại lệ không nghiêm trọng đến mức này (Pavel là đúng; xét cho cùng, nó phụ thuộc vào việc triển khai). Tất cả những gì tôi đang nói là: hãy đối mặt với sự thật, các bạn - trong những trường hợp như trên, ném và bắt một ngoại lệ cũng tương tự như vậy GetNine()
; đó chỉ là một cách làm không hiệu quả để làm một điều gì đó có thể dễ dàng được thực hiện tốt hơn .
Bạn đang yêu cầu một cơ sở lý luận như thể đây là một trong những tình huống mà mọi người đã nhảy vào một nhóm mà không biết tại sao. Nhưng trên thực tế, câu trả lời là hiển nhiên, và tôi nghĩ bạn cũng biết rồi. Xử lý ngoại lệ có hiệu suất khủng khiếp.
Được rồi, có thể điều đó tốt cho tình huống kinh doanh đặc biệt của bạn, nhưng nói một cách tương đối , việc ném / bắt một ngoại lệ sẽ gây tốn kém hơn mức cần thiết trong nhiều trường hợp. Bạn biết điều đó, tôi biết điều đó: hầu hết thời gian , nếu bạn đang sử dụng ngoại lệ để kiểm soát luồng chương trình, bạn chỉ đang viết mã chậm.
Bạn cũng có thể hỏi: tại sao mã này xấu?
private int GetNine() {
for (int i = 0; i < 10; i++) {
if (i == 9) return i;
}
}
Tôi dám cá rằng nếu bạn mô tả chức năng này, bạn sẽ thấy nó hoạt động khá nhanh có thể chấp nhận được đối với ứng dụng kinh doanh thông thường của bạn. Điều đó không thay đổi thực tế rằng đó là một cách kém hiệu quả khủng khiếp để hoàn thành một việc mà có thể được thực hiện tốt hơn rất nhiều.
Đó là ý của mọi người khi họ nói về "lạm dụng" ngoại lệ.