Có phải các nhà phát triển của Java đã từ bỏ RAII?


82

Là một lập trình viên C # lâu năm, gần đây tôi đã đến để tìm hiểu thêm về những lợi thế của việc mua lại tài nguyên (RAII). Cụ thể, tôi đã phát hiện ra rằng thành ngữ C #:

using (var dbConn = new DbConnection(connStr)) {
    // do stuff with dbConn
}

có tương đương C ++:

{
    DbConnection dbConn(connStr);
    // do stuff with dbConn
}

có nghĩa là việc nhớ kèm theo việc sử dụng các tài nguyên như DbConnectiontrong một usingkhối là không cần thiết trong C ++! Đây dường như là một lợi thế lớn của C ++. Điều này thậm chí còn thuyết phục hơn khi bạn xem xét một lớp có một thành viên thể hiện của loại DbConnection, ví dụ

class Foo {
    DbConnection dbConn;

    // ...
}

Trong C # tôi sẽ cần phải thực hiện Foo IDisposablenhư sau:

class Foo : IDisposable {
    DbConnection dbConn;

    public void Dispose()
    {       
        dbConn.Dispose();
    }
}

và điều tồi tệ hơn, mọi người dùng Foosẽ cần phải nhớ gửi kèm theo Foomột usingkhối, như:

   using (var foo = new Foo()) {
       // do stuff with "foo"
   }

Bây giờ nhìn vào C # và nguồn gốc Java của nó, tôi tự hỏi ... các nhà phát triển của Java có hoàn toàn đánh giá cao những gì họ đã từ bỏ khi họ từ bỏ ngăn xếp có lợi cho đống, do đó từ bỏ RAII không?

(Tương tự, Stroustrup có đánh giá đầy đủ ý nghĩa của RAII không?)


5
Tôi không chắc chắn những gì bạn đang nói về việc không kèm theo tài nguyên trong C ++. Đối tượng DBConnection có thể xử lý việc đóng tất cả các tài nguyên trong hàm hủy của nó.
maple_shaft

16
@maple_shaft, chính xác là quan điểm của tôi! Đó là lợi thế của C ++ mà tôi đang giải quyết trong câu hỏi này. Trong C #, bạn cần gửi tài nguyên vào "bằng cách sử dụng" ... trong C ++, bạn không cần.
JoelFan

12
Hiểu biết của tôi là RAII, như một chiến lược, chỉ được hiểu một khi trình biên dịch C ++ đủ tốt để thực sự sử dụng khuôn mẫu nâng cao, cũng là sau Java. C ++ thực sự có sẵn để sử dụng khi Java được tạo ra là một kiểu rất nguyên thủy, "C với các lớp", với các mẫu có thể cơ bản, nếu bạn may mắn.
Sean McMillan

6
"Sự hiểu biết của tôi là RAII, với tư cách là một chiến lược, chỉ được hiểu khi trình biên dịch C ++ đủ tốt để thực sự sử dụng khuôn mẫu nâng cao, cũng là sau Java." - Điều đó không thực sự chính xác. Các nhà xây dựng và phá hủy đã là các tính năng cốt lõi của C ++ kể từ ngày đầu tiên, trước khi sử dụng rộng rãi các mẫu và trước cả Java.
Jim ở Texas

8
@JimInTexas: Tôi nghĩ Sean có một hạt giống cơ bản của sự thật ở đâu đó (Mặc dù không phải mẫu nhưng ngoại lệ là mấu chốt). Các nhà xây dựng / phá hủy đã có ngay từ đầu, nhưng có tầm quan trọng và khái niệm RAII ban đầu không phải là từ (từ mà tôi đang tìm kiếm) đã nhận ra. Phải mất một vài năm và một thời gian để các trình biên dịch có thể hoạt động tốt trước khi chúng tôi nhận ra toàn bộ RAII quan trọng như thế nào.
Martin York

Câu trả lời:


38

Bây giờ nhìn vào C # và nguồn gốc Java của nó, tôi tự hỏi ... các nhà phát triển của Java có hoàn toàn đánh giá cao những gì họ đã từ bỏ khi họ từ bỏ ngăn xếp có lợi cho đống, do đó từ bỏ RAII không?

(Tương tự, Stroustrup có đánh giá đầy đủ ý nghĩa của RAII không?)

Tôi khá chắc chắn rằng Gosling đã không nhận được tầm quan trọng của RAII tại thời điểm anh ta thiết kế Java. Trong các cuộc phỏng vấn của mình, ông thường nói về những lý do để loại bỏ thuốc generic và quá tải toán tử, nhưng không bao giờ đề cập đến các hàm hủy xác định và RAII.

Hài hước lắm, ngay cả Stroustrup cũng không nhận thức được tầm quan trọng của những kẻ hủy diệt xác định tại thời điểm ông thiết kế chúng. Tôi không thể tìm thấy trích dẫn, nhưng nếu bạn thực sự thích nó, bạn có thể tìm thấy nó trong số các cuộc phỏng vấn của anh ấy ở đây: http://www.stroustrup.com/interviews.html


11
@maple_shaft: Tóm lại, điều đó là không thể. Ngoại trừ nếu bạn đã phát minh ra một cách để có bộ sưu tập rác xác định (nói chung dường như là không thể và vô hiệu hóa tất cả các tối ưu hóa GC của những thập kỷ trước trong mọi trường hợp), bạn phải giới thiệu các đối tượng được phân bổ theo ngăn xếp, nhưng điều đó mở ra một số lon sâu: những đối tượng này cần ngữ nghĩa, "vấn đề cắt" với phân nhóm (và do đó KHÔNG đa hình), con trỏ lơ lửng trừ khi bạn có thể hạn chế đáng kể về nó hoặc thay đổi hệ thống loại không tương thích lớn. Và đó chỉ là trên đỉnh đầu của tôi.

13
@DeadMG: Vì vậy, bạn đề nghị chúng tôi quay lại quản lý bộ nhớ thủ công. Đó là một cách tiếp cận hợp lệ để lập trình nói chung, và tất nhiên nó cho phép phá hủy xác định. Nhưng điều đó không trả lời cho câu hỏi này, nó liên quan đến cài đặt chỉ có GC muốn cung cấp sự an toàn cho bộ nhớ và hành vi được xác định rõ ngay cả khi tất cả chúng ta hành động như những kẻ ngốc. Điều đó đòi hỏi GC cho mọi thứ và không có cách nào để khởi động việc phá hủy đối tượng một cách thủ công (và tất cả mã Java trong tồn tại ít nhất phụ thuộc vào cái trước), do đó, bạn có thể xác định được GC hoặc bạn không gặp may.

26
@delan. Tôi sẽ không gọi manualquản lý bộ nhớ con trỏ thông minh C ++ . Chúng giống như một công cụ thu gom rác có thể kiểm soát hạt mịn xác định. Nếu sử dụng đúng con trỏ thông minh là những con ong đầu gối.
Martin York

10
@LokiAstari: Chà, tôi muốn nói rằng chúng hơi tự động hơn so với toàn bộ GC (bạn phải nghĩ về loại thông minh nào bạn thực sự muốn) và thực hiện chúng như thư viện yêu cầu con trỏ thô (và do đó quản lý bộ nhớ thủ công) để xây dựng . Ngoài ra, tôi không biết về bất kỳ con trỏ thông minh nào ngoài việc tự động xử lý các tham chiếu theo chu kỳ, đây là một yêu cầu nghiêm ngặt đối với việc thu gom rác trong sách của tôi. Con trỏ thông minh chắc chắn rất tuyệt vời và hữu ích, nhưng bạn phải đối mặt với việc chúng không thể cung cấp một số đảm bảo (cho dù bạn có coi chúng hữu ích hay không) của ngôn ngữ GC'd hoàn toàn và độc quyền.

11
@delan: Tôi phải không đồng ý ở đó. Tôi nghĩ rằng chúng tự động hơn so với GC vì chúng mang tính quyết định. ĐỒNG Ý. Để hiệu quả, bạn cần chắc chắn rằng bạn sử dụng đúng (tôi sẽ cung cấp cho bạn điều đó). std :: yếu_ptr xử lý chu kỳ hoàn toàn tốt. Chu kỳ luôn luôn được tìm ra nhưng trong thực tế hầu như không có vấn đề gì vì đối tượng cơ sở thường là dựa trên ngăn xếp và khi điều này xảy ra, nó sẽ dọn dẹp phần còn lại. Đối với các trường hợp hiếm hoi, nó có thể là một vấn đề std :: yếu_ptr.
Martin York

60

Có, các nhà thiết kế của C # (và, tôi chắc chắn, Java) đã quyết định cụ thể chống lại quyết toán quyết định. Tôi đã hỏi Anders Hejlsberg về điều này nhiều lần vào khoảng năm 1999-2002.

Đầu tiên, ý tưởng về ngữ nghĩa khác nhau cho một đối tượng dựa trên việc dựa trên ngăn xếp hay đống của nó chắc chắn phản lại mục tiêu thiết kế thống nhất của cả hai ngôn ngữ, nhằm giải quyết các lập trình viên về các vấn đề chính xác như vậy.

Thứ hai, ngay cả khi bạn thừa nhận rằng có những lợi thế, có những phức tạp triển khai và sự thiếu hiệu quả đáng kể liên quan đến việc giữ sổ sách. Bạn thực sự không thể đặt các đối tượng giống như ngăn xếp lên ngăn xếp bằng ngôn ngữ được quản lý. Bạn chỉ còn cách nói "ngữ nghĩa giống như ngăn xếp" và cam kết thực hiện công việc quan trọng (các loại giá trị đã đủ khó, hãy nghĩ về một đối tượng là một thể hiện của một lớp phức tạp, với các tham chiếu đến và quay trở lại vào bộ nhớ được quản lý).

Do đó, bạn không muốn hoàn thiện xác định trên mọi đối tượng trong một hệ thống lập trình trong đó "(hầu hết) mọi thứ là một đối tượng". Vì vậy, bạn đừng có để giới thiệu một số loại cú pháp lập trình điều khiển để tách một đối tượng thường-theo dõi từ một trong đó có quyết toán xác định.

Trong C #, bạn có usingtừ khóa, xuất hiện khá muộn trong thiết kế của cái đã trở thành C # 1.0. Toàn bộ IDisposableđiều này khá tồi tệ, và người ta tự hỏi liệu nó có thanh lịch hơn khi usinglàm việc với cú pháp hàm hủy C ++ ~đánh dấu các lớp mà IDisposablemô hình tấm nồi hơi có thể được áp dụng tự động không?


2
Điều gì về những gì C ++ / CLI (.NET) đã làm, trong đó các đối tượng trên heap được quản lý cũng có một "xử lý" dựa trên ngăn xếp, cung cấp RIAA?
JoelFan

3
C ++ / CLI có một tập hợp các quyết định và ràng buộc thiết kế rất khác nhau. Một số trong những quyết định đó có nghĩa là bạn có thể đòi hỏi nhiều suy nghĩ hơn về việc phân bổ bộ nhớ và ý nghĩa hiệu suất từ ​​các lập trình viên: toàn bộ "cho em đủ dây để tự treo mình" đánh đổi. Và tôi tưởng tượng rằng trình biên dịch C ++ / CLI phức tạp hơn đáng kể so với C # (đặc biệt là ở các thế hệ đầu của nó).
Larry OBrien

5
+1 đây là câu trả lời đúng duy nhất cho đến nay - đó là vì Java cố tình không có các đối tượng dựa trên ngăn xếp (không nguyên thủy).
BlueRaja - Daniel Pflughoeft

8
@Peter Taylor - đúng rồi. Nhưng tôi cảm thấy rằng hàm hủy không xác định của C # có giá trị rất ít, vì bạn không thể dựa vào nó để quản lý bất kỳ loại tài nguyên bị ràng buộc nào. Vì vậy, theo tôi, có lẽ tốt hơn là sử dụng ~cú pháp để trở thành cú pháp đường choIDisposable.Dispose()
Larry OBrien

3
@Larry: Tôi đồng ý. C ++ / CLI không sử dụng ~làm đường cú pháp IDisposable.Dispose(), và nó thuận tiện hơn nhiều so với cú pháp C #.
dan04

41

Hãy nhớ rằng Java đã được phát triển vào năm 1991-1995 khi C ++ là một ngôn ngữ khác. Các ngoại lệ (khiến RAII trở nên cần thiết ) và các mẫu (giúp dễ dàng thực hiện các con trỏ thông minh hơn) là các tính năng "mới mẻ". Hầu hết các lập trình viên C ++ đã đến từ C và đã quen với việc quản lý bộ nhớ thủ công.

Vì vậy, tôi nghi ngờ rằng các nhà phát triển của Java đã cố tình quyết định từ bỏ RAII. Tuy nhiên, đó là một quyết định có chủ ý cho Java để thích ngữ nghĩa tham chiếu thay vì ngữ nghĩa giá trị. Phá hủy xác định là khó thực hiện trong một ngôn ngữ ngữ nghĩa tham khảo.

Vậy tại sao sử dụng ngữ nghĩa tham khảo thay vì ngữ nghĩa giá trị?

Bởi vì nó làm cho ngôn ngữ đơn giản hơn rất nhiều .

  • Không cần phân biệt cú pháp giữa FooFoo*hoặc giữa foo.barfoo->bar.
  • Không cần gán quá tải, khi tất cả các nhiệm vụ thực hiện là sao chép một con trỏ.
  • Không cần cho các nhà xây dựng sao chép. ( Đôi khi cần một chức năng sao chép rõ ràng như clone(). Nhiều đối tượng không cần phải sao chép. Ví dụ: bất biến không.)
  • Không cần phải khai báo các hàm tạo privatesao chép và operator=làm cho một lớp không thể sao chép được . Nếu bạn không muốn các đối tượng của một lớp được sao chép, bạn chỉ không viết một hàm để sao chép nó.
  • Không cần cho các swapchức năng. (Trừ khi bạn đang viết một thói quen sắp xếp.)
  • Không cần tham khảo giá trị kiểu C ++ 0x.
  • Không cần cho (N) RVO.
  • Không có vấn đề cắt lát.
  • Trình biên dịch xác định bố cục đối tượng dễ dàng hơn vì các tham chiếu có kích thước cố định.

Nhược điểm chính của ngữ nghĩa tham chiếu là khi mọi đối tượng có khả năng có nhiều tham chiếu đến nó, sẽ khó biết khi nào nên xóa nó. Bạn khá nhiều phải có quản lý bộ nhớ tự động.

Java đã chọn sử dụng một trình thu gom rác không xác định.

Không thể xác định được GC?

Vâng, nó có thể. Ví dụ, việc triển khai C của Python sử dụng tính tham chiếu. Và sau đó đã thêm dấu vết GC để xử lý rác theo chu kỳ trong đó việc đếm không thành công.

Nhưng việc đếm tiền là không hiệu quả khủng khiếp. Rất nhiều chu kỳ CPU dành để cập nhật số lượng. Thậm chí tệ hơn trong môi trường đa luồng (như loại Java được thiết kế cho) nơi các bản cập nhật đó cần được đồng bộ hóa. Tốt hơn nhiều để sử dụng trình thu gom rác null cho đến khi bạn cần chuyển sang một trình thu thập khác.

Bạn có thể nói rằng Java đã chọn tối ưu hóa trường hợp phổ biến (bộ nhớ) với chi phí cho các tài nguyên không bị nhiễm nấm như các tệp và ổ cắm. Ngày nay, trong bối cảnh việc áp dụng RAII trong C ++, điều này có vẻ như là sự lựa chọn sai lầm. Nhưng hãy nhớ rằng phần lớn đối tượng mục tiêu cho Java là các lập trình viên C (hoặc "C với các lớp"), những người đã quen với việc đóng những thứ này một cách rõ ràng.

Nhưng những gì về "đối tượng ngăn xếp" C ++ / CLI?

Chúng chỉ là đường cú pháp choDispose ( liên kết ban đầu ), giống như C # using. Tuy nhiên, nó không giải quyết được vấn đề chung về phá hủy xác định, bởi vì bạn có thể tạo một ẩn danh gcnew FileStream("filename.ext")và C ++ / CLI sẽ không tự động Loại bỏ nó.


3
Ngoài ra, các liên kết đẹp (đặc biệt là liên kết đầu tiên, rất phù hợp với cuộc thảo luận này) .
BlueRaja - Danny Pflughoeft

Các usingtuyên bố xử lý nhiều vấn đề liên quan đến dọn dẹp độc đáo, nhưng nhiều người khác vẫn còn. Tôi sẽ đề xuất rằng cách tiếp cận phù hợp cho ngôn ngữ và khung sẽ là phân biệt khai báo giữa các vị trí lưu trữ "sở hữu" một tham chiếu IDisposabletừ các vị trí không; ghi đè hoặc từ bỏ một vị trí lưu trữ sở hữu một tham chiếu IDisposablenên loại bỏ mục tiêu trong trường hợp không có chỉ thị ngược lại.
supercat

1
"Không cần các nhà xây dựng sao chép" nghe có vẻ hay, nhưng thất bại nặng nề trong thực tế. java.util.Date và Lịch có lẽ là những ví dụ nổi tiếng nhất. Không có gì đáng yêu hơn new Date(oldDate.getTime()).
kevin cline

2
iow RAII không bị "bỏ rơi", đơn giản là nó không tồn tại để bị bỏ rơi :) Khi sao chép các nhà xây dựng, tôi chưa bao giờ thích chúng, quá dễ để hiểu sai, chúng là một nguồn đau đầu liên tục khi ở đâu đó sâu bên dưới một ai đó (khác) quên tạo một bản sao sâu, khiến tài nguyên được chia sẻ giữa các bản sao không nên.
jwenting

20

Java7 đã giới thiệu một cái gì đó tương tự như C # using: Tuyên bố thử tài nguyên

một trytuyên bố tuyên bố một hoặc nhiều tài nguyên. Một tài nguyên là một đối tượng phải được đóng lại sau khi chương trình kết thúc với nó. Câu lệnh try-with-resource đảm bảo rằng mỗi tài nguyên được đóng ở cuối câu lệnh. Bất kỳ đối tượng nào thực hiện java.lang.AutoCloseable, bao gồm tất cả các đối tượng triển khai java.io.Closeable, có thể được sử dụng làm tài nguyên ...

Vì vậy, tôi đoán rằng họ đã không chủ ý chọn không thực hiện RAII hoặc họ đã thay đổi ý định trong khi đó.


Thú vị, nhưng có vẻ như điều này chỉ hoạt động với các đối tượng thực hiện java.lang.AutoCloseable. Có lẽ không phải là một vấn đề lớn nhưng tôi không thích cảm giác này bị hạn chế phần nào. Có thể tôi có một số đối tượng khác nên được tự động chuyển đổi, nhưng thật kỳ lạ về mặt ngữ nghĩa để khiến nó thực hiện AutoCloseable...
Thất vọngWithFormsDesigner

9
@Patrick: Ơ, vậy sao? usingkhông giống với RAII - trong một trường hợp, người gọi lo lắng về việc xử lý tài nguyên, trong trường hợp khác, callee xử lý nó.
BlueRaja - Daniel Pflughoeft

1
+1 Tôi không biết về thử tài nguyên; nó sẽ hữu ích trong việc bán phá giá nhiều hơn.
jprete

3
-1 cho using/ thử với các tài nguyên không giống với RAII.
Sean McMillan

4
@Sean: Đồng ý. usingvà nó không ở đâu gần RAII.
DeadMG

18

Java cố ý không có các đối tượng dựa trên ngăn xếp (còn gọi là các đối tượng giá trị). Đây là những điều cần thiết để có đối tượng tự động hủy ở cuối phương thức như thế.

Bởi vì điều này và thực tế là Java được thu gom rác, việc hoàn thiện xác định ít nhiều không thể xảy ra (ví dụ: Nếu đối tượng "cục bộ" của tôi trở thành tham chiếu ở nơi nào khác thì sao? ) .

Tuy nhiên, điều này tốt với hầu hết chúng ta, bởi vì hầu như không bao giờ cần phải hoàn thiện xác định, ngoại trừ khi tương tác với tài nguyên bản địa (C ++)!


Tại sao Java không có các đối tượng dựa trên ngăn xếp?

(Khác với nguyên thủy ..)

Bởi vì các đối tượng dựa trên ngăn xếp có ngữ nghĩa khác với các tham chiếu dựa trên heap. Hãy tưởng tượng đoạn mã sau trong C ++; nó làm gì?

return myObject;
  • Nếu myObjectlà một đối tượng dựa trên ngăn xếp cục bộ, hàm tạo sao chép được gọi (nếu kết quả được gán cho một cái gì đó).
  • Nếu myObjectlà một đối tượng dựa trên ngăn xếp cục bộ và chúng tôi đang trả về một tham chiếu, kết quả không được xác định.
  • Nếu myObjectlà một thành viên / đối tượng toàn cầu, hàm tạo sao chép được gọi (nếu kết quả được gán cho một cái gì đó).
  • Nếu myObjectlà một thành viên / đối tượng toàn cầu và chúng tôi đang trả lại một tham chiếu, thì tham chiếu được trả về.
  • Nếu myObjectlà một con trỏ tới một đối tượng dựa trên ngăn xếp cục bộ, kết quả không được xác định.
  • Nếu myObjectlà một con trỏ tới một đối tượng thành viên / toàn cầu, con trỏ đó được trả về.
  • Nếu myObjectlà một con trỏ tới một đối tượng dựa trên heap, con trỏ đó được trả về.

Bây giờ mã tương tự làm gì trong Java?

return myObject;
  • Các tài liệu tham khảo myObjectđược trả lại. Không thành vấn đề nếu biến là cục bộ, thành viên hoặc toàn cầu; và không có đối tượng dựa trên ngăn xếp hoặc trường hợp con trỏ phải lo lắng.

Những điều trên cho thấy tại sao các đối tượng dựa trên ngăn xếp là nguyên nhân rất phổ biến gây ra lỗi lập trình trong C ++. Do đó, các nhà thiết kế Java đã đưa họ ra ngoài; và không có chúng, không có điểm nào trong việc sử dụng RAII trong Java.


6
Tôi không biết ý của bạn là gì bởi "không có điểm nào trong RAII" ... Tôi nghĩ bạn có nghĩa là "không có khả năng cung cấp RAII trong Java" ... RAII độc lập với bất kỳ ngôn ngữ nào ... nó không trở nên "vô nghĩa" vì 1 ngôn ngữ cụ thể không cung cấp ngôn ngữ đó
JoelFan

4
Đó không phải là một lý do hợp lệ. Một đối tượng không phải thực sự sống trên stack để sử dụng RAII dựa trên stack. Nếu có một thứ gọi là "tham chiếu duy nhất", thì hàm hủy có thể được kích hoạt một khi nó vượt ra khỏi phạm vi. Xem ví dụ, cách nó hoạt động với ngôn ngữ lập trình D: d-programming-lingu.org/exception-safe.html
Nemanja Trifunovic

3
@Nemanja: Một đối tượng không để sống trên stack có ngữ nghĩa dựa trên stack, và tôi chưa bao giờ nói nó đã làm. Nhưng đó không phải là vấn đề; vấn đề, như tôi đã đề cập, là chính ngữ nghĩa dựa trên ngăn xếp.
BlueRaja - Daniel Pflughoeft

4
@Aaronaught: Quỷ dữ ở "hầu như luôn luôn" và "hầu hết thời gian". Nếu bạn không đóng kết nối db của mình và để nó cho GC để kích hoạt trình hoàn thiện, nó sẽ hoạt động tốt với các bài kiểm tra đơn vị của bạn và bị hỏng nghiêm trọng khi được triển khai trong sản xuất. Dọn dẹp quyết định là quan trọng bất kể ngôn ngữ.
Nemanja Trifunovic

8
@NemanjaTrifunovic: Tại sao bạn thử nghiệm đơn vị trên kết nối cơ sở dữ liệu trực tiếp? Đó không thực sự là một bài kiểm tra đơn vị. Không, xin lỗi, tôi không mua nó. Dù sao thì bạn cũng không nên tạo kết nối DB ở mọi nơi, bạn nên chuyển chúng qua các hàm tạo hoặc thuộc tính và điều đó có nghĩa là bạn không muốn ngữ nghĩa tự động phá hủy giống như ngăn xếp. Rất ít đối tượng phụ thuộc vào kết nối cơ sở dữ liệu nên thực sự sở hữu nó. Nếu việc dọn dẹp không xác định thường xuyên cắn bạn, điều đó khó khăn, thì đó là do thiết kế ứng dụng tồi, không phải là thiết kế ngôn ngữ xấu.
Aaronaught

17

Mô tả của bạn về các lỗ của usingkhông đầy đủ. Hãy xem xét vấn đề sau:

interface Bar {
    ...
}
class Foo : Bar, IDisposable {
    ...
}

Bar b = new Foo();

// Where's the Dispose?

Theo tôi, không có cả RAII và GC là một ý tưởng tồi. Khi nói đến việc đóng các tệp trong Java, nó malloc()free()ở đằng kia.


2
Tôi đồng ý rằng RAII là những con ong đầu gối. Nhưng usingđiều khoản này là một bước tiến tuyệt vời cho C # trên Java. Nó không cho phép phá hủy xác định và do đó quản lý tài nguyên chính xác (nó không hoàn toàn tốt như RAII như bạn cần nhớ để làm điều đó, nhưng chắc chắn đó là một ý tưởng tốt).
Martin York

8
Càng khi nói đến việc đóng các tệp trong Java, đó là malloc () và free () ở đằng kia.
Konrad Rudolph

9
@KonradRudolph: Nó tệ hơn malloc và miễn phí. Ít nhất trong C bạn không có ngoại lệ.
Nemanja Trifunovic

1
@Nemanja: Hãy công bằng, bạn có thể free()trong finally.
DeadMG

4
@Loki: Vấn đề lớp cơ sở quan trọng hơn nhiều như một vấn đề. Ví dụ, bản gốc IEnumerablekhông được thừa kế từ đó IDisposable, và có một loạt các trình vòng lặp đặc biệt không bao giờ được thực hiện.
DeadMG

14

Tôi khá già Tôi đã ở đó và nhìn thấy nó và đập đầu tôi về nó nhiều lần.

Tôi đã có mặt tại một hội nghị ở Công viên Hursley nơi các chàng trai IBM đang nói với chúng tôi rằng ngôn ngữ Java hoàn toàn mới này tuyệt vời như thế nào, chỉ có ai đó hỏi ... tại sao không có kẻ hủy diệt cho các đối tượng này. Anh ta không có nghĩa là thứ mà chúng ta biết là kẻ hủy diệt trong C ++, nhưng cũng không có người quyết định (hoặc nó có người hoàn thiện nhưng về cơ bản họ không hoạt động). Đây là cách quay trở lại và chúng tôi quyết định Java là một chút ngôn ngữ đồ chơi vào thời điểm đó.

bây giờ họ đã thêm Người quyết định vào đặc tả ngôn ngữ và Java đã thấy một số thông qua.

Tất nhiên, sau đó mọi người được khuyên không nên đưa những người vào chung kết vào đối tượng của họ vì điều đó làm chậm quá trình GC xuống rất nhiều. (vì nó không chỉ khóa heap mà còn di chuyển các đối tượng sắp hoàn thành sang khu vực tạm thời, vì các phương thức này không thể được gọi vì GC đã tạm dừng ứng dụng chạy. Thay vào đó, chúng sẽ được gọi ngay trước khi tiếp theo Chu trình GC) (và tệ hơn, đôi khi trình hoàn thiện sẽ không bao giờ được gọi khi ứng dụng ngừng hoạt động. Hãy tưởng tượng không có tệp xử lý của bạn đóng, bao giờ)

Sau đó, chúng tôi đã có C #, và tôi nhớ diễn đàn thảo luận về MSDN nơi chúng tôi được cho biết ngôn ngữ C # mới này tuyệt vời như thế nào. Có người hỏi tại sao không có quyết toán quyết định và các chàng trai MS nói với chúng tôi rằng chúng tôi không cần những thứ như thế nào, sau đó nói với chúng tôi rằng chúng tôi cần thay đổi cách thiết kế ứng dụng, sau đó cho chúng tôi biết GC tuyệt vời như thế nào và tất cả các ứng dụng cũ của chúng tôi như thế nào rác và không bao giờ làm việc vì tất cả các tài liệu tham khảo thông tư. Sau đó, họ trích dẫn để gây áp lực và nói với chúng tôi rằng họ đã thêm mẫu IDispose này vào thông số kỹ thuật mà chúng tôi có thể sử dụng. Tôi nghĩ rằng đó là khá nhiều trở lại để quản lý bộ nhớ thủ công cho chúng tôi trong các ứng dụng C # tại thời điểm đó.

Tất nhiên, các chàng trai MS sau đó phát hiện ra rằng tất cả những gì họ đã nói với chúng tôi là ... tốt, họ đã tạo ra IDispose nhiều hơn một chút so với chỉ một giao diện tiêu chuẩn và sau đó thêm vào câu lệnh sử dụng. W00t! Rốt cuộc, họ nhận ra rằng quyết toán cuối cùng là thứ gì đó còn thiếu từ ngôn ngữ. Tất nhiên, bạn vẫn phải nhớ đặt nó ở mọi nơi, vì vậy nó vẫn hơi thủ công, nhưng tốt hơn.

Vậy tại sao họ lại làm điều đó khi họ có thể có ngữ nghĩa sử dụng kiểu tự động được đặt trên mỗi khối phạm vi ngay từ đầu? Có lẽ là hiệu quả, nhưng tôi muốn nghĩ rằng họ chỉ không nhận ra. Giống như cuối cùng họ nhận ra bạn vẫn cần con trỏ thông minh trong .NET (google SafeHandle), họ nghĩ rằng GC thực sự sẽ giải quyết tất cả các vấn đề. Họ quên rằng một đối tượng không chỉ là bộ nhớ và mà GC được thiết kế chủ yếu để xử lý việc quản lý bộ nhớ. họ bị cuốn vào ý tưởng rằng GC sẽ xử lý việc này và quên rằng bạn đặt những thứ khác vào đó, một đối tượng không chỉ là một đốm bộ nhớ không thành vấn đề nếu bạn không xóa nó trong một thời gian.

Nhưng tôi cũng nghĩ rằng việc thiếu một phương thức hoàn thiện trong Java ban đầu có nhiều hơn một chút - rằng các đối tượng bạn tạo ra đều là về bộ nhớ và nếu bạn muốn xóa một cái gì đó khác (như tay cầm DB hoặc ổ cắm hoặc bất cứ thứ gì ) sau đó bạn đã được dự kiến ​​sẽ làm điều đó bằng tay .

Hãy nhớ rằng Java được thiết kế cho các môi trường nhúng, nơi mọi người đã quen viết mã C với nhiều phân bổ thủ công, vì vậy việc không có tự động miễn phí không phải là vấn đề - họ chưa bao giờ làm điều đó trước đây, vậy tại sao bạn lại cần nó trong Java? Vấn đề không liên quan gì đến các chủ đề, hoặc stack / heap, có lẽ nó chỉ ở đó để phân bổ bộ nhớ (và do đó phân bổ lại) dễ dàng hơn một chút. Trong tất cả, câu lệnh try / cuối cùng có lẽ là một nơi tốt hơn để xử lý các tài nguyên không có bộ nhớ.

Vì vậy, IMHO, cách .NET đơn giản sao chép lỗ hổng lớn nhất của Java là điểm yếu lớn nhất của nó. .NET nên là một C ++ tốt hơn, không phải là một Java tốt hơn.


IMHO, những thứ như 'sử dụng' các khối là cách tiếp cận phù hợp để dọn dẹp xác định, nhưng cũng cần thêm một số điều nữa: (1) một phương tiện để đảm bảo rằng các đối tượng được xử lý nếu kẻ hủy diệt của chúng ném ngoại lệ; (2) một phương tiện tự động tạo ra một phương thức thường quy để gọi Disposetrên tất cả các trường được đánh dấu bằng một lệnh usingvà chỉ định xem có IDisposable.Disposenên tự động gọi nó không; (3) một lệnh tương tự using, nhưng sẽ chỉ gọi Disposetrong trường hợp ngoại lệ; (4) một biến thể trong IDisposableđó sẽ lấy một Exceptiontham số và ...
supercat

... sẽ được sử dụng tự động usingnếu thích hợp; tham số sẽ là nullnếu usingkhối thoát bình thường, nếu không thì sẽ chỉ ra ngoại lệ nào đang chờ xử lý nếu nó thoát qua ngoại lệ. Nếu những thứ như vậy tồn tại, việc quản lý tài nguyên hiệu quả và tránh rò rỉ sẽ dễ dàng hơn nhiều.
supercat

11

Bruce Eckel, tác giả của "Thinking in Java" và "Thinking in C ++" và là thành viên của Ủy ban Tiêu chuẩn C ++, cho rằng, trong nhiều lĩnh vực (không chỉ RAII), Gosling và nhóm Java đã không làm bài tập về nhà.

... Để hiểu làm thế nào ngôn ngữ có thể vừa khó chịu vừa phức tạp, đồng thời được thiết kế tốt, bạn phải ghi nhớ quyết định thiết kế chính mà mọi thứ trong C ++ đều treo: khả năng tương thích với C. Stroustrup đã quyết định - và đúng như vậy , nó sẽ xuất hiện - rằng cách để khiến cho khối lượng lập trình viên C di chuyển đến các đối tượng là làm cho việc di chuyển trở nên trong suốt: cho phép họ biên dịch mã C của họ không thay đổi trong C ++. Đây là một hạn chế rất lớn và luôn là thế mạnh lớn nhất của C ++ ... và là nguyên nhân chính của nó. Đó là những gì làm cho C ++ thành công như nó vốn có, và phức tạp như nó vốn có.

Nó cũng đánh lừa các nhà thiết kế Java, những người không hiểu rõ về C ++. Ví dụ, họ nghĩ rằng quá tải toán tử là quá khó để lập trình viên sử dụng đúng cách. Điều này về cơ bản là đúng trong C ++, vì C ++ có cả phân bổ ngăn xếp và phân bổ heap và bạn phải quá tải các toán tử của mình để xử lý tất cả các tình huống và không gây rò rỉ bộ nhớ. Khó thực sự. Tuy nhiên, Java có một cơ chế phân bổ lưu trữ duy nhất và trình thu gom rác, khiến cho toán tử quá tải tầm thường - như đã được trình bày trong C # (nhưng đã được hiển thị trong Python, trước Java). Nhưng trong nhiều năm, dòng một phần từ nhóm Java là "Quá tải toán tử quá phức tạp". Điều này và nhiều quyết định khác mà một người rõ ràng đã không '

Có rất nhiều ví dụ khác. Nguyên thủy "phải được đưa vào cho hiệu quả." Câu trả lời đúng là giữ đúng với "mọi thứ là một đối tượng" và cung cấp một cánh cửa bẫy để thực hiện các hoạt động cấp thấp hơn khi cần hiệu quả (điều này cũng sẽ cho phép các công nghệ hotspot làm cho mọi thứ hiệu quả hơn, vì cuối cùng chúng sẽ có). Ồ, và thực tế là bạn không thể sử dụng trực tiếp bộ xử lý dấu phẩy động để tính toán các hàm siêu việt (thay vào đó nó được thực hiện trong phần mềm). Tôi đã viết về những vấn đề như thế này nhiều nhất có thể, và câu trả lời tôi nghe được luôn là một câu trả lời mang tính chất taut cho hiệu ứng "đây là cách Java".

Khi tôi viết về việc các tướng được thiết kế tệ đến mức nào, tôi cũng nhận được phản hồi tương tự, cùng với "chúng ta phải tương thích ngược với các quyết định (xấu) trước đây được đưa ra trong Java". Gần đây ngày càng có nhiều người có đủ kinh nghiệm với Generics để thấy rằng chúng thực sự rất khó sử dụng - thực sự, các mẫu C ++ mạnh mẽ và nhất quán hơn nhiều (và giờ đây dễ sử dụng hơn khi các thông báo lỗi trình biên dịch có thể chấp nhận được). Mọi người thậm chí đã thực hiện việc thống nhất một cách nghiêm túc - điều gì đó sẽ hữu ích nhưng sẽ không đặt nhiều vấn đề vào một thiết kế bị tê liệt bởi những ràng buộc tự áp đặt.

Danh sách đi đến điểm thật tẻ nhạt ...


5
Điều này nghe giống như một câu trả lời Java so với C ++, thay vì tập trung vào RAII. Tôi nghĩ C ++ và Java là các ngôn ngữ khác nhau, mỗi ngôn ngữ có điểm mạnh và điểm yếu. Ngoài ra, các nhà thiết kế C ++ đã không làm bài tập về nhà của họ trong nhiều lĩnh vực (nguyên tắc KISS không được áp dụng, cơ chế nhập đơn giản cho các lớp bị thiếu, v.v.). Nhưng trọng tâm của câu hỏi là RAII: điều này bị thiếu trong Java và bạn phải lập trình nó theo cách thủ công.
Giorgio

4
@Giorgio: Quan điểm của bài viết là, Java dường như đã bỏ lỡ con thuyền về một số vấn đề, một số vấn đề liên quan trực tiếp đến RAII. Về C ++ và tác động của nó đối với Java, Eckels lưu ý: "Bạn phải ghi nhớ quyết định thiết kế chính mà mọi thứ trong C ++ đều treo: tương thích với C. Đây là một hạn chế rất lớn và luôn là thế mạnh lớn nhất của C ++ ... và bane. Nó cũng đánh lừa các nhà thiết kế Java, những người không hiểu rõ về C ++. " Thiết kế của C ++ ảnh hưởng trực tiếp đến Java, trong khi C # có cơ hội học hỏi từ cả hai. (Cho dù đó là một câu hỏi khác.)
Gnawme

2
@Giorgio Nghiên cứu các ngôn ngữ hiện có trong một mô hình cụ thể và gia đình ngôn ngữ thực sự là một phần của bài tập về nhà cần thiết để phát triển ngôn ngữ mới. Đây là một ví dụ trong đó họ chỉ đơn giản đánh hơi nó bằng Java. Họ đã có C ++ và Smalltalk để xem xét. C ++ không có Java để xem xét khi nó được phát triển.
Jeremy

1
@Gnawme: "Java dường như đã bỏ lỡ một số vấn đề, một số vấn đề liên quan trực tiếp đến RAII": bạn có thể đề cập đến những vấn đề này không? Bài viết bạn đăng không đề cập đến RAII.
Giorgio

2
@Giorgio Chắc chắn, đã có những đổi mới kể từ khi phát triển C ++, chiếm nhiều tính năng bạn thấy thiếu ở đó. Có bất kỳ tính năng nào mà họ nên tìm thấy khi xem xét các ngôn ngữ được thiết lập trước khi phát triển C ++ không? Đó là loại bài tập về nhà mà chúng ta đang nói về Java - không có lý do gì để họ không xem xét mọi tính năng C ++ trong phát ngôn của Java. Một số người thích thừa kế mà họ cố tình bỏ đi - những người khác như RAII mà họ dường như đã bỏ qua.
Jeremy

10

Lý do tốt nhất là đơn giản hơn nhiều so với hầu hết các câu trả lời ở đây.

Bạn không thể chuyển các đối tượng được phân bổ ngăn xếp cho các chủ đề khác.

Dừng lại và suy nghĩ về điều đó. Hãy tiếp tục suy nghĩ .... Bây giờ C ++ không có chủ đề khi mọi người rất quan tâm đến RAII. Ngay cả Erlang (heaps riêng cho mỗi luồng) cũng bị lỗi khi bạn vượt qua quá nhiều đối tượng xung quanh. C ++ chỉ có một mô hình bộ nhớ trong C ++ 2011; bây giờ bạn gần như có thể suy luận về sự tương tranh trong C ++ mà không cần phải tham khảo "tài liệu" của nhà biên dịch.

Java được thiết kế từ (gần như) ngày một cho nhiều luồng.

Tôi vẫn còn có bản sao cũ của "Ngôn ngữ lập trình C ++" nơi Stroustrup đảm bảo với tôi rằng tôi sẽ không cần các chủ đề.

Lý do đau đớn thứ hai là để tránh cắt lát.


1
Java được thiết kế cho nhiều luồng cũng giải thích lý do tại sao GC không dựa trên việc đếm tham chiếu.
dan04

4
@NemanjaTrifunovic: Bạn không thể so sánh C ++ / CLI với Java hoặc C #, nó được thiết kế gần như cho mục đích rõ ràng là tương tác với mã C / C ++ không được quản lý; nó giống như một ngôn ngữ không được quản lý để cung cấp quyền truy cập vào .NET framework hơn là ngược lại.
Aaronaught

3
@NemanjaTrifunovic: Có, C ++ / CLI là một ví dụ về cách nó có thể được thực hiện theo cách hoàn toàn không phù hợp cho các ứng dụng thông thường . Nó chỉ hữu ích cho interop C / C ++. Không chỉ các nhà phát triển bình thường không cần phải chịu một quyết định "stack hay heap" hoàn toàn không liên quan, mà nếu bạn từng cố gắng cấu trúc lại nó thì việc vô tình dễ dàng tạo ra một con trỏ null / lỗi tham chiếu và / hoặc rò rỉ bộ nhớ. Xin lỗi, nhưng tôi phải tự hỏi liệu bạn đã thực sự lập trình bằng Java hay C # chưa, bởi vì tôi không nghĩ bất kỳ ai thực sự muốn sử dụng ngữ nghĩa được sử dụng trong C ++ / CLI.
Aaronaught

2
@Aaronaught: Tôi đã lập trình với cả Java (một chút) và C # (rất nhiều) và dự án hiện tại của tôi có khá nhiều C #. Hãy tin tôi, tôi biết những gì tôi đang nói và không liên quan gì đến "stack vs. heap" - nó có mọi thứ để đảm bảo rằng tất cả tài nguyên của bạn được giải phóng ngay khi bạn không cần chúng. Tự động. Nếu họ không - bạn sẽ gặp rắc rối.
Nemanja Trifunovic

4
@NemanjaTrifunovic: Điều đó thật tuyệt, thực sự tuyệt vời, nhưng cả C # và C ++ / CLI đều yêu cầu bạn phải nói rõ khi bạn muốn điều này xảy ra, họ chỉ cần sử dụng một cú pháp khác nhau. Không ai tranh cãi về điểm cốt yếu mà bạn hiện đang lan man (rằng "tài nguyên được giải phóng ngay khi bạn không cần đến chúng") nhưng bạn đang thực hiện một bước nhảy vọt logic đến "tất cả các ngôn ngữ được quản lý chỉ nên tự động nhưng chỉ -sort-of xử lý xác định dựa trên ngăn xếp cuộc gọi ". Nó chỉ không giữ nước.
Aaronaught

5

Trong C ++, bạn sử dụng các tính năng ngôn ngữ cấp thấp hơn, đa mục đích hơn (các hàm hủy tự động được gọi trên các đối tượng dựa trên ngăn xếp) để triển khai một cấp độ cao hơn (RAII) và cách tiếp cận này là thứ mà mọi người C # / Java dường như không có quá thích Họ muốn thiết kế các công cụ cấp cao cụ thể cho các nhu cầu cụ thể và cung cấp chúng cho các lập trình viên sẵn sàng, được tích hợp vào ngôn ngữ. Vấn đề với các công cụ cụ thể như vậy là chúng thường không thể tùy chỉnh (một phần đó là điều khiến chúng rất dễ học). Khi xây dựng từ các khối nhỏ hơn, một giải pháp tốt hơn có thể xuất hiện theo thời gian, trong khi nếu bạn chỉ có các cấu trúc tích hợp cao cấp, thì điều này ít có khả năng.

Vì vậy, vâng, tôi nghĩ (tôi thực sự không ở đó ...) đó là một quyết định khó hiểu, với mục tiêu làm cho các ngôn ngữ dễ tiếp thu hơn, nhưng theo tôi, đó là một quyết định tồi. Sau đó, một lần nữa, tôi thường thích triết lý cho người lập trình C ++, một cơ hội để tự tung ra triết lý của riêng họ, vì vậy tôi hơi thiên vị.


7
"Triết lý cho người lập trình-một cơ hội để tự lăn lộn" hoạt động tốt UNTIL để bạn cần kết hợp các thư viện được viết bởi các lập trình viên, những người từng cuộn các lớp chuỗi của riêng họ và con trỏ thông minh.
dan04

@ dan04 vì vậy các ngôn ngữ được quản lý cung cấp cho bạn các lớp chuỗi được xác định trước, sau đó cho phép bạn vá chúng, đây là một công thức cho thảm họa nếu bạn là loại người không thể đối phó với một chuỗi cuộn khác lớp học.
gbjbaanb

-1

Bạn đã gọi ra phần thô tương đương với điều này trong C # với Disposephương thức. Java cũng có finalize. LƯU Ý: Tôi nhận thấy rằng quyết toán của Java là không xác định và khác với Dispose, tôi chỉ chỉ ra rằng cả hai đều có một phương pháp làm sạch tài nguyên cùng với GC.

Nếu bất cứ điều gì C ++ trở nên đau đớn hơn bởi vì một đối tượng phải bị phá hủy về mặt vật lý. Trong các ngôn ngữ cấp cao hơn như C # và Java, chúng tôi phụ thuộc vào trình thu gom rác để dọn sạch khi không còn tham chiếu đến nó. Không có gì đảm bảo rằng đối tượng DBConnection trong C ++ không có các tham chiếu hoặc con trỏ giả mạo đối với nó.

Có, mã C ++ có thể trực quan hơn để đọc nhưng có thể là cơn ác mộng để gỡ lỗi vì các ranh giới và giới hạn mà các ngôn ngữ như Java đưa ra đã loại trừ một số lỗi khó khăn và nghiêm trọng hơn cũng như bảo vệ các nhà phát triển khác khỏi các lỗi tân binh thông thường.

Có lẽ nó tùy thuộc vào sở thích, một số như sức mạnh, sự kiểm soát và độ tinh khiết của C ++ ở mức độ thấp, nơi những người khác giống như tôi thích một ngôn ngữ hộp cát rõ ràng hơn nhiều.


12
Trước hết, "hoàn thiện" của Java là không xác định ... nó không tương đương với "xử lý" của C # hoặc các hàm hủy của C ++ ... ngoài ra, C ++ cũng có trình thu gom rác nếu bạn sử dụng .NET
JoelFan

2
@DeadMG: Vấn đề là, bạn có thể không phải là một thằng ngốc, nhưng anh chàng khác vừa rời khỏi công ty (và người đã viết mã mà bạn hiện đang duy trì) có thể đã bị.
Kevin

7
Gã đó sẽ viết mã shitty bất cứ điều gì bạn làm. Bạn không thể lấy một lập trình viên tồi và khiến anh ta viết mã tốt. Con trỏ lơ lửng là mối quan tâm ít nhất của tôi khi làm việc với những kẻ ngốc. Các tiêu chuẩn mã hóa tốt sử dụng các con trỏ thông minh cho bộ nhớ phải được theo dõi, vì vậy quản lý thông minh nên làm rõ cách phân bổ và truy cập bộ nhớ một cách an toàn.
DeadMG

3
Những gì DeadMG nói. Có nhiều điều tồi tệ về C ++. Nhưng RAII không phải là một trong số họ kéo dài. Trong thực tế, việc thiếu Java và .NET để giải thích chính xác việc quản lý tài nguyên (vì bộ nhớ là tài nguyên duy nhất, phải không?) Là một trong những vấn đề lớn nhất của họ.
Konrad Rudolph

8
Theo tôi, quyết toán là một thiết kế thảm họa. Vì bạn đang buộc sử dụng đúng một đối tượng từ người thiết kế cho người dùng đối tượng (không phải về mặt quản lý bộ nhớ mà là quản lý tài nguyên). Trong C ++, trách nhiệm của người thiết kế lớp là quản lý tài nguyên chính xác (chỉ được thực hiện một lần). Trong Java, trách nhiệm của người dùng lớp là quản lý tài nguyên đúng và do đó phải được thực hiện mỗi khi lớp chúng ta sử dụng. stackoverflow.com/questions/161177/ trộm
Martin York
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.