Có lý do chính đáng tại sao đó là một cách thực hành tốt hơn khi chỉ có một câu lệnh return trong một hàm không?
Hoặc có thể trả về từ một hàm ngay khi nó đúng về mặt logic để làm như vậy, có nghĩa là có thể có nhiều câu lệnh return trong hàm?
Có lý do chính đáng tại sao đó là một cách thực hành tốt hơn khi chỉ có một câu lệnh return trong một hàm không?
Hoặc có thể trả về từ một hàm ngay khi nó đúng về mặt logic để làm như vậy, có nghĩa là có thể có nhiều câu lệnh return trong hàm?
Câu trả lời:
Tôi thường có một vài câu khi bắt đầu một phương thức để trả về các tình huống "dễ dàng". Ví dụ: cái này:
public void DoStuff(Foo foo)
{
if (foo != null)
{
...
}
}
... có thể dễ đọc hơn (IMHO) như thế này:
public void DoStuff(Foo foo)
{
if (foo == null) return;
...
}
Vì vậy, có, tôi nghĩ sẽ tốt khi có nhiều "điểm thoát" từ một hàm / phương thức.
DoStuff() { DoStuffInner(); IncreaseStuffCallCounter(); }
Không ai đã đề cập hoặc trích dẫn Hoàn thành mã vì vậy tôi sẽ làm điều đó.
Giảm thiểu số lượng lợi nhuận trong mỗi thói quen . Thật khó để hiểu một thói quen nếu đọc nó ở phía dưới, bạn không biết về khả năng nó trở lại ở đâu đó phía trên.
Sử dụng trở lại khi nó tăng cường khả năng đọc . Trong một số thói quen nhất định, một khi bạn biết câu trả lời, bạn muốn đưa nó trở lại thói quen gọi ngay lập tức. Nếu thường trình được định nghĩa theo cách mà nó không yêu cầu dọn dẹp, không quay lại ngay có nghĩa là bạn phải viết thêm mã.
Tôi sẽ nói rằng sẽ rất khó khôn ngoan khi quyết định tùy tiện đối với nhiều điểm thoát vì tôi đã thấy kỹ thuật này hữu ích trong thực tế nhiều lần , trên thực tế tôi thường tái cấu trúc mã hiện tại thành nhiều điểm thoát cho rõ ràng. Chúng ta có thể so sánh hai cách tiếp cận như vậy: -
string fooBar(string s, int? i) {
string ret = "";
if(!string.IsNullOrEmpty(s) && i != null) {
var res = someFunction(s, i);
bool passed = true;
foreach(var r in res) {
if(!r.Passed) {
passed = false;
break;
}
}
if(passed) {
// Rest of code...
}
}
return ret;
}
So sánh mã này với mã nơi nhiều điểm thoát được cho phép: -
string fooBar(string s, int? i) {
var ret = "";
if(string.IsNullOrEmpty(s) || i == null) return null;
var res = someFunction(s, i);
foreach(var r in res) {
if(!r.Passed) return null;
}
// Rest of code...
return ret;
}
Tôi nghĩ rằng sau này là rõ ràng hơn đáng kể. Theo như tôi có thể nói những lời chỉ trích về nhiều điểm thoát là một quan điểm khá cổ xưa ngày nay.
Tôi hiện đang làm việc trên một cơ sở mã, trong đó hai trong số những người làm việc trên nó đăng ký một cách mù quáng theo lý thuyết "một điểm thoát" và tôi có thể nói với bạn rằng từ kinh nghiệm, đó là một thực tế khủng khiếp. Nó làm cho mã cực kỳ khó bảo trì và tôi sẽ chỉ cho bạn lý do tại sao.
Với lý thuyết "một điểm thoát duy nhất", chắc chắn bạn sẽ kết thúc với mã trông như thế này:
function()
{
HRESULT error = S_OK;
if(SUCCEEDED(Operation1()))
{
if(SUCCEEDED(Operation2()))
{
if(SUCCEEDED(Operation3()))
{
if(SUCCEEDED(Operation4()))
{
}
else
{
error = OPERATION4FAILED;
}
}
else
{
error = OPERATION3FAILED;
}
}
else
{
error = OPERATION2FAILED;
}
}
else
{
error = OPERATION1FAILED;
}
return error;
}
Điều này không chỉ làm cho mã rất khó theo dõi, mà bây giờ nói sau này bạn cần quay lại và thêm một thao tác trong khoảng từ 1 đến 2. Bạn phải thụt lề về toàn bộ chức năng kỳ dị và may mắn đảm bảo tất cả điều kiện if / other và niềng răng của bạn được kết hợp đúng.
Phương pháp này làm cho việc bảo trì mã vô cùng khó khăn và dễ bị lỗi.
Lập trình có cấu trúc nói rằng bạn chỉ nên có một câu lệnh return cho mỗi hàm. Điều này là để hạn chế sự phức tạp. Nhiều người như Martin Fowler cho rằng việc viết các hàm với nhiều câu lệnh return trở nên đơn giản hơn. Ông trình bày lập luận này trong cuốn sách tái cấu trúc cổ điển mà ông đã viết. Điều này hoạt động tốt nếu bạn làm theo lời khuyên khác của anh ấy và viết các chức năng nhỏ. Tôi đồng ý với quan điểm này và chỉ những người theo chủ nghĩa lập trình có cấu trúc chặt chẽ mới tuân thủ các câu trả về duy nhất cho mỗi hàm.
GOTO
để di chuyển luồng điều khiển ngay cả khi chức năng tồn tại. Nó không bao giờ nói "không bao giờ sử dụng GOTO
".
Như Kent Beck lưu ý khi thảo luận về các mệnh đề bảo vệ trong Các mẫu thực hiện tạo một thói quen có một điểm vào và thoát duy nhất ...
. với các phương pháp nhỏ và chủ yếu là dữ liệu cục bộ, nó không cần thiết phải bảo thủ. "
Tôi tìm thấy một hàm được viết bằng các mệnh đề bảo vệ dễ theo dõi hơn nhiều so với một if then else
câu lệnh lồng nhau dài .
Trong một chức năng không có tác dụng phụ, không có lý do chính đáng nào để có nhiều hơn một lần trả lại và bạn nên viết chúng theo kiểu chức năng. Trong một phương thức có tác dụng phụ, mọi thứ sẽ tuần tự hơn (được lập chỉ mục theo thời gian), vì vậy bạn viết theo kiểu bắt buộc, sử dụng câu lệnh return làm lệnh để dừng thực thi.
Nói cách khác, khi có thể, hãy ủng hộ phong cách này
return a > 0 ?
positively(a):
negatively(a);
trên này
if (a > 0)
return positively(a);
else
return negatively(a);
Nếu bạn thấy mình viết một vài lớp điều kiện lồng nhau, có lẽ có một cách bạn có thể cấu trúc lại, bằng cách sử dụng danh sách vị ngữ chẳng hạn. Nếu bạn thấy rằng if và elses của bạn cách xa nhau về mặt cú pháp, bạn có thể muốn chia nó thành các chức năng nhỏ hơn. Một khối điều kiện kéo dài hơn một màn hình văn bản rất khó đọc.
Không có quy tắc cứng và nhanh nào áp dụng cho mọi ngôn ngữ. Một cái gì đó giống như có một tuyên bố trả lại duy nhất sẽ không làm cho mã của bạn tốt. Nhưng mã tốt sẽ có xu hướng cho phép bạn viết các chức năng của mình theo cách đó.
Tôi đã thấy nó trong các tiêu chuẩn mã hóa cho C ++, bị treo từ C, vì nếu bạn không có RAII hoặc quản lý bộ nhớ tự động khác thì bạn phải dọn sạch cho mỗi lần trả lại, có nghĩa là cắt và dán của việc dọn dẹp hoặc một goto (về mặt logic giống như 'cuối cùng' trong các ngôn ngữ được quản lý), cả hai đều được coi là hình thức xấu. Nếu thực tiễn của bạn là sử dụng các con trỏ và bộ sưu tập thông minh trong C ++ hoặc một hệ thống bộ nhớ tự động khác, thì đó không phải là lý do mạnh mẽ cho nó, và nó trở thành tất cả về khả năng đọc và nhiều hơn một cuộc gọi phán xét.
auto_ptr
, bạn có thể sử dụng các con trỏ đơn giản song song. Mặc dù sẽ rất kỳ quặc khi viết mã 'được tối ưu hóa' với trình biên dịch không tối ưu hóa ngay từ đầu.
try
... finally
trong Java) và bạn cần bảo trì tài nguyên bạn có thể làm với một trở lại vào cuối của một phương thức. Trước khi bạn làm điều này, bạn nên nghiêm túc xem xét việc tái cấu trúc mã để thoát khỏi tình huống.
Tôi nghiêng về ý tưởng rằng các câu lệnh return ở giữa hàm là xấu. Bạn có thể sử dụng trả về để xây dựng một vài mệnh đề bảo vệ ở đầu hàm và dĩ nhiên nói cho trình biên dịch biết cái gì sẽ trả về ở cuối hàm mà không gặp vấn đề gì, nhưng trả về ở giữa hàm có thể dễ bị bỏ lỡ và có thể làm cho chức năng khó diễn giải hơn.
Có lý do chính đáng tại sao đó là một cách thực hành tốt hơn khi chỉ có một câu lệnh return trong một hàm không?
Vâng , có:
Câu hỏi thường được đặt ra như một sự phân đôi sai giữa nhiều câu trả lời hoặc được lồng sâu nếu các câu lệnh. Hầu như luôn luôn có một giải pháp thứ ba rất tuyến tính (không làm tổ sâu) chỉ với một điểm thoát duy nhất.
Cập nhật : Rõ ràng hướng dẫn MISRA cũng thúc đẩy lối thoát đơn .
Để rõ ràng, tôi không nói rằng luôn luôn sai khi có nhiều lợi nhuận. Nhưng đưa ra các giải pháp tương đương khác, có rất nhiều lý do tốt để thích một giải pháp duy nhất.
Contract.Ensures
với nhiều điểm trả về.
goto
để lấy mã dọn dẹp chung, thì có lẽ bạn đã đơn giản hóa chức năng để có một mã return
ở cuối mã dọn dẹp. Vì vậy, bạn có thể nói rằng bạn đã giải quyết vấn đề với goto
, nhưng tôi muốn nói rằng bạn đã giải quyết nó bằng cách đơn giản hóa thành một return
.
Có một điểm thoát duy nhất cung cấp một lợi thế trong việc gỡ lỗi, bởi vì nó cho phép bạn đặt một điểm dừng duy nhất ở cuối hàm để xem giá trị nào thực sự sẽ được trả về.
Nói chung, tôi cố gắng chỉ có một điểm thoát duy nhất từ một chức năng. Tuy nhiên, đôi khi, làm như vậy thực sự kết thúc việc tạo ra một cơ thể chức năng phức tạp hơn mức cần thiết, trong trường hợp đó tốt hơn là có nhiều điểm thoát. Nó thực sự phải là một "cuộc gọi phán xét" dựa trên mức độ phức tạp, nhưng mục tiêu phải là càng ít điểm thoát càng tốt mà không làm mất đi sự phức tạp và dễ hiểu.
Không, bởi vì chúng ta không còn sống trong những năm 1970 nữa . Nếu chức năng của bạn đủ dài để nhiều lần trả về là một vấn đề, thì nó quá dài.
(Khác với thực tế là bất kỳ chức năng đa dòng nào trong một ngôn ngữ có ngoại lệ đều có nhiều điểm thoát.)
Sở thích của tôi sẽ là cho một lối thoát duy nhất trừ khi nó thực sự làm phức tạp mọi thứ. Tôi đã thấy rằng trong một số trường hợp, nhiều điểm tồn tại có thể che giấu các vấn đề thiết kế quan trọng khác:
public void DoStuff(Foo foo)
{
if (foo == null) return;
}
Khi thấy mã này, tôi sẽ hỏi ngay:
Tùy thuộc vào câu trả lời cho những câu hỏi này mà có thể là
Trong cả hai trường hợp trên, mã có thể được làm lại với một xác nhận để đảm bảo rằng 'foo' không bao giờ là null và những người gọi có liên quan đã thay đổi.
Có hai lý do khác (tôi nghĩ cụ thể đối với mã C ++) khi nhiều tồn tại thực sự có thể có ảnh hưởng tiêu cực . Chúng là kích thước mã và tối ưu hóa trình biên dịch.
Một đối tượng không phải POD C ++ trong phạm vi ở lối ra của hàm sẽ có hàm hủy của nó được gọi. Trong trường hợp có một vài câu lệnh return, có thể có trường hợp có các đối tượng khác nhau trong phạm vi và do đó, danh sách các hàm hủy sẽ gọi sẽ khác nhau. Do đó trình biên dịch cần tạo mã cho mỗi câu lệnh return:
void foo (int i, int j) {
A a;
if (i > 0) {
B b;
return ; // Call dtor for 'b' followed by 'a'
}
if (i == j) {
C c;
B b;
return ; // Call dtor for 'b', 'c' and then 'a'
}
return 'a' // Call dtor for 'a'
}
Nếu kích thước mã là một vấn đề - thì đây có thể là điều đáng để tránh.
Vấn đề khác liên quan đến "Tối ưu hóa giá trị trả về được đặt tên" (còn gọi là Sao chép Elision, ISO C ++ '03 12.8 / 15). C ++ cho phép thực hiện bỏ qua việc gọi hàm tạo sao chép nếu có thể:
A foo () {
A a1;
// do something
return a1;
}
void bar () {
A a2 ( foo() );
}
Chỉ cần lấy mã như hiện tại, đối tượng 'a1' được xây dựng trong 'foo' và sau đó cấu trúc sao chép của nó sẽ được gọi để xây dựng 'a2'. Tuy nhiên, sao chép bản sao cho phép trình biên dịch xây dựng 'a1' ở cùng một vị trí trên ngăn xếp là 'a2'. Do đó, không cần phải "sao chép" đối tượng khi hàm trả về.
Nhiều điểm thoát làm phức tạp công việc của trình biên dịch khi cố gắng phát hiện điều này và ít nhất là đối với phiên bản tương đối gần đây của VC ++, việc tối ưu hóa không diễn ra khi cơ thể hàm có nhiều trả về. Xem Tối ưu hóa giá trị trả về được đặt tên trong Visual C ++ 2005 để biết thêm chi tiết.
throw new ArgumentNullException()
trong C # trong trường hợp này), tôi thực sự thích những cân nhắc khác của bạn, tất cả đều hợp lệ với tôi và có thể rất quan trọng trong một số bối cảnh thích hợp.
foo
đang được thử nghiệm không liên quan gì đến chủ đề này, đó là liệu có nên làm if (foo == NULL) return; dowork;
hay khôngif (foo != NULL) { dowork; }
Có một điểm thoát duy nhất làm giảm Độ phức tạp theo chu kỳ và do đó, theo lý thuyết , sẽ giảm khả năng bạn sẽ đưa các lỗi vào mã của mình khi bạn thay đổi nó. Tuy nhiên, thực tế có xu hướng đề xuất rằng một cách tiếp cận thực tế hơn là cần thiết. Do đó, tôi có xu hướng nhắm đến một điểm thoát duy nhất, nhưng cho phép mã của tôi có một vài điểm nếu điều đó dễ đọc hơn.
Tôi buộc bản thân chỉ sử dụng một return
tuyên bố, vì nó sẽ tạo ra mùi mã. Hãy để tôi giải thích:
function isCorrect($param1, $param2, $param3) {
$toret = false;
if ($param1 != $param2) {
if ($param1 == ($param3 * 2)) {
if ($param2 == ($param3 / 3)) {
$toret = true;
} else {
$error = 'Error 3';
}
} else {
$error = 'Error 2';
}
} else {
$error = 'Error 1';
}
return $toret;
}
(Các điều kiện là đơn giản ...)
Càng nhiều điều kiện, chức năng càng lớn thì càng khó đọc. Vì vậy, nếu bạn cảm nhận được mùi mã, bạn sẽ nhận ra nó và muốn cấu trúc lại mã. Hai giải pháp có thể là:
Nhiều lợi nhuận
function isCorrect($param1, $param2, $param3) {
if ($param1 == $param2) { $error = 'Error 1'; return false; }
if ($param1 != ($param3 * 2)) { $error = 'Error 2'; return false; }
if ($param2 != ($param3 / 3)) { $error = 'Error 3'; return false; }
return true;
}
Chức năng riêng biệt
function isEqual($param1, $param2) {
return $param1 == $param2;
}
function isDouble($param1, $param2) {
return $param1 == ($param2 * 2);
}
function isThird($param1, $param2) {
return $param1 == ($param2 / 3);
}
function isCorrect($param1, $param2, $param3) {
return !isEqual($param1, $param2)
&& isDouble($param1, $param3)
&& isThird($param2, $param3);
}
Cấp, nó dài hơn và hơi lộn xộn, nhưng trong quá trình tái cấu trúc chức năng theo cách này, chúng tôi đã
Tôi muốn nói rằng bạn nên có nhiều như yêu cầu, hoặc bất kỳ điều gì làm cho mã sạch hơn (như các mệnh đề bảo vệ ).
Cá nhân tôi chưa bao giờ nghe / thấy bất kỳ "thực hành tốt nhất" nào nói rằng bạn chỉ nên có một tuyên bố trở lại.
Đối với hầu hết các phần, tôi có xu hướng thoát khỏi một chức năng càng sớm càng tốt dựa trên một đường dẫn logic (mệnh đề bảo vệ là một ví dụ tuyệt vời về điều này).
Tôi tin rằng nhiều lợi nhuận thường là tốt (trong mã mà tôi viết bằng C #). Kiểu trả về một lần là một sự tiếp quản từ C. Nhưng có lẽ bạn không mã hóa bằng C.
Không có luật chỉ yêu cầu một điểm thoát cho một phương thức trong tất cả các ngôn ngữ lập trình . Một số người nhấn mạnh vào tính ưu việt của phong cách này, và đôi khi họ nâng nó thành "quy tắc" hoặc "luật" nhưng niềm tin này không được hỗ trợ bởi bất kỳ bằng chứng hay nghiên cứu nào.
Nhiều kiểu trả về có thể là một thói quen xấu trong mã C, trong đó các tài nguyên phải được phân bổ rõ ràng, nhưng các ngôn ngữ như Java, C #, Python hoặc JavaScript có các cấu trúc như bộ sưu tập và try..finally
khối rác tự động (và using
các khối trong C # ) và đối số này không được áp dụng - trong các ngôn ngữ này, rất hiếm khi cần xử lý tài nguyên thủ công tập trung.
Có những trường hợp một lợi nhuận duy nhất dễ đọc hơn và những trường hợp không trả lại. Xem nếu nó làm giảm số lượng dòng mã, làm cho logic rõ ràng hơn hoặc giảm số lượng dấu ngoặc và thụt lề hoặc các biến tạm thời.
Do đó, sử dụng càng nhiều lợi nhuận càng phù hợp với sự nhạy cảm nghệ thuật của bạn, bởi vì đây là vấn đề về bố cục và khả năng đọc, không phải là vấn đề kỹ thuật.
Tôi đã nói về điều này ở độ dài lớn hơn trên blog của tôi .
Có những điều tốt để nói về việc có một điểm thoát duy nhất, giống như có những điều không tốt để nói về lập trình "mũi tên" không thể tránh khỏi mà kết quả.
Nếu sử dụng nhiều điểm thoát trong quá trình xác thực đầu vào hoặc phân bổ tài nguyên, tôi cố gắng đặt tất cả các 'lỗi thoát' ở trên cùng của hàm.
Cả bài viết Lập trình Spartan của "SSDSLPedia" và bài viết điểm thoát chức năng duy nhất của "Wiki của Kho lưu trữ mẫu Portland" đều có một số tranh luận sâu sắc xung quanh vấn đề này. Ngoài ra, tất nhiên, có bài này để xem xét.
Ví dụ, nếu bạn thực sự muốn một điểm thoát (trong bất kỳ ngôn ngữ không có ngoại lệ nào) để phát hành tài nguyên ở một nơi duy nhất, tôi thấy ứng dụng cẩn thận của goto là tốt; xem ví dụ ví dụ khá giả định này (được nén để lưu màn hình bất động sản):
int f(int y) {
int value = -1;
void *data = NULL;
if (y < 0)
goto clean;
if ((data = malloc(123)) == NULL)
goto clean;
/* More code */
value = 1;
clean:
free(data);
return value;
}
Cá nhân tôi, nói chung, không thích lập trình mũi tên hơn tôi không thích nhiều điểm thoát, mặc dù cả hai đều hữu ích khi được áp dụng đúng. Tất nhiên, tốt nhất là cấu trúc chương trình của bạn để không yêu cầu. Chia nhỏ chức năng của bạn thành nhiều phần thường giúp :)
Mặc dù khi làm như vậy, tôi thấy rằng cuối cùng tôi cũng có nhiều điểm thoát như trong ví dụ này, trong đó một số hàm lớn hơn đã được chia thành một số hàm nhỏ hơn:
int g(int y) {
value = 0;
if ((value = g0(y, value)) == -1)
return -1;
if ((value = g1(y, value)) == -1)
return -1;
return g2(y, value);
}
Tùy thuộc vào dự án hoặc hướng dẫn mã hóa, hầu hết mã của nồi hơi có thể được thay thế bằng macro. Là một lưu ý phụ, phá vỡ nó theo cách này làm cho các chức năng g0, g1, g2 rất dễ dàng để kiểm tra riêng lẻ.
Rõ ràng, trong một ngôn ngữ OO và ngôn ngữ kích hoạt ngoại lệ, tôi sẽ không sử dụng các câu lệnh if như thế (hoặc hoàn toàn, nếu tôi có thể thoát khỏi nó với một chút nỗ lực), và mã sẽ đơn giản hơn nhiều. Và không mũi tên. Và hầu hết lợi nhuận không phải là cuối cùng có thể là ngoại lệ.
Nói ngắn gọn;
Bạn biết câu ngạn ngữ - vẻ đẹp là trong mắt của kẻ si tình .
Một số người chửi bới NetBeans và một số bởi IntelliJ IDEA , một số bởi Python và một số bởi PHP .
Trong một số cửa hàng, bạn có thể mất việc nếu bạn khăng khăng làm việc này:
public void hello()
{
if (....)
{
....
}
}
Câu hỏi là tất cả về tầm nhìn và khả năng bảo trì.
Tôi nghiện sử dụng đại số boolean để giảm và đơn giản hóa logic và sử dụng các máy trạng thái. Tuy nhiên, có những đồng nghiệp trong quá khứ tin rằng việc tôi sử dụng "các kỹ thuật toán học" trong mã hóa là không phù hợp, bởi vì nó sẽ không thể nhìn thấy và duy trì được. Và đó sẽ là một thực hành tồi. Xin lỗi mọi người, các kỹ thuật tôi sử dụng rất dễ thấy và có thể duy trì được - bởi vì khi tôi trở lại mã sáu tháng sau, tôi sẽ hiểu mã rõ ràng hơn là nhìn thấy một mớ mì spaghetti tục ngữ.
Này anh bạn (giống như một khách hàng cũ từng nói) làm những gì bạn muốn miễn là bạn biết cách sửa nó khi tôi cần bạn sửa nó.
Tôi nhớ 20 năm trước, một đồng nghiệp của tôi đã bị sa thải vì sử dụng chiến lược phát triển nhanh ngày nay . Ông đã có một kế hoạch gia tăng tỉ mỉ. Nhưng người quản lý của anh ta đã hét vào mặt anh ta "Bạn không thể tăng dần các tính năng cho người dùng! Bạn phải gắn bó với thác nước ." Phản ứng của ông với người quản lý là sự phát triển gia tăng sẽ chính xác hơn với nhu cầu của khách hàng. Ông tin vào việc phát triển cho nhu cầu của khách hàng, nhưng người quản lý tin vào mã hóa theo "yêu cầu của khách hàng".
Chúng tôi thường xuyên phạm tội vì phá vỡ chuẩn hóa dữ liệu, ranh giới MVP và MVC . Chúng tôi nội tuyến thay vì xây dựng một chức năng. Chúng tôi đi đường tắt.
Cá nhân, tôi tin rằng PHP là thực tiễn tồi, nhưng tôi biết gì. Tất cả các lý luận lý thuyết tập trung vào việc cố gắng hoàn thành một bộ quy tắc
chất lượng = độ chính xác, khả năng duy trì và lợi nhuận.
Tất cả các quy tắc khác mờ dần vào nền. Và tất nhiên quy tắc này không bao giờ mất dần:
Lười biếng là đức tính của một lập trình viên giỏi.
Tôi nghiêng về phía sử dụng các mệnh đề bảo vệ để quay lại sớm và thoát ra khi kết thúc một phương thức. Quy tắc nhập và thoát đơn có ý nghĩa lịch sử và đặc biệt hữu ích khi xử lý mã kế thừa chạy tới 10 trang A4 cho một phương thức C ++ duy nhất có nhiều trả về (và nhiều lỗi). Gần đây, thực hành tốt được chấp nhận là giữ cho các phương thức nhỏ làm cho nhiều lối thoát ít cản trở sự hiểu biết. Trong ví dụ Kronoz sau được sao chép từ phía trên, câu hỏi là điều gì xảy ra trong // Phần còn lại của mã ... ?:
void string fooBar(string s, int? i) {
if(string.IsNullOrEmpty(s) || i == null) return null;
var res = someFunction(s, i);
foreach(var r in res) {
if(!r.Passed) return null;
}
// Rest of code...
return ret;
}
Tôi nhận ra ví dụ này hơi khó hiểu nhưng tôi sẽ cố gắng cấu trúc lại vòng lặp foreach thành một câu lệnh LINQ mà sau đó có thể được coi là một mệnh đề bảo vệ. Một lần nữa, trong một ví dụ contrived mục đích của mã này là không rõ ràng và someFunction () có thể có một số tác dụng phụ khác hoặc kết quả có thể được sử dụng trong // Phần còn lại của mã ... .
if (string.IsNullOrEmpty(s) || i == null) return null;
if (someFunction(s, i).Any(r => !r.Passed)) return null;
Đưa ra chức năng tái cấu trúc sau:
void string fooBar(string s, int? i) {
if (string.IsNullOrEmpty(s) || i == null) return null;
if (someFunction(s, i).Any(r => !r.Passed)) return null;
// Rest of code...
return ret;
}
null
thay vì ném một ngoại lệ cho thấy rằng đối số không được chấp nhận?
Một lý do tốt mà tôi có thể nghĩ đến là để bảo trì mã: bạn có một điểm thoát duy nhất. Nếu bạn muốn thay đổi định dạng của kết quả, ..., việc thực hiện sẽ đơn giản hơn nhiều. Ngoài ra, để gỡ lỗi, bạn chỉ cần đặt một điểm dừng ở đó :)
Phải nói rằng, tôi đã từng phải làm việc trong một thư viện nơi các tiêu chuẩn mã hóa áp đặt 'một tuyên bố trả về cho mỗi chức năng' và tôi thấy điều đó khá khó khăn. Tôi viết rất nhiều mã tính toán số và thường có 'trường hợp đặc biệt', vì vậy mã cuối cùng khá khó theo dõi ...
Nhiều điểm thoát là tốt cho các chức năng đủ nhỏ - nghĩa là, một chức năng có thể được xem trên toàn bộ một chiều dài màn hình. Nếu một hàm dài tương tự bao gồm nhiều điểm thoát, thì đó là dấu hiệu cho thấy hàm này có thể được cắt nhỏ hơn nữa.
Điều đó nói rằng tôi tránh các chức năng đa lối ra trừ khi thực sự cần thiết . Tôi đã cảm thấy đau đớn của các lỗi là do một số trở lại đi lạc trong một số dòng tối nghĩa trong các chức năng phức tạp hơn.
Tôi đã làm việc với các tiêu chuẩn mã hóa khủng khiếp đã tạo ra một lối thoát duy nhất cho bạn và kết quả là hầu như luôn luôn không có cấu trúc spaghetti nếu chức năng là bất cứ điều gì ngoài tầm thường - bạn kết thúc với rất nhiều lần nghỉ và tiếp tục cản trở.
if
tuyên bố trước mỗi cuộc gọi phương thức có trả lại thành công hay không :(
Điểm thoát đơn - tất cả những thứ khác bằng nhau - làm cho mã dễ đọc hơn đáng kể. Nhưng có một nhược điểm: xây dựng phổ biến
resulttype res;
if if if...
return res;
là giả, "res =" không tốt hơn "trả lại". Nó có câu lệnh return duy nhất, nhưng nhiều điểm trong đó hàm thực sự kết thúc.
Nếu bạn có hàm có nhiều trả về (hoặc "res =" s), thì thường nên chia nó thành nhiều hàm nhỏ hơn với một điểm thoát duy nhất.
Chính sách thông thường của tôi là chỉ có một câu lệnh return ở cuối hàm trừ khi độ phức tạp của mã giảm đi rất nhiều bằng cách thêm nhiều hơn. Trên thực tế, tôi khá hâm mộ Eiffel, thực thi quy tắc trả về duy nhất bằng cách không có tuyên bố trả về (chỉ có một biến 'kết quả' được tạo tự động để đưa kết quả của bạn vào).
Chắc chắn có những trường hợp mã có thể được làm rõ ràng hơn với nhiều lợi nhuận hơn phiên bản rõ ràng mà không có chúng. Người ta có thể lập luận rằng cần phải làm lại nhiều hơn nếu bạn có một hàm quá phức tạp để có thể hiểu được nếu không có nhiều câu trả về, nhưng đôi khi thật tốt khi thực dụng về những điều đó.
Nếu bạn kết thúc với nhiều hơn một vài lần trả về, có thể có gì đó không đúng với mã của bạn. Mặt khác, tôi đồng ý rằng đôi khi thật tuyệt khi có thể quay lại từ nhiều nơi trong chương trình con, đặc biệt là khi nó làm cho mã sạch hơn.
sub Int_to_String( Int i ){
given( i ){
when 0 { return "zero" }
when 1 { return "one" }
when 2 { return "two" }
when 3 { return "three" }
when 4 { return "four" }
...
default { return undef }
}
}
sẽ được viết tốt hơn như thế này
@Int_to_String = qw{
zero
one
two
three
four
...
}
sub Int_to_String( Int i ){
return undef if i < 0;
return undef unless i < @Int_to_String.length;
return @Int_to_String[i]
}
Lưu ý đây chỉ là một ví dụ nhanh
Tôi bỏ phiếu cho một lần trở lại vào cuối như một hướng dẫn. Điều này giúp xử lý dọn dẹp mã phổ biến ... Ví dụ: hãy xem mã sau đây ...
void ProcessMyFile (char *szFileName)
{
FILE *fp = NULL;
char *pbyBuffer = NULL:
do {
fp = fopen (szFileName, "r");
if (NULL == fp) {
break;
}
pbyBuffer = malloc (__SOME__SIZE___);
if (NULL == pbyBuffer) {
break;
}
/*** Do some processing with file ***/
} while (0);
if (pbyBuffer) {
free (pbyBuffer);
}
if (fp) {
fclose (fp);
}
}
Đây có lẽ là một viễn cảnh khác thường, nhưng tôi nghĩ rằng bất cứ ai tin rằng nhiều tuyên bố hoàn trả sẽ được ưa chuộng thì không bao giờ phải sử dụng trình gỡ lỗi trên bộ vi xử lý chỉ hỗ trợ 4 điểm dừng phần cứng. ;-)
Mặc dù các vấn đề về "mã mũi tên" là hoàn toàn chính xác, một vấn đề dường như biến mất khi sử dụng nhiều câu lệnh trả về là trong trường hợp bạn đang sử dụng trình gỡ lỗi. Bạn không có vị trí bắt tất cả thuận tiện để đặt điểm dừng để đảm bảo rằng bạn sẽ thấy lối ra và do đó điều kiện quay trở lại.
Bạn càng có nhiều câu lệnh return trong hàm, độ phức tạp càng cao trong một phương thức đó. Nếu bạn thấy mình tự hỏi nếu bạn có quá nhiều câu lệnh return, bạn có thể tự hỏi mình nếu bạn có quá nhiều dòng mã trong hàm đó.
Nhưng, không, không có gì sai với một / nhiều báo cáo trả lại. Trong một số ngôn ngữ, đó là một cách thực hành tốt hơn (C ++) so với các ngôn ngữ khác (C).