Là sử dụng mệnh đề cuối cùng để làm việc sau khi trả lại phong cách xấu / nguy hiểm?


30

Là một phần của việc viết Iterator, tôi thấy mình đã viết đoạn mã sau (xử lý lỗi tước)

public T next() {
  try {
    return next;
  } finally {
    next = fetcher.fetchNext(next);
  }
}

thấy nó hơi dễ đọc hơn

public T next() {
  T tmp = next;
  next = fetcher.fetchNext(next);
  return tmp;
}

Tôi biết đó là một ví dụ đơn giản, trong đó sự khác biệt về khả năng đọc có thể không quá lớn, nhưng tôi quan tâm đến ý kiến ​​chung là liệu có tệ khi sử dụng thử trong những trường hợp như thế này khi không có ngoại lệ liên quan, hoặc nếu nó thực sự được ưa thích khi nó đơn giản hóa mã.

Nếu nó xấu: tại sao? Phong cách, hiệu suất, cạm bẫy, ...?

Kết luận Cảm ơn tất cả các câu trả lời của bạn! Tôi đoán kết luận (ít nhất là đối với tôi) là ví dụ đầu tiên có thể dễ đọc hơn nếu đó là một mẫu chung, nhưng không phải vậy. Do đó, sự nhầm lẫn được đưa ra bằng cách sử dụng một cấu trúc bên ngoài mục đích của nó, cùng với dòng ngoại lệ có thể bị che khuất, sẽ vượt xa mọi sự đơn giản hóa.


10
Cá nhân tôi sẽ có thể hiểu cái thứ hai nhiều hơn cái thứ nhất, nhưng tôi hiếm khi sử dụng finallycác khối.
Christian Mann

2
return current = fetcher.fetchNext (current); // Làm thế nào về điều này, aka bạn thực sự cần tìm nạp trước?
Scarfridge

1
@scarfridge Ví dụ về triển khai Iterator, nơi bạn thực sự cần sắp xếp trước hasNext()để làm việc. Hãy thử nó.
maaartinus

1
@maaartinus Tôi biết điều đó. Đôi khi hasNext () chỉ đơn giản trả về true, ví dụ như chuỗi hailstone và đôi khi việc tìm nạp phần tử tiếp theo rất tốn kém. Trong trường hợp sau, bạn nên sử dụng chỉ tìm nạp phần tử theo yêu cầu tương tự như khởi tạo lười biếng.
Scarfridge

1
@scarfridge Vấn đề với việc sử dụng phổ biến Iteratorlà bạn cần tìm nạp giá trị trong hasNext()(vì việc tìm nạp nó thường là cách duy nhất để tìm hiểu xem nó có tồn tại không) và trả lại nó next()giống như OP đã làm.
maaartinus

Câu trả lời:


18

Cá nhân, tôi sẽ suy nghĩ về ý tưởng phân tách truy vấn lệnh . Khi bạn truy cập thẳng vào nó, next () có hai mục đích: mục đích được quảng cáo là lấy phần tử tiếp theo và tác dụng phụ ẩn của việc thay đổi trạng thái bên trong của tiếp theo. Vì vậy, những gì bạn đang làm là thực hiện mục đích được quảng cáo trong phần thân của phương thức và sau đó xử lý một hiệu ứng phụ ẩn trong mệnh đề cuối cùng, có vẻ như ... vụng về, mặc dù không "sai", chính xác.

Điều tôi thực sự hiểu được là mã dễ hiểu như thế nào, tôi nói, và trong trường hợp này, câu trả lời là "meh". Bạn đang "thử" một câu lệnh trả lại đơn giản và sau đó thực hiện một hiệu ứng phụ ẩn trong một khối mã được cho là để phục hồi lỗi không an toàn. Những gì bạn đang làm là 'thông minh' và mã 'thông minh' thường khiến những người bảo trì lẩm bẩm, "những gì ... oh, tôi nghĩ rằng tôi đã nhận được nó." Tốt hơn cho những người đọc mã của bạn để lẩm bẩm "yep, uh-huh, có ý nghĩa, vâng ..."

Vì vậy, điều gì sẽ xảy ra nếu bạn tách đột biến trạng thái khỏi các cuộc gọi của người truy cập? Tôi tưởng tượng vấn đề dễ đọc mà bạn quan tâm trở thành tranh luận, nhưng tôi không biết điều đó ảnh hưởng đến sự trừu tượng rộng lớn hơn của bạn như thế nào. Một cái gì đó để xem xét, dù sao.


1
+1 cho nhận xét "thông minh". Điều đó rất chính xác nắm bắt ví dụ này.

2
Hành động 'trả lại và tạm ứng' của next () bị ép buộc bởi OP bởi giao diện trình lặp Java khó sử dụng.
kevin cline

37

Hoàn toàn từ quan điểm phong cách, tôi nghĩ ba dòng này:

T current = next;
next = fetcher.getNext();
return current;

... Cả hai đều rõ ràng hơn và ngắn hơn một khối thử / cuối cùng. Vì bạn không mong đợi bất kỳ ngoại lệ nào sẽ bị ném, sử dụng một trykhối sẽ khiến mọi người nhầm lẫn.


2
Khối thử / bắt / cuối cùng cũng có thể thêm chi phí phụ, tôi không chắc liệu javac hoặc trình biên dịch JIT sẽ loại bỏ chúng ngay cả khi bạn không ném Ngoại lệ.
Martijn Verburg

2
@MartijnVerburg yeah, javac vẫn thêm một mục vào bảng ngoại lệ và sao chép mã cuối cùng ngay cả khi khối thử trống.
Daniel Lubarov

@Daniel - cảm ơn tôi đã quá lười biếng để thực thi javap :-)
Martijn Verburg

@Martijn Verburg: AFAIK, một khối thử-bắt-vây-về cơ bản là miễn phí miễn là không có ngoại lệ thực sự bị ném. Ở đây, javac không thử và không cần tối ưu hóa bất cứ điều gì vì JIT sẽ chăm sóc nó.
maaartinus

@maaartinus - cảm ơn điều đó có ý nghĩa - cần đọc lại mã nguồn OpenJDK :-)
Martijn Verburg

6

Điều đó phụ thuộc vào mục đích của mã bên trong khối cuối cùng. Ví dụ kinh điển đang đóng một luồng sau khi đọc / ghi từ nó, một số loại "dọn dẹp" phải luôn luôn được thực hiện. Khôi phục một đối tượng (trong trường hợp này là một trình vòng lặp) về trạng thái hợp lệ IMHO cũng được tính là dọn dẹp, vì vậy tôi thấy không có vấn đề gì ở đây. Nếu OTOH bạn đang sử dụng returnngay khi tìm thấy giá trị trả về của bạn và thêm rất nhiều mã không liên quan vào khối cuối cùng, thì nó sẽ che khuất mục đích và làm cho tất cả trở nên dễ hiểu hơn.

Tôi không thấy bất kỳ vấn đề nào trong việc sử dụng nó khi "không có ngoại lệ liên quan". Nó rất phổ biến để sử dụngtry...finally mà không có catchkhi mã chỉ có thể ném RuntimeExceptionvà bạn không có kế hoạch xử lý chúng. Đôi khi, cuối cùng chỉ là một biện pháp bảo vệ và bạn biết logic của chương trình của bạn rằng sẽ không có ngoại lệ nào bị ném (điều kiện "điều này không bao giờ nên xảy ra" cổ điển).

Cạm bẫy: bất kỳ ngoại lệ nào được nêu ra trong trykhối sẽ khiến finallybock chạy. Điều đó có thể đưa bạn vào một trạng thái không nhất quán. Vì vậy, nếu tuyên bố trở lại của bạn là một cái gì đó như:

return preProcess(next);

và mã này đưa ra một ngoại lệ, fetchNextvẫn sẽ chạy. OTOH nếu bạn mã hóa nó như:

T ret = preProcess(next);
next = fetcher.fetchNext(next);
return ret;

sau đó nó sẽ không chạy Tôi biết bạn đang giả sử mã thử có thể không bao giờ đưa ra bất kỳ ngoại lệ nào, nhưng đối với các trường hợp phức tạp hơn mức này thì làm sao bạn có thể chắc chắn? Nếu trình vòng lặp của bạn là một đối tượng tồn tại lâu, điều đó sẽ tiếp tục tồn tại ngay cả khi một lỗi không thể phục hồi đã xảy ra trong luồng hiện tại, điều quan trọng là phải giữ nó ở trạng thái hợp lệ mọi lúc. Mặt khác, nó không thực sự quan trọng lắm ...

Hiệu năng: thật thú vị khi dịch ngược mã như vậy để xem cách nó hoạt động dưới mui xe, nhưng tôi không biết đủ về JVM để thậm chí đoán đúng ... Các ngoại lệ thường là "ngoại lệ", vì vậy mã xử lý chúng không cần phải được tối ưu hóa cho tốc độ (do đó lời khuyên không bao giờ sử dụng ngoại lệ trong luồng điều khiển thông thường của các chương trình của bạn), nhưng tôi không biết finally.


Không phải bạn thực sự cung cấp một lý do khá chính đáng để tránh sử dụng finallytheo cách này sao? Ý tôi là, như bạn nói, mã cuối cùng có những hậu quả không mong muốn; thậm chí tệ hơn, bạn có thể thậm chí không nghĩ về ngoại lệ.
Andres F.

2
Tốc độ dòng điều khiển bình thường không bị ảnh hưởng đáng kể bởi các cấu trúc xử lý ngoại lệ. Hình phạt hiệu suất chỉ được thanh toán khi thực sự ném và bắt ngoại lệ, không phải khi vào khối thử hoặc nhập khối cuối cùng trong hoạt động bình thường.
yfeldblum

1
@AresresF. Đúng, nhưng điều đó cũng hợp lệ cho bất kỳ mã dọn dẹp nào . Ví dụ: giả sử tham chiếu đến luồng mở được đặt thành nullmã lỗi; cố gắng đọc từ nó sẽ đưa ra một ngoại lệ, cố gắng đóng nó trong finallykhối sẽ đưa ra một ngoại lệ khác . Ngoài ra, đây là tình huống trong đó trạng thái tính toán không thành vấn đề, bạn muốn đóng luồng cho dù có xảy ra ngoại lệ hay không. Có thể có những tình huống khác như vậy là tốt. Vì lý do này, tôi coi nó như một cạm bẫy cho việc sử dụng hợp lệ, thay vì giới thiệu để không bao giờ sử dụng nó.
mgibsonbr

Ngoài ra còn có một vấn đề là khi cả thử và cuối cùng chặn một ngoại lệ, ngoại lệ trong thử không bao giờ được nhìn thấy bởi bất kỳ ai, nhưng nó có thể là nguyên nhân của vấn đề.
Hans-Peter Störr

3

Tuyên bố tiêu đề: "... điều khoản cuối cùng để thực hiện công việc sau khi trở về ..." là sai. Khối cuối cùng xảy ra trước khi hàm trả về. Đó là toàn bộ điểm cuối cùng trong thực tế.

Những gì bạn đang lạm dụng ở đây là thứ tự đánh giá, trong đó giá trị tiếp theo được lưu trữ để trả về trước khi bạn thay đổi nó. Đó không phải là thông lệ và theo tôi là sai vì nó khiến bạn viết mã không tuần tự và do đó khó theo dõi hơn nhiều.

Mục đích sử dụng của các khối cuối cùng là để xử lý ngoại lệ trong đó một số hậu quả phải xảy ra bất kể ngoại lệ nào được đưa ra, ví dụ như dọn sạch một số tài nguyên, đóng kết nối DB, đóng tệp / ổ cắm, v.v.


+1, tôi sẽ nói thêm rằng ngay cả khi thông số ngôn ngữ đảm bảo rằng giá trị được lưu trữ trước khi trả về, nó sẽ đi ngược lại mong đợi của người đọc và do đó nên tránh khi có thể. Gỡ lỗi khó gấp đôi so với mã hóa ...
jmoreno

0

Tôi sẽ thực sự cẩn thận, bởi vì (nói chung, không phải trong ví dụ của bạn) phần thử có thể ném ngoại lệ, và nếu nó bị ném ra, sẽ không có gì được trả lại và nếu bạn thực sự muốn thực hiện những gì cuối cùng sau khi trở về, nó sẽ thực hiện khi bạn không muốn nó.

Và một loại sâu khác là, nói chung, một số hành động hữu ích cuối cùng có thể đưa ra các ngoại lệ, vì vậy bạn có thể kết thúc bằng phương pháp thực sự lộn xộn.

Bởi vì những điều này, ai đó đọc nó phải suy nghĩ về những vấn đề này, vì vậy nó khó hiểu hơn.

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.