Tại sao goto nguy hiểm?
goto
không gây ra sự mất ổn định của chính nó. Mặc dù có khoảng 100.000 goto
giây, nhân Linux vẫn là một mô hình ổn định.
goto
tự nó không gây ra lỗ hổng bảo mật. Tuy nhiên, trong một số ngôn ngữ, việc trộn nó với try
/ catch
khối quản lý ngoại lệ có thể dẫn đến các lỗ hổng như được giải thích trong khuyến nghị CERT này . Trình biên dịch C ++ chính thống gắn cờ và ngăn chặn các lỗi như vậy, nhưng thật không may, trình biên dịch cũ hơn hoặc kỳ lạ hơn thì không.
goto
gây ra mã không thể đọc và không thể nhầm lẫn. Đây cũng được gọi là mã spaghetti , bởi vì, giống như trong một đĩa mì spaghetti, rất khó để theo dòng kiểm soát khi có quá nhiều gotos.
Ngay cả khi bạn quản lý để tránh mã spaghetti và nếu bạn chỉ sử dụng một vài gotos, chúng vẫn tạo điều kiện cho các lỗi như và rò rỉ tài nguyên:
- Mã sử dụng lập trình cấu trúc, với các khối và vòng lặp lồng nhau rõ ràng, dễ theo dõi; dòng chảy kiểm soát của nó là rất dễ đoán. Do đó, dễ dàng hơn để đảm bảo rằng bất biến được tôn trọng.
- Với một
goto
tuyên bố, bạn phá vỡ dòng chảy đơn giản đó, và phá vỡ sự mong đợi. Ví dụ, bạn có thể không nhận thấy rằng bạn vẫn còn tài nguyên miễn phí.
- Nhiều
goto
nơi khác nhau có thể đưa bạn đến một mục tiêu goto duy nhất. Vì vậy, không rõ ràng để biết chắc chắn trạng thái bạn đang ở khi đến nơi này. Do đó, rủi ro của việc đưa ra các giả định sai / vô căn cứ là khá lớn.
Thông tin bổ sung và báo giá:
C cung cấp các goto
tuyên bố và nhãn có thể sử dụng vô hạn để phân nhánh. Chính thức goto
là không bao giờ cần thiết, và trong thực tế, hầu như luôn luôn dễ dàng để viết mã mà không cần nó. (...)
Tuy nhiên, chúng tôi sẽ đề xuất một vài tình huống trong đó goto có thể tìm thấy một địa điểm. Việc sử dụng phổ biến nhất là từ bỏ xử lý trong một số cấu trúc lồng nhau sâu, chẳng hạn như thoát ra khỏi hai vòng cùng một lúc. (...)
Mặc dù chúng tôi không giáo điều về vấn đề này, nhưng có vẻ như các tuyên bố goto nên được sử dụng một cách tiết kiệm, nếu có .
Khi nào goto có thể được sử dụng?
Giống như K & R Tôi không giáo điều về gotos. Tôi thừa nhận rằng có những tình huống mà goto có thể làm dịu cuộc sống của một người.
Thông thường, trong C, goto cho phép thoát vòng lặp đa cấp hoặc xử lý lỗi yêu cầu đạt đến điểm thoát thích hợp giải phóng / giải phóng tất cả các tài nguyên được phân bổ cho đến nay (phân bổ iemultipl trong chuỗi có nghĩa là nhiều nhãn). Bài viết này định lượng việc sử dụng khác nhau của goto trong nhân Linux.
Cá nhân tôi thích tránh nó và trong 10 năm C, tôi đã sử dụng tối đa 10 gotos. Tôi thích sử dụng if
s lồng nhau , mà tôi nghĩ là dễ đọc hơn. Khi điều này sẽ dẫn đến việc lồng quá sâu, tôi sẽ chọn phân tách chức năng của mình thành các phần nhỏ hơn hoặc sử dụng chỉ báo boolean trong tầng. Trình biên dịch tối ưu hóa ngày nay đủ thông minh để tạo ra gần như cùng một mã so với cùng mã goto
.
Việc sử dụng goto phụ thuộc nhiều vào ngôn ngữ:
Trong C ++, việc sử dụng RAII đúng cách sẽ khiến trình biên dịch tự động phá hủy các đối tượng nằm ngoài phạm vi, do đó tài nguyên / khóa sẽ được làm sạch bằng mọi cách và không cần thêm goto nữa.
Trong Java không cần cho goto (xem trích dẫn tác giả của Java trên và điều này xuất sắc trả lời Stack Overflow ): thu gom rác làm sạch đống lộn xộn, break
, continue
, và try
/ catch
xử lý ngoại lệ bao gồm tất cả các trường hợp goto
có thể là hữu ích, nhưng trong một an toàn hơn và tốt hơn cách thức. Sự phổ biến của Java chứng minh rằng tuyên bố goto có thể tránh được bằng ngôn ngữ hiện đại.
Phóng to lỗ hổng SSL goto nổi tiếng
Tuyên bố miễn trừ trách nhiệm quan trọng: theo quan điểm thảo luận gay gắt trong các bình luận, tôi muốn làm rõ rằng tôi không giả vờ rằng tuyên bố goto là nguyên nhân duy nhất của lỗi này. Tôi không giả vờ rằng nếu không có goto thì sẽ không có lỗi. Tôi chỉ muốn chỉ ra rằng một goto có thể liên quan đến một lỗi nghiêm trọng.
Tôi không biết có bao nhiêu lỗi nghiêm trọng liên quan đến goto
lịch sử lập trình: chi tiết thường không được truyền đạt. Tuy nhiên, có một lỗi Apple SSL nổi tiếng làm suy yếu tính bảo mật của iOS. Tuyên bố dẫn đến lỗi này là một goto
tuyên bố sai .
Một số ý kiến cho rằng nguyên nhân sâu xa của lỗi không phải là bản thân goto, mà là bản sao / dán sai, thụt sai, thiếu dấu ngoặc nhọn quanh khối điều kiện hoặc có lẽ là thói quen làm việc của nhà phát triển. Tôi không thể xác nhận bất kỳ trong số họ: tất cả những lập luận này là những giả thuyết và giải thích có thể xảy ra. Không ai thực sự biết. ( trong khi đó, giả thuyết về sự hợp nhất đã sai khi ai đó đề xuất trong các bình luận dường như là một ứng cử viên rất tốt trong quan điểm về một số mâu thuẫn thụt đầu dòng khác trong cùng chức năng ).
Thực tế khách quan duy nhất là một nhân đôi goto
dẫn đến thoát khỏi chức năng sớm. Nhìn vào mã, chỉ một tuyên bố duy nhất có thể gây ra hiệu ứng tương tự sẽ là một sự trở lại.
Lỗi là trong chức năng SSLEncodeSignedServerKeyExchange()
trong tập tin này :
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) != 0)
goto fail;
if ((err =...) !=0)
goto fail;
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
goto fail; // <====OUCH: INDENTATION MISLEADS: THIS IS UNCONDITIONDAL!!
if (...)
goto fail;
... // Do some cryptographic operations here
fail:
... // Free resources to process error
Thật vậy, các dấu ngoặc nhọn xung quanh khối có điều kiện có thể đã ngăn được lỗi:
nó sẽ dẫn đến một lỗi cú pháp khi biên dịch (và do đó là một hiệu chỉnh) hoặc một goto vô hại dự phòng. Nhân tiện, GCC 6 sẽ có thể phát hiện ra các lỗi này nhờ cảnh báo tùy chọn để phát hiện vết lõm không nhất quán.
Nhưng ở nơi đầu tiên, tất cả các gotos này có thể tránh được với mã có cấu trúc chặt chẽ hơn. Vì vậy, goto ít nhất là gián tiếp là một nguyên nhân của lỗi này. Có ít nhất hai cách khác nhau có thể tránh được:
Phương pháp 1: nếu khoản hoặc lồng nhau if
s
Thay vì kiểm tra rất nhiều điều kiện cho lỗi liên tục và mỗi lần gửi đến fail
nhãn trong trường hợp có vấn đề, người ta có thể đã chọn thực hiện các hoạt động mã hóa theo cách if
thức chỉ thực hiện nếu không có điều kiện trước sai:
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0 &&
(err = ...) == 0 ) &&
(err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0) &&
...
(err = ...) == 0 ) )
{
... // Do some cryptographic operations here
}
... // Free resources
Cách tiếp cận 2: sử dụng bộ tích lũy lỗi
Cách tiếp cận này dựa trên thực tế là hầu hết tất cả các câu lệnh ở đây đều gọi một số hàm để đặt err
mã lỗi và chỉ thực thi phần còn lại của mã nếu err
là 0 (nghĩa là hàm được thực thi không có lỗi). Một sự thay thế an toàn và dễ đọc là:
bool ok = true;
ok = ok && (err = ReadyHash(&SSLHashSHA1, &hashCtx))) == 0;
ok = ok && (err = NextFunction(...)) == 0;
...
ok = ok && (err = ...) == 0;
... // Free resources
Ở đây, không có một goto nào: không có rủi ro để nhảy nhanh đến điểm thoát thất bại. Và trực quan sẽ dễ dàng phát hiện ra một dòng sai hoặc bị lãng quên ok &&
.
Cấu trúc này nhỏ gọn hơn. Nó dựa trên thực tế là trong C, phần thứ hai của logic và ( &&
) chỉ được đánh giá nếu phần thứ nhất là đúng. Trong thực tế, trình biên dịch được tạo bởi trình biên dịch tối ưu hóa gần như tương đương với mã gốc với gotos: Trình tối ưu hóa phát hiện rất tốt chuỗi điều kiện và tạo mã, ở giá trị trả về không null đầu tiên nhảy đến cuối ( bằng chứng trực tuyến ).
Bạn thậm chí có thể dự tính một kiểm tra tính nhất quán ở cuối chức năng có thể trong giai đoạn thử nghiệm xác định sự không khớp giữa cờ ok và mã lỗi.
assert( (ok==false && err!=0) || (ok==true && err==0) );
Những lỗi ==0
vô ý được thay thế bằng !=0
lỗi kết nối logic hoặc lỗi sẽ dễ dàng bị phát hiện trong giai đoạn gỡ lỗi.
Như đã nói: Tôi không giả vờ rằng các cấu trúc thay thế sẽ tránh được bất kỳ lỗi nào. Tôi chỉ muốn nói rằng họ có thể đã làm cho lỗi khó xảy ra hơn.