Tại sao một hàm chỉ nên có một điểm thoát? [đóng cửa]


97

Tôi đã luôn nghe nói về một hàm điểm thoát duy nhất là một cách viết mã tồi vì bạn mất khả năng đọc và hiệu quả. Tôi chưa bao giờ nghe ai tranh luận về phía bên kia.

Tôi nghĩ điều này có liên quan đến CS nhưng câu hỏi này đã bị bắn hạ tại cstheory stackexchange.



6
Câu trả lời là không có câu trả lời nào luôn đúng. Tôi thường thấy nó dễ dàng hơn để viết mã với nhiều lần thoát. Tôi cũng nhận thấy (khi cập nhật mã ở trên) rằng việc sửa đổi / mở rộng mã khó khăn hơn do có nhiều lần thoát giống nhau. Việc đưa ra các quyết định theo từng trường hợp này là công việc của chúng tôi. Khi một quyết định luôn có câu trả lời "tốt nhất", không cần chúng ta làm.
JS.

1
@finnw mods của phe phát xít đã xóa hai câu hỏi cuối cùng, để đảm bảo rằng chúng sẽ phải được trả lời lại, lặp đi lặp lại
Maarten Bodewes 19/02/12

Mặc dù có từ "tranh luận" trong câu hỏi, tôi thực sự không nghĩ đây là một câu hỏi dựa trên quan điểm. Nó khá phù hợp với thiết kế tốt, v.v. Tôi không thấy lý do gì để nó bị đóng cửa, nhưng w / e.
Ungeheuer

1
Một điểm thoát duy nhất đơn giản hóa việc gỡ lỗi, đọc, đo lường hiệu suất và điều chỉnh, cấu trúc lại, Điều này là khách quan và có ý nghĩa quan trọng. Sử dụng trả về sớm (sau khi kiểm tra đối số đơn giản) tạo ra sự pha trộn hợp lý của cả hai kiểu. Với những lợi ích của một điểm thoát duy nhất, việc rải mã của bạn với các giá trị trả về chỉ đơn giản là bằng chứng về một lập trình viên lười biếng, cẩu thả, bất cẩn - và ít nhất có thể không thích chó con.
Rick O'Shea

Câu trả lời:


108

Có nhiều trường phái tư tưởng khác nhau, và nó phần lớn phụ thuộc vào sở thích cá nhân.

Một là sẽ ít nhầm lẫn hơn nếu chỉ có một điểm thoát duy nhất - bạn có một con đường duy nhất thông qua phương pháp và bạn biết nơi để tìm lối ra. Mặt trừ nếu bạn sử dụng thụt lề để thể hiện việc lồng nhau, mã của bạn sẽ bị thụt lề ồ ạt sang phải và rất khó theo dõi tất cả các phạm vi lồng nhau.

Khác là bạn có thể kiểm tra điều kiện tiên quyết và sớm thoát vào lúc bắt đầu của một phương pháp, vì vậy mà bạn biết trong cơ thể của phương pháp mà điều kiện nhất định là đúng sự thật, nếu không có sự toàn bộ cơ thể của phương pháp đang được thụt 5 dặm về phía bên phải. Điều này thường giảm thiểu số lượng phạm vi bạn phải lo lắng, điều này làm cho mã dễ theo dõi hơn nhiều.

Thứ ba là bạn có thể thoát ra bất cứ nơi nào bạn muốn. Điều này ngày xưa còn khó hiểu hơn, nhưng bây giờ chúng ta có các trình biên dịch và trình biên dịch tô màu cú pháp phát hiện mã không thể truy cập được, việc xử lý dễ dàng hơn rất nhiều.

Tôi đang ở ngay trong trại giữa. Việc thực thi một điểm thoát duy nhất là một hạn chế vô nghĩa hoặc thậm chí phản tác dụng của IMHO, trong khi việc thoát ngẫu nhiên trên tất cả một phương thức đôi khi có thể dẫn đến tình trạng hỗn độn khó tuân theo logic, nơi mà việc xem một bit mã nhất định sẽ hay không. Thực thi. Nhưng "kiểm tra" phương pháp của bạn làm cho nó có thể đơn giản hóa đáng kể phần thân của phương pháp.


1
Việc lồng ghép sâu có thể bị xóa sổ trong singe exitmô hình bởi vô số go tocâu lệnh. Ngoài ra, người ta có cơ hội thực hiện một số xử lý hậu kỳ dưới Errornhãn cục bộ của hàm , điều này là không thể với nhiều returns.
Ant_222

2
Thường có một giải pháp tốt mà tránh phải đi đến. Tôi thích 'return (Fail (...))' và đặt mã dọn dẹp được chia sẻ vào phương thức Fail. Điều này có thể yêu cầu chuyển một số local vào để cho phép giải phóng bộ nhớ, v.v., nhưng trừ khi bạn đang sử dụng một đoạn mã quan trọng về hiệu suất thì đây thường là một giải pháp gọn gàng hơn nhiều so với goto IMO. Nó cũng cho phép một số phương pháp chia sẻ mã dọn dẹp tương tự.
Jason Williams

Có một cách tiếp cận tối ưu dựa trên các tiêu chí khách quan nhưng chúng ta có thể đồng ý rằng có những trường phái tư tưởng (đúng và sai) và nó phụ thuộc vào sở thích cá nhân (ưa thích hoặc chống lại một cách tiếp cận đúng).
Rick O'Shea

39

Khuyến nghị chung của tôi là các câu lệnh trả về, khi thực tế, nên được đặt trước mã đầu tiên có bất kỳ tác dụng phụ nào hoặc sau mã cuối cùng có bất kỳ tác dụng phụ nào. Tôi sẽ xem xét một cái gì đó như:

  if (! đối số) // Kiểm tra xem có phải không
    trả về ERR_NULL_ARGUMENT;
  ... xử lý đối số không rỗng
  nếu (được)
    trả về 0;
  khác
    trả về ERR_NOT_OK;

rõ ràng hơn:

  int return_value;
  if (đối số) // Non-null
  {
    .. xử lý đối số không rỗng
    .. đặt kết quả thích hợp
  }
  khác
    kết quả = ERR_NULL_ARGUMENT;
  trả về kết quả;

Nếu một điều kiện nhất định ngăn một hàm thực hiện bất kỳ điều gì, tôi muốn trả lại sớm khỏi hàm tại một điểm trên điểm mà hàm sẽ thực hiện bất kỳ điều gì. Tuy nhiên, một khi chức năng đã thực hiện các hành động với các tác dụng phụ, tôi muốn quay lại từ phía dưới, để làm rõ rằng tất cả các tác dụng phụ phải được xử lý.


Ví dụ đầu tiên của bạn, quản lý okbiến, giống như cách tiếp cận một lần đối với tôi. Hơn nữa, khối if-else có thể được chỉ đơn giản là viết lại để:return ok ? 0 : ERR_NOT_OK;
Melebius

2
Ví dụ đầu tiên có một returnở đầu trước tất cả mã thực hiện mọi thứ. Đối với việc sử dụng ?:toán tử, viết nó ra trên các dòng riêng biệt giúp nhiều IDE dễ dàng đính kèm điểm ngắt gỡ lỗi vào tình huống không ổn. BTW, chìa khóa thực sự của "điểm thoát duy nhất" nằm ở chỗ hiểu rằng điều quan trọng là đối với mỗi lệnh gọi cụ thể đến một chức năng bình thường, điểm thoát là điểm ngay sau cuộc gọi . Các lập trình viên ngày nay coi đó là điều hiển nhiên, nhưng mọi thứ không phải lúc nào cũng như vậy. Trong một số trường hợp hiếm hoi, mã có thể phải chạy mà không có không gian ngăn xếp, dẫn đến các hàm ...
supercat

... mà thoát qua gotos có điều kiện hoặc tính toán. Nói chung, bất kỳ thứ gì có đủ tài nguyên để được lập trình bằng bất kỳ thứ gì khác ngoài ngôn ngữ hợp ngữ sẽ có thể hỗ trợ ngăn xếp, nhưng tôi đã viết mã hợp ngữ phải hoạt động dưới một số ràng buộc rất chặt chẽ (xuống tới KHÔNG byte RAM trong một trường hợp), và có nhiều điểm thoát có thể hữu ích trong những trường hợp như vậy.
supercat

1
Ví dụ rõ ràng hơn được gọi là ít rõ ràng hơn và khó đọc. Một điểm thoát luôn dễ đọc hơn, dễ bảo trì hơn, dễ gỡ lỗi hơn.
GuidoG

8
@GuidoG: Một trong hai mẫu có thể dễ đọc hơn, tùy thuộc vào những gì xuất hiện trong các phần bị bỏ qua. Sử dụng "return x;" nói rõ rằng nếu đạt được câu lệnh, giá trị trả về sẽ là x. Sử dụng "result = x;" mở ra khả năng một cái gì đó khác có thể thay đổi kết quả trước khi nó được trả về. Điều đó có thể hữu ích nếu trên thực tế cần phải thay đổi kết quả, nhưng các lập trình viên kiểm tra mã sẽ phải kiểm tra nó để xem kết quả có thể thay đổi như thế nào ngay cả khi câu trả lời là "nó không thể".
supercat

15

Một điểm vào và ra là khái niệm ban đầu của lập trình có cấu trúc so với Mã Spaghetti từng bước. Có một niềm tin rằng nhiều hàm điểm thoát yêu cầu nhiều mã hơn vì bạn phải dọn dẹp thích hợp các không gian bộ nhớ được cấp cho các biến. Hãy xem xét một tình huống trong đó hàm phân bổ các biến (tài nguyên) và thoát khỏi hàm sớm và không được dọn dẹp thích hợp sẽ dẫn đến rò rỉ tài nguyên. Ngoài ra, việc xây dựng dọn dẹp trước mỗi lần thoát sẽ tạo ra rất nhiều mã dư thừa.


Đó thực sự không phải là vấn đề với RAII
BigSandwich

14

Với hầu hết mọi thứ, nó phụ thuộc vào nhu cầu của người có thể phân phối. Trong "ngày xưa", mã spaghetti với nhiều điểm trả về đã bị rò rỉ bộ nhớ, vì những người viết mã ưa thích phương pháp đó thường không dọn dẹp tốt. Cũng có vấn đề với một số trình biên dịch "mất" tham chiếu đến biến trả về khi ngăn xếp được bật lên trong quá trình trả về, trong trường hợp trả về từ phạm vi lồng nhau. Vấn đề chung hơn là một trong những mã tham gia lại, mã này cố gắng để trạng thái gọi của một hàm giống hệt như trạng thái trả về của nó. Những kẻ đột biến của oop đã vi phạm điều này và khái niệm này đã bị xếp dỡ.

Có các phân phối, đặc biệt nhất là các nhân, cần tốc độ mà nhiều điểm thoát cung cấp. Những môi trường này thường có bộ nhớ và quản lý quy trình riêng của chúng, do đó nguy cơ rò rỉ được giảm thiểu.

Cá nhân tôi thích có một điểm thoát duy nhất, vì tôi thường sử dụng nó để chèn một điểm ngắt trên câu lệnh trả về và thực hiện kiểm tra mã về cách mã xác định giải pháp đó. Tôi chỉ có thể đi đến lối vào và bước qua, điều mà tôi làm với các giải pháp đệ quy và lồng ghép rộng rãi. Là một người đánh giá mã, nhiều lần trả về trong một hàm đòi hỏi phải phân tích sâu hơn nhiều - vì vậy nếu bạn đang làm điều đó để tăng tốc độ triển khai, bạn đang cướp Peter để cứu Paul. Sẽ cần thêm thời gian để xem xét mã, làm mất hiệu lực giả định về việc triển khai hiệu quả.

- 2 xu

Vui lòng xem tài liệu này để biết thêm chi tiết: NISTIR 5459


8
multiple returns in a function requires a much deeper analysischỉ khi chức năng là rất lớn rồi (> 1 màn hình), nếu không nó làm cho việc phân tích dễ dàng hơn
dss539

2
nhiều lợi nhuận không bao giờ làm cho việc phân tích dễ dàng hơn, chỉ là đối diện
GuidoG

1
liên kết đã chết (404).
fusi

1
@fusi - tìm thấy nó trên archive.org và cập nhật các liên kết ở đây
sscheider

4

Theo quan điểm của tôi, lời khuyên thoát khỏi một chức năng (hoặc cấu trúc kiểm soát khác) tại một thời điểm thường bị bán quá mức. Hai lý do thường được đưa ra để thoát chỉ tại một điểm:

  1. Mã một lần thoát được cho là dễ đọc và gỡ lỗi hơn. (Tôi thừa nhận rằng tôi không nghĩ nhiều về lý do này, nhưng nó đã được đưa ra. Điều về cơ bản dễ đọc và gỡ lỗi hơn đáng kể là mã mục nhập một lần .)
  2. Các liên kết mã một lần thoát và trả về rõ ràng hơn.

Lý do thứ hai là tinh tế và có một số giá trị, đặc biệt nếu hàm trả về một cấu trúc dữ liệu lớn. Tuy nhiên, tôi sẽ không lo lắng về điều đó quá nhiều, ngoại trừ ...

Nếu là học sinh, bạn muốn đạt điểm cao nhất trong lớp của mình. Làm những gì người hướng dẫn thích. Anh ấy có lẽ có lý do chính đáng từ quan điểm của mình; vì vậy, ít nhất, bạn sẽ học được quan điểm của anh ấy. Điều này tự nó có giá trị.

Chúc may mắn.


4

Tôi từng là người ủng hộ phong cách một lối thoát. Lý do của tôi chủ yếu đến từ nỗi đau ...

Một lần thoát dễ gỡ lỗi hơn.

Với các kỹ thuật và công cụ mà chúng ta có ngày nay, đây là một vị trí kém hợp lý hơn nhiều để thực hiện vì các bài kiểm tra đơn vị và ghi nhật ký có thể khiến việc thoát một lần trở nên không cần thiết. Điều đó nói lên rằng, khi bạn cần xem mã thực thi trong trình gỡ lỗi, việc hiểu và làm việc với mã chứa nhiều điểm thoát sẽ khó hơn nhiều.

Điều này đặc biệt đúng khi bạn cần xen vào các bài tập để kiểm tra trạng thái (được thay thế bằng biểu thức đồng hồ trong trình gỡ lỗi hiện đại). Cũng quá dễ dàng để thay đổi luồng điều khiển theo những cách che giấu sự cố hoặc phá vỡ hoàn toàn việc thực thi.

Các phương thức một lần thoát dễ dàng hơn trong trình gỡ lỗi và dễ dàng tách rời mà không phá vỡ logic.


0

Câu trả lời là rất phụ thuộc vào ngữ cảnh. Nếu bạn đang tạo GUI và có một chức năng khởi chạy API và mở các cửa sổ ở đầu máy chính của bạn, nó sẽ có đầy các lệnh gọi có thể gây ra lỗi, mỗi lệnh sẽ khiến phiên bản của chương trình bị đóng. Nếu bạn sử dụng các câu lệnh IF lồng nhau và thụt lề, mã của bạn có thể nhanh chóng bị lệch sang phải. Quay lại lỗi ở mỗi giai đoạn có thể tốt hơn và thực sự dễ đọc hơn trong khi dễ gỡ lỗi bằng một vài cờ trong mã.

Tuy nhiên, nếu bạn đang thử nghiệm các điều kiện khác nhau và trả về các giá trị khác nhau tùy thuộc vào kết quả trong phương pháp của bạn, thì có thể tốt hơn nhiều để có một điểm thoát duy nhất. Tôi đã từng làm việc trên các tập lệnh xử lý hình ảnh trong MATLAB có thể rất lớn. Nhiều điểm thoát có thể làm cho mã cực kỳ khó theo dõi. Câu lệnh chuyển đổi thích hợp hơn nhiều.

Điều tốt nhất nên làm là học khi bạn tiếp tục. Nếu bạn đang viết mã cho một thứ gì đó, hãy thử tìm mã của người khác và xem cách họ triển khai nó. Quyết định bit nào bạn thích và bit nào bạn không.


-5

Nếu bạn cảm thấy như bạn cần nhiều điểm thoát trong một hàm, thì hàm đó quá lớn và đang hoạt động quá nhiều.

Tôi khuyên bạn nên đọc chương về các chức năng trong cuốn sách Clean Code của Robert C. Martin.

Về cơ bản, bạn nên cố gắng viết các hàm với 4 dòng mã hoặc ít hơn.

Một số ghi chú từ Blog của Mike Long :

  • Quy tắc đầu tiên của các chức năng: chúng phải nhỏ
  • Quy tắc thứ hai của các hàm: chúng phải nhỏ hơn
  • Các khối trong câu lệnh if, câu lệnh while, vòng lặp for, v.v. phải dài một dòng
  • … Và dòng mã đó thường sẽ là một lệnh gọi hàm
  • Không nên có nhiều hơn một hoặc có thể hai mức độ thụt lề
  • Các hàm nên làm một việc
  • Tất cả các câu lệnh hàm phải ở cùng một mức trừu tượng
  • Một hàm không được có nhiều hơn 3 đối số
  • Các đối số đầu ra là một mùi mã
  • Việc chuyển một cờ boolean vào một hàm thực sự rất tệ. Theo định nghĩa, bạn đang thực hiện hai - điều trong hàm.
  • Tác dụng phụ là dối trá.

29
4 dòng? Bạn viết mã gì cho phép bạn đơn giản như vậy? Tôi thực sự nghi ngờ rằng hạt nhân Linux hoặc git chẳng hạn làm được điều đó.
shinzou

7
"Truyền một cờ boolean vào một hàm thực sự rất tệ. Theo định nghĩa, bạn đang thực hiện hai - điều trong hàm." Theo định nghĩa? Không ... boolean đó chỉ có thể ảnh hưởng đến một trong bốn dòng của bạn. Ngoài ra, mặc dù tôi đồng ý với việc giữ kích thước hàm nhỏ, nhưng bốn là hơi quá hạn chế. Đây nên được coi là một hướng dẫn rất lỏng lẻo.
Jesse

12
Thêm các hạn chế như thế này chắc chắn sẽ tạo ra mã khó hiểu. Nó thiên về việc các phương pháp phải ngắn gọn rõ ràng và chỉ làm theo những gì chúng phải làm mà không có tác dụng phụ không cần thiết.
Jesse

10
Một trong những câu trả lời hiếm hoi trên SO mà tôi ước mình có thể phản đối nhiều lần.
Steven Rands

8
Thật không may, câu trả lời này đang làm nhiều thứ và có lẽ cần được chia thành nhiều thứ khác — tất cả đều ít hơn bốn dòng.
Eli
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.