Sự khác biệt về khái niệm giữa cuối cùng và một kẻ hủy diệt là gì?


12

Đầu tiên, tôi nhận thức rõ tại sao không có cấu trúc 'cuối cùng' trong C ++? nhưng một cuộc thảo luận bình luận kéo dài về một câu hỏi khác dường như đảm bảo một câu hỏi riêng biệt.

Ngoài vấn đề là finallytrong C # và Java về cơ bản chỉ có thể tồn tại một lần (== 1) cho mỗi phạm vi và một phạm vi có thể có nhiều hàm hủy (C = = n) C ++, tôi nghĩ rằng về cơ bản chúng giống nhau. (Với một số khác biệt về kỹ thuật.)

Tuy nhiên, một người dùng khác lập luận :

... Tôi đã cố gắng nói rằng một dtor vốn dĩ là một công cụ cho (Phát hành sơ đồ) và cuối cùng vốn là một công cụ cho (Cam kết ngữ nghĩa). Nếu bạn không thấy lý do tại sao: hãy xem xét lý do tại sao việc ném các ngoại lệ lên nhau trong các khối cuối cùng là hợp pháp và tại sao điều tương tự lại không dành cho kẻ hủy diệt. (Theo một nghĩa nào đó, đó là dữ liệu so với điều khiển. Công cụ phá hủy là để phát hành dữ liệu, cuối cùng là để phát hành quyền kiểm soát. Chúng khác nhau; thật không may khi C ++ liên kết chúng lại với nhau.)

Ai đó có thể làm rõ điều này?

Câu trả lời:


6
  • Giao dịch ( try)
  • Lỗi đầu ra / phản hồi ( catch)
  • Lỗi bên ngoài ( throw)
  • Lỗi lập trình viên ( assert)
  • Phục hồi (điều gần nhất có thể là người bảo vệ phạm vi trong các ngôn ngữ hỗ trợ họ nguyên bản)
  • Phát hành tài nguyên (hủy diệt)
  • Lưu lượng kiểm soát độc lập giao dịch ( finally)

Không thể đưa ra một mô tả tốt hơn cho finallyluồng kiểm soát độc lập giao dịch linh tinh. Nó không nhất thiết phải ánh xạ trực tiếp đến bất kỳ khái niệm cấp cao nào trong bối cảnh tư duy phục hồi giao dịch và lỗi, đặc biệt là trong một ngôn ngữ lý thuyết có cả hàm hủy và finally.

Điều mà tôi thiếu nhất đối với tôi là một tính năng ngôn ngữ đại diện trực tiếp cho khái niệm đẩy lùi các tác dụng phụ bên ngoài. Phạm vi bảo vệ trong các ngôn ngữ như D là điều gần gũi nhất mà tôi có thể nghĩ đến gần với việc thể hiện khái niệm đó. Từ quan điểm của luồng điều khiển, việc khôi phục trong phạm vi của một chức năng cụ thể sẽ cần phân biệt một đường dẫn đặc biệt với một đường dẫn thông thường, đồng thời tự động hóa việc khôi phục lại bất kỳ tác dụng phụ nào do chức năng gây ra khi giao dịch thất bại, nhưng không phải khi giao dịch thành công . Điều đó đủ dễ thực hiện với các hàm hủy nếu chúng ta nói, đặt một giá trị boolean thành giá trị như succeededđúng ở cuối khối thử của chúng ta để ngăn logic rollback trong hàm hủy. Nhưng đó là một cách khá tròn để làm điều này.

Mặc dù điều đó có vẻ như sẽ không tiết kiệm được nhiều, nhưng đảo ngược tác dụng phụ là một trong những điều khó nhất để làm đúng (ví dụ: điều gì gây khó khăn cho việc viết một thùng chứa chung an toàn ngoại lệ).


4

Theo cách chúng - giống như cách mà cả Ferrari và phương tiện đều có thể được sử dụng để hạ gục các cửa hàng để lấy một cốc sữa mặc dù chúng được thiết kế cho các mục đích sử dụng khác nhau.

Bạn có thể đặt cấu trúc thử / cuối cùng trong mọi phạm vi và dọn sạch tất cả các biến được xác định phạm vi trong khối cuối cùng để mô phỏng hàm hủy C ++. Về mặt khái niệm, đây là những gì C ++ làm - trình biên dịch sẽ tự động gọi hàm hủy khi một biến đi ra khỏi phạm vi (tức là ở cuối khối phạm vi). Bạn sẽ phải sắp xếp thử / cuối cùng để thử là điều đầu tiên và cuối cùng là điều cuối cùng trong mỗi phạm vi. Bạn cũng phải xác định một tiêu chuẩn cho từng đối tượng để có một phương thức được đặt tên cụ thể mà nó sử dụng để dọn sạch trạng thái mà bạn sẽ gọi trong khối cuối cùng, mặc dù tôi đoán bạn có thể rời khỏi quản lý bộ nhớ thông thường mà ngôn ngữ của bạn cung cấp làm sạch các đối tượng bây giờ trống rỗng khi nó thích.

Mặc dù vậy, sẽ không tốt nếu làm điều này và mặc dù .NET đã giới thiệu IDispose như một công cụ hủy được quản lý thủ công và sử dụng các khối như một nỗ lực để làm cho việc quản lý thủ công đó dễ dàng hơn một chút, nhưng đó vẫn không phải là điều bạn muốn làm trong thực tế .


4

Theo quan điểm của tôi, sự khác biệt chính là một hàm hủy trong c ++ là một cơ chế ngầm (được gọi tự động) để giải phóng các tài nguyên được phân bổ trong khi thử ... cuối cùng có thể được sử dụng như một cơ chế rõ ràng để làm điều đó.

Trong các chương trình c ++, lập trình viên chịu trách nhiệm giải phóng các tài nguyên được phân bổ. Điều này thường được thực hiện trong hàm hủy của một lớp và được thực hiện ngay lập tức khi một biến đi ra khỏi phạm vi hoặc khi xóa được gọi.

Khi trong c ++, một biến cục bộ của một lớp được tạo mà không sử dụng newtài nguyên của các thể hiện đó được giải phóng ngầm định bởi hàm hủy khi có ngoại lệ.

// c++
void test() {
    MyClass myClass(someParameter);
    // if there is an exception the destructor of MyClass is called automatically
    // this does not work with
    // MyClass* pMyClass = new MyClass(someParameter);

} // on test() exit the destructor of myClass is implicitly called

Trong java, c # và các hệ thống khác có quản lý bộ nhớ tự động, trình thu gom rác hệ thống quyết định khi một cá thể lớp bị phá hủy.

// c#
void test() {
    MyClass myClass = new MyClass(someParameter);
    // if there is an exception myClass is NOT destroyed so there may be memory/resource leakes

    myClass.destroy(); // this is never called
}

Không có cơ chế ngầm nào để bạn phải lập trình rõ ràng bằng cách sử dụng thử cuối cùng

// c#
void test() {
    MyClass myClass = null;

    try {
        myClass = new MyClass(someParameter);
        ...
    } finally {
        // explicit memory management
        // even if there is an exception myClass resources are freed
        myClass.destroy();
    }

    myClass.destroy(); // this is never called
}

Trong C ++, tại sao hàm hủy chỉ được gọi tự động với đối tượng ngăn xếp mà không phải với đối tượng heap trong trường hợp ngoại lệ?
Giorgio

@Giorgio Vì tài nguyên heap sống trong một không gian bộ nhớ không được liên kết trực tiếp với ngăn xếp cuộc gọi. Ví dụ, hãy tưởng tượng một ứng dụng đa luồng có 2 luồng AB. Nếu một luồng bị ném, A'sgiao dịch quay ngược sẽ không phá hủy các tài nguyên được phân bổ B, ví dụ - các trạng thái luồng độc lập với nhau và bộ nhớ liên tục sống trên heap độc lập với cả hai. Tuy nhiên, thông thường trong C ++, bộ nhớ heap vẫn được gắn với các đối tượng trên ngăn xếp.

@Giorgio Ví dụ: std::vector đối tượng có thể sống trên ngăn xếp nhưng chỉ vào bộ nhớ trên heap - cả đối tượng vectơ (trên ngăn xếp) và nội dung của nó (trên heap) sẽ bị hủy bỏ trong một ngăn xếp thư giãn trong trường hợp đó, vì phá hủy vectơ trên ngăn xếp sẽ gọi một hàm hủy giải phóng bộ nhớ liên quan trên heap (và tương tự phá hủy các phần tử heap đó). Thông thường để đảm bảo an toàn ngoại lệ, hầu hết các đối tượng C ++ đều sống trên stack, ngay cả khi chúng chỉ xử lý trỏ vào bộ nhớ trên heap, tự động hóa quá trình giải phóng cả heap và stack stack trên stack stack.

4

Vui mừng bạn đăng điều này như một câu hỏi. :)

Tôi đã cố gắng nói rằng các kẻ hủy diệt và finallykhác nhau về mặt khái niệm:

  • Phá hủy là để giải phóng tài nguyên ( dữ liệu )
  • finallylà để trả lại cho người gọi ( điều khiển )

Hãy xem xét, giả sử mã giả định này:

try {
    bar();
} finally {
    logfile.print("bar has exited...");
}

finallyđây là giải quyết hoàn toàn một vấn đề kiểm soát và không phải là vấn đề quản lý tài nguyên.
Sẽ không có nghĩa gì khi làm điều đó trong một kẻ hủy diệt vì nhiều lý do:

  • Không có thứ gì được "mua lại" hoặc "tạo ra"
  • Việc không in ra tệp nhật ký sẽ không dẫn đến rò rỉ tài nguyên, hỏng dữ liệu, v.v. (giả sử rằng tệp logfile ở đây không được đưa trở lại chương trình ở nơi khác)
  • Đó là hợp pháp logfile.printđể thất bại, trong khi phá hủy (về mặt khái niệm) không thể thất bại

Đây là một ví dụ khác, lần này giống như trong Javascript:

var mo_document = document, mo;
function observe(mutations) {
    mo.disconnect();  // stop observing changes to prevent re-entrance
    try {
        /* modify stuff */
    } finally {
        mo.observe(mo_document);  // continue observing (conceptually, this can fail)
    }
}
mo = new MutationObserver(observe);
return observe();

Trong ví dụ trên, một lần nữa, không có tài nguyên nào được phát hành.
Trên thực tế, finallykhối này đang có được các nguồn lực bên trong để đạt được mục tiêu của mình, điều này có khả năng thất bại. Do đó, không có nghĩa gì khi sử dụng hàm hủy (nếu Javascript có).

Mặt khác, trong ví dụ này:

b = get_data();
try {
    a.write(b);
} finally {
    free(b);
}

finallyđang phá hủy một nguồn tài nguyên b,. Đó là một vấn đề dữ liệu. Vấn đề không nằm ở việc trả lại quyền kiểm soát cho người gọi một cách sạch sẽ, mà là về việc tránh rò rỉ tài nguyên.
Thất bại không phải là một lựa chọn, và (về mặt khái niệm) không bao giờ xảy ra.
Mỗi bản phát hành bnhất thiết phải được kết hợp với việc mua lại và sử dụng RAII là hợp lý.

Nói cách khác, chỉ vì bạn có thể sử dụng hoặc để mô phỏng hoặc điều đó không có nghĩa là cả hai đều là một và cùng một vấn đề hoặc cả hai đều là giải pháp phù hợp cho cả hai vấn đề.


Cảm ơn. Tôi không đồng ý, nhưng này :-) Tôi nghĩ rằng tôi sẽ có thể thêm một câu trả lời quan điểm đối lập kỹ lưỡng trong những ngày tiếp theo ...
Martin Ba

2
Làm thế nào mà thực tế finallychủ yếu được sử dụng để giải phóng yếu tố tài nguyên (không phải bộ nhớ) vào điều này?
Bart van Ingen Schenau 2/12/2015

1
@BartvanIngenSchenau: Tôi chưa bao giờ tranh luận rằng bất kỳ ngôn ngữ nào hiện đang tồn tại đều có triết lý hoặc cách thực hiện phù hợp với những gì tôi mô tả. Mọi người chưa hoàn thành việc phát minh ra mọi thứ có thể tồn tại. Tôi chỉ lập luận rằng sẽ có giá trị trong việc tách hai khái niệm vì chúng là những ý tưởng khác nhau và có các trường hợp sử dụng khác nhau. Để thỏa mãn sự tò mò của bạn, tôi tin rằng D có cả hai. Có lẽ có những ngôn ngữ khác nữa. Mặc dù vậy, tôi không xem nó có liên quan và tôi không quan tâm đến việc tại sao ví dụ Java lại được ưa chuộng finally.
dùng541686

1
Một ví dụ thực tế tôi gặp trong JavaScript là các hàm tạm thời thay đổi con trỏ chuột thành đồng hồ cát trong một số thao tác dài (có thể ném ngoại lệ), sau đó thay đổi nó trở lại bình thường trong finallymệnh đề. Thế giới quan C ++ sẽ giới thiệu một lớp quản lý tài nguyên này của một người chuyển nhượng cho một biến toàn cục giả. Ý nghĩa khái niệm nào làm cho? Nhưng các hàm hủy là búa của C ++ để thực thi mã cuối khối bắt buộc.
dan04

1
@ dan04: Cảm ơn rất nhiều, đó là ví dụ hoàn hảo cho việc này. Tôi có thể thề rằng tôi đã gặp rất nhiều tình huống mà RAII không có ý nghĩa nhưng tôi đã rất khó khăn khi nghĩ về chúng.
dùng541686

1

Câu trả lời của k3b thực sự là cụm từ độc đáo:

một hàm hủy trong c ++ là một cơ chế ngầm (được gọi tự động) để giải phóng các tài nguyên được phân bổ trong khi thử ... cuối cùng có thể được sử dụng như một bản tường minh cơ chế để làm điều đó.

Về "tài nguyên", tôi muốn tham khảo Jon Kalb: RAII có nghĩa là Khởi tạo trách nhiệm là Khởi tạo .

Dù sao, như đối với ngầm so với rõ ràng điều này dường như thực sự là nó:

  • Một d'tor là một công cụ để xác định những hoạt động sẽ xảy ra - hoàn toàn - khi thời gian tồn tại của một đối tượng kết thúc (thường trùng với kết thúc của phạm vi)
  • Khối cuối cùng là một công cụ để xác định - một cách rõ ràng - những hoạt động nào sẽ xảy ra ở cuối phạm vi.
  • Thêm vào đó, về mặt kỹ thuật, bạn luôn được phép ném từ cuối cùng, nhưng xem bên dưới.

Tôi nghĩ đó là phần khái niệm, ...


... bây giờ có IMHO một số chi tiết thú vị:

Tôi cũng không nghĩ rằng c'tor / d'tor cần khái niệm "thu nhận" hoặc "tạo ra" bất cứ thứ gì, ngoài trách nhiệm chạy một số mã trong hàm hủy. Đó là những gì cuối cùng cũng làm: chạy một số mã.

Và mặc dù mã trong một khối cuối cùng chắc chắn có thể tạo ra một ngoại lệ, nhưng điều đó không đủ để tôi nói rằng chúng khác nhau về mặt khái niệm so với rõ ràng so với ẩn.

(Thêm vào đó, tôi không tin chắc rằng mã "tốt" cuối cùng sẽ được đưa ra - có lẽ đó là một câu hỏi hoàn toàn khác đối với chính nó.)


Suy nghĩ của bạn về ví dụ Javascript của tôi là gì?
dùng541686

Liên quan đến các lập luận khác của bạn: "Chúng tôi có thực sự muốn đăng nhập cùng một thứ không phân biệt không?" Vâng, đó chỉ là một ví dụ và bạn đang thiếu điểm, và vâng, không ai bị cấm đăng nhập chi tiết cụ thể hơn cho từng trường hợp. Vấn đề ở đây là bạn chắc chắn không thể khẳng định rằng không bao giờ có tình huống mà bạn muốn đăng nhập một cái gì đó chung cho cả hai. Một số mục nhật ký là chung chung, một số là cụ thể; bạn muốn cả hai. Và một lần nữa, bạn hoàn toàn thiếu quan điểm bằng cách tập trung vào việc đăng nhập. Tạo động lực cho các ví dụ 10 dòng là khó; hãy cố gắng để không bỏ lỡ điểm.
dùng541686

Bạn chưa bao giờ giải quyết những điều này ...
user541686 8/12/2015

@Mehrdad - Tôi đã không giải quyết ví dụ javascript của bạn bởi vì nó sẽ đưa tôi một trang khác để thảo luận về những gì tôi nghĩ về nó. (Tôi đã thử, nhưng tôi mất rất nhiều thời gian để diễn đạt một cái gì đó mạch lạc mà tôi đã bỏ qua :-)
Martin Ba

@Mehrdad - đối với các điểm khác của bạn - có vẻ như chúng tôi phải đồng ý không đồng ý. Tôi thấy nơi bạn đang nhắm đến với sự khác biệt, nhưng tôi không tin rằng chúng là một thứ gì đó khác biệt về mặt khái niệm: Chủ yếu bởi vì tôi chủ yếu ở trong trại nghĩ rằng ném từ cuối cùng là một ý tưởng thực sự tồi tệ ( lưu ý : Tôi cũng nghĩ trong observerví dụ của bạn rằng ném sẽ có một ý tưởng thực sự tồi tệ.) Hãy thoải mái mở một cuộc trò chuyện, nếu bạn muốn thảo luận thêm về điều này. Đó chắc chắn là niềm vui khi nghĩ về lập luận của bạn. Chúc mừng.
Martin Ba
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.