Phần 'cuối cùng' của cấu trúc 'thử bắt bắt cuối cùng' có cần thiết không?


25

Một số ngôn ngữ (như C ++ và các phiên bản đầu tiên của PHP) không hỗ trợ finallyphần try ... catch ... finallycấu trúc. Có finallybao giờ cần thiết? Bởi vì mã trong nó luôn chạy, tại sao tôi không / không nên đặt mã đó sau một try ... catchkhối mà không có finallymệnh đề? Tại sao nên sử dụng một? (Tôi đang tìm kiếm một lý do / động lực cho việc sử dụng / không sử dụng finally, không phải là lý do để loại bỏ 'bắt' hoặc tại sao nó hợp pháp để làm như vậy.)


Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
maple_shaft

Câu trả lời:


36

Ngoài những gì người khác đã nói, cũng có thể có một ngoại lệ được ném vào trong mệnh đề bắt. Xem xét điều này:

try { 
    throw new SomeException();
} catch {
    DoSomethingWhichUnexpectedlyThrows();
}
Cleanup();

Trong ví dụ này, Cleanup()hàm không bao giờ chạy, bởi vì một ngoại lệ được đưa vào mệnh đề bắt và lần bắt cao nhất tiếp theo trong ngăn xếp cuộc gọi sẽ bắt được điều đó. Sử dụng một khối cuối cùng sẽ loại bỏ rủi ro này và làm cho mã sạch hơn để khởi động.


4
Cảm ơn bạn đã trả lời ngắn gọn và trực tiếp mà không đi sâu vào lý thuyết và 'ngôn ngữ X tốt hơn lãnh thổ của Y'.
Búa búa Agi

56

Như những người khác đã đề cập, không có gì đảm bảo rằng mã sau một trycâu lệnh sẽ được thực thi trừ khi bạn nắm bắt mọi ngoại lệ có thể. Điều đó nói rằng, điều này:

try {
   mightThrowSpecificException();
} catch (SpecificException e) {
   handleError();
} finally {
   cleanUp();
}

có thể viết lại 1 như:

try {
   mightThrowSpecificException();
} catch (SpecificException e) {
   try {
       handleError();
   } catch (Throwable e2) {
       cleanUp();
       throw e2;
   }
} catch (Throwable e) {
   cleanUp();
   throw e;
}
cleanUp();

Nhưng cái sau đòi hỏi bạn phải nắm bắt tất cả các ngoại lệ chưa được xử lý, sao chép mã dọn dẹp và nhớ ném lại. Vì vậy, finallykhông cần thiết , nhưng nó hữu ích .

C ++ không có finallyBjarne Stroustrup tin rằng RAII tốt hơn hoặc ít nhất là đủ cho hầu hết các trường hợp:

Tại sao C ++ không cung cấp cấu trúc "cuối cùng"?

Bởi vì C ++ hỗ trợ một giải pháp thay thế gần như luôn luôn tốt hơn: Kỹ thuật "thu nhận tài nguyên là khởi tạo" (TC ++ PL3 phần 14.4). Ý tưởng cơ bản là biểu diễn một tài nguyên bởi một đối tượng cục bộ, để hàm hủy của đối tượng cục bộ sẽ giải phóng tài nguyên. Bằng cách đó, lập trình viên không thể quên giải phóng tài nguyên.


1 Mã cụ thể để bắt tất cả các ngoại lệ và suy nghĩ lại mà không mất thông tin theo dõi ngăn xếp thay đổi theo ngôn ngữ. Tôi đã sử dụng Java, trong đó dấu vết ngăn xếp được ghi lại khi ngoại lệ được tạo. Trong C # bạn chỉ cần sử dụng throw;.


8
Bạn cũng phải bắt Ngoại lệ trong handleError()trường hợp thứ hai, phải không?
Juri Robl

1
Bạn cũng có thể đang ném một lỗi. Tôi sẽ viết lại rằng catch (Throwable t) {}, với khối thử .. bắt xung quanh toàn bộ khối ban đầu (cũng sẽ bắt được các vật ném handleErrorđược)
njzk2

1
Tôi thực sự đã thêm phần thử thêm mà bạn đã bỏ qua khi gọi handleErro();, điều này sẽ khiến cuộc tranh luận tốt hơn về lý do tại sao các khối cuối cùng lại hữu ích (mặc dù đó không phải là câu hỏi ban đầu).
Alex

1
Câu trả lời này không thực sự giải quyết được câu hỏi tại sao C ++ không có finally, nó mang nhiều sắc thái hơn.
DeadMG

1
@AgiHammerthief Các lồng nhau trylà bên trong catchcho cụ thể là ngoại lệ. Thứ hai, có thể bạn không biết liệu bạn có thể xử lý lỗi thành công hay không cho đến khi bạn kiểm tra ngoại lệ hoặc nguyên nhân của ngoại lệ đó cũng ngăn bạn xử lý lỗi (ít nhất là ở mức đó). Điều đó khá phổ biến khi thực hiện I / O. Việc suy nghĩ lại là có bởi vì cách duy nhất để đảm bảo các cleanUplần chạy là bắt tất cả mọi thứ , nhưng mã ban đầu sẽ cho phép các ngoại lệ bắt nguồn từ catch (SpecificException e)khối truyền lên trên.
Doval

22

finally các khối thường được sử dụng để dọn sạch các tài nguyên có thể giúp dễ đọc khi sử dụng nhiều câu lệnh return:

int DoSomething() {
    try {
        open_connection();
        return get_result();
    }
    catch {
        return 2;
    }
    finally {
        close_connection();
    }
}

đấu với

int DoSomething() {
    int result;
    try {
        open_connection();
        result = get_result();
    }
    catch {
        result = 2;
    }
    close_connection();
    return result;
}

2
Tôi nghĩ rằng đây là câu trả lời tốt nhất. Sử dụng một cuối cùng như là một thay thế cho một ngoại lệ chung chỉ có vẻ shitty. Trường hợp sử dụng đúng là để dọn dẹp tài nguyên hoặc các hoạt động tương tự.
Kik

3
Có lẽ thậm chí phổ biến hơn là trở lại bên trong khối thử, thay vì bên trong khối bắt.
Michael Anderson

Theo tôi, mã không giải thích thỏa đáng việc sử dụng finally. (Tôi sẽ sử dụng mã như trong khối thứ hai vì nhiều tuyên bố trả lại không được khuyến khích ở nơi tôi làm việc.)
Agi Hammerthief

15

Như bạn đã rõ ràng đã đoán, vâng, C ++ cung cấp các khả năng tương tự mà không cần cơ chế đó. Như vậy, nói đúng ra, cơ chế try/ finallylà không thực sự cần thiết.

Điều đó nói rằng, làm mà không có nó áp đặt một số yêu cầu trên cách phần còn lại của ngôn ngữ được thiết kế. Trong C ++, tập hợp các hành động tương tự được thể hiện trong hàm hủy của lớp. Điều này hoạt động chủ yếu (độc quyền?) Bởi vì lời gọi hàm hủy trong C ++ mang tính xác định. Điều này, đến lượt nó, dẫn đến một số quy tắc khá phức tạp về thời gian sống của đối tượng, một số trong đó được quyết định là không trực quan.

Hầu hết các ngôn ngữ khác cung cấp một số hình thức thu gom rác thay thế. Mặc dù có những điều về thu gom rác gây tranh cãi (ví dụ, hiệu quả của nó so với các phương pháp quản lý bộ nhớ khác), một điều thường không phải là: thời gian chính xác khi một đối tượng sẽ được "dọn dẹp" bởi trình thu gom rác không được gắn trực tiếp đến phạm vi của đối tượng. Điều này ngăn chặn việc sử dụng nó khi dọn dẹp cần phải có tính xác định hoặc khi nó chỉ đơn giản là cần thiết cho hoạt động chính xác hoặc khi xử lý các tài nguyên quý giá đến mức hầu hết việc dọn dẹp của chúng không bị trì hoãn một cách tùy tiện. try/ finallycung cấp một cách để các ngôn ngữ đó xử lý các tình huống đòi hỏi phải dọn dẹp xác định.

Tôi nghĩ rằng những người cho rằng cú pháp C ++ cho khả năng này là "kém thân thiện" hơn so với Java là khá thiếu điểm. Tồi tệ hơn, họ đang thiếu một điểm quan trọng hơn nhiều về sự phân chia trách nhiệm vượt xa cú pháp và có nhiều việc phải làm với cách thiết kế mã.

Trong C ++, việc dọn dẹp xác định này xảy ra trong hàm hủy của đối tượng. Điều đó có nghĩa là đối tượng có thể (và thông thường nên được) được thiết kế để tự dọn dẹp. Điều này đi vào bản chất của thiết kế hướng đối tượng - một lớp nên được thiết kế để cung cấp một sự trừu tượng hóa và thực thi các bất biến của chính nó. Trong C ++, người ta thực hiện chính xác điều đó - và một trong những bất biến mà nó cung cấp là khi đối tượng bị phá hủy, các tài nguyên được điều khiển bởi đối tượng đó (tất cả chúng, không chỉ bộ nhớ) sẽ bị hủy một cách chính xác.

Java (và tương tự) có phần khác nhau. Mặc dù họ (hỗ trợ) finalizevề mặt lý thuyết có thể cung cấp các khả năng tương tự, nhưng sự hỗ trợ đó yếu đến mức về cơ bản nó không thể sử dụng được (và trên thực tế, về cơ bản không bao giờ được sử dụng).

Kết quả là, thay vì bản thân lớp có thể thực hiện việc dọn dẹp cần thiết, khách hàng của lớp cần thực hiện các bước để làm như vậy. Nếu chúng ta thực hiện một so sánh đủ thiển cận, thoạt nhìn có thể thấy rằng sự khác biệt này là khá nhỏ và Java khá cạnh tranh với C ++ về mặt này. Chúng tôi kết thúc với một cái gì đó như thế này. Trong C ++, lớp trông giống như thế này:

class Foo {
    // ...
public:
    void do_whatever() { if (xyz) throw something; }
    ~Foo() { /* handle cleanup */ }
};

... và mã máy khách trông giống như thế này:

void f() { 
    Foo f;
    f.do_whatever();
    // possibly more code that might throw here
}

Trong Java, chúng ta trao đổi thêm một chút mã trong đó đối tượng được sử dụng ít hơn một chút trong lớp. Điều này ban đầu trông giống như một sự đánh đổi thậm chí khá. Trong thực tế, nó cách xa nó, bởi vì trong hầu hết các mã điển hình, chúng tôi chỉ định nghĩa lớp ở một nơi, nhưng chúng tôi sử dụng nó ở nhiều nơi. Cách tiếp cận C ++ có nghĩa là chúng ta chỉ viết mã đó để xử lý việc dọn dẹp ở một nơi. Cách tiếp cận Java có nghĩa là chúng ta phải viết mã đó để xử lý việc dọn dẹp nhiều lần, ở nhiều nơi - mỗi nơi chúng ta sử dụng một đối tượng của lớp đó.

Nói tóm lại, cách tiếp cận Java về cơ bản đảm bảo rằng nhiều trừu tượng mà chúng tôi cố gắng cung cấp là "rò rỉ" - bất kỳ và mọi lớp yêu cầu dọn dẹp xác định đều bắt buộc khách hàng của lớp phải biết về các chi tiết về việc dọn dẹp và cách làm sạch , thay vì những chi tiết bị ẩn trong chính lớp.

Mặc dù tôi đã gọi nó là "cách tiếp cận Java" ở trên, try/ finallyvà các cơ chế tương tự dưới các tên khác không hoàn toàn bị hạn chế đối với Java. Đối với một ví dụ nổi bật, hầu hết (tất cả?) Của các ngôn ngữ .NET (ví dụ: C #) đều cung cấp giống nhau.

Các lần lặp lại gần đây của cả Java và C # cũng cung cấp một cái gì đó ở nửa điểm giữa Java "cổ điển" và C ++ về vấn đề này. Trong C #, một đối tượng muốn tự động dọn dẹp nó có thể thực hiện IDisposablegiao diện, cung cấp một Disposephương thức (ít nhất là mơ hồ) tương tự như hàm hủy của C ++. Mặc dù điều này có thể được sử dụng thông qua try/ finallylike trong Java, C # tự động hóa tác vụ thêm một chút với usingcâu lệnh cho phép bạn xác định các tài nguyên sẽ được tạo khi phạm vi được nhập và bị hủy khi phạm vi được thoát. Mặc dù vẫn còn thiếu mức độ tự động hóa và sự chắc chắn do C ++ cung cấp, đây vẫn là một cải tiến đáng kể so với Java. Đặc biệt, người thiết kế lớp có thể tập trung hóa các chi tiết về cách thứcđể loại bỏ các lớp trong việc thực hiện IDisposable. Tất cả những gì còn lại cho lập trình viên máy khách là gánh nặng ít hơn khi viết một usingcâu lệnh để đảm bảo rằng IDisposablegiao diện sẽ được sử dụng khi cần. Trong Java 7 và mới hơn, các tên đã được thay đổi để bảo vệ tội lỗi, nhưng ý tưởng cơ bản là giống hệt nhau.


1
Câu trả lời hoàn hảo. Destructors là THE cần phải có tính năng trong C ++.
Thomas Eding

13

Không thể tin được ai khác đã nêu ra điều này (không có ý định chơi chữ) - bạn không cần một điều khoản bắt !

Điều này là hoàn toàn hợp lý:

try 
{
   AcquireManyResources(); 
   DoSomethingThatMightFail(); 
}
finally 
{
   CleanUpThoseResources(); 
}

Không có mệnh đề bắt ở bất cứ đâu là tầm nhìn, bởi vì phương pháp này không thể làm bất cứ điều gì hữu ích với những ngoại lệ đó; chúng được để lại để sao lưu ngăn xếp cuộc gọi đến một trình xử lý có thể . Bắt và ném lại ngoại lệ trong mọi phương pháp là một Ý tưởng tồi, đặc biệt nếu bạn chỉ ném lại cùng một ngoại lệ. Nó hoàn toàn đi ngược lại cách Xử lý ngoại lệ có cấu trúc được cho là hoạt động (và khá gần với việc trả lại "mã lỗi" từ mọi phương thức, chỉ trong "hình dạng" của Ngoại lệ).

Mặc dù vậy, phương pháp này phải làm là gì, để tự dọn dẹp, để "Thế giới bên ngoài" không bao giờ cần biết bất cứ điều gì về mớ hỗn độn mà nó tự mắc phải. Mệnh đề cuối cùng thực hiện điều đó - bất kể các phương thức được gọi hoạt động như thế nào, mệnh đề cuối cùng sẽ được thực thi "trên đường ra" của phương thức (và điều này cũng đúng với mọi mệnh đề cuối cùng giữa điểm mà Ngoại lệ được ném và mệnh đề bắt cuối cùng xử lý nó); mỗi cái được chạy khi ngăn xếp cuộc gọi "thư giãn".


9

Điều gì sẽ xảy ra nếu và ngoại lệ được ném mà bạn không mong đợi. Thử sẽ thoát ở giữa nó và không có mệnh đề bắt nào được thực thi.

Khối cuối cùng là để giúp với điều đó và đảm bảo rằng không có vấn đề ngoại lệ, việc dọn dẹp sẽ xảy ra.


4
Điều đó không đủ lý do cho một finally, vì bạn có thể ngăn chặn các trường hợp ngoại lệ "không mong muốn" với catch(Object)hoặc catch(...)bắt tất cả.
MSalters

1
Mà âm thanh như một công việc xung quanh. Về mặt khái niệm cuối cùng là sạch hơn. Mặc dù tôi phải thú nhận là hiếm khi sử dụng nó.
quick_now 27/1/2015

7

Một số ngôn ngữ cung cấp cả hàm tạo và hàm hủy cho các đối tượng của chúng (ví dụ: C ++ tôi tin). Với những ngôn ngữ này, bạn có thể thực hiện hầu hết (có thể nói là tất cả) những gì thường được thực hiện finallytrong một hàm hủy. Như vậy - trong các ngôn ngữ đó - một finallymệnh đề có thể là thừa.

Trong một ngôn ngữ không có hàm hủy (ví dụ Java), rất khó (thậm chí là không thể) để đạt được dọn dẹp chính xác mà không có finallymệnh đề. NB - Trong Java có một finalisephương thức nhưng không có gì đảm bảo nó sẽ được gọi.


Có thể hữu ích để lưu ý rằng các công cụ hủy diệt giúp làm sạch tài nguyên khi việc phá hủy mang tính quyết định . Nếu chúng ta không biết khi nào đối tượng sẽ bị phá hủy và / hoặc thu gom rác, thì các công cụ hủy diệt không đủ an toàn.
Morwenn

@Morwenn - Điểm tốt. Tôi đã gợi ý nó với tài liệu tham khảo của tôi về Java finalisenhưng tôi không muốn tham gia vào các cuộc tranh luận chính trị xung quanh các kẻ hủy diệt / trận chung kết tại thời điểm này.
OldCurmudgeon

Trong C ++ phá hủy là xác định. Khi phạm vi chứa một đối tượng tự động thoát (ví dụ: nó bị bật ra khỏi ngăn xếp), hàm hủy của nó được gọi. (C ++ cho phép bạn phân bổ các đối tượng trên ngăn xếp, không chỉ là đống.)
Rob K

@RobK - Và đây là chức năng chính xác của một finalisenhưng với cả hương vị có thể mở rộng và cơ chế giống như oop - rất biểu cảm và có thể so sánh với finalisecơ chế của các ngôn ngữ khác.
OldCurmudgeon

1

Thử cuối cùng và thử bắt là hai điều khác nhau chỉ chia sẻ từ khóa: "thử". Cá nhân tôi muốn thấy sự khác biệt đó. Lý do bạn thấy chúng cùng nhau là vì các ngoại lệ tạo ra một "bước nhảy".

Và thử cuối cùng được thiết kế để chạy mã ngay cả khi dòng lập trình nhảy ra. Cho dù đó là vì một ngoại lệ hoặc bất kỳ lý do khác. Đó là một cách gọn gàng để có được một tài nguyên và đảm bảo rằng nó được dọn sạch sau khi không phải lo lắng về việc nhảy.


3
Trong .NET chúng được thực hiện bằng các cơ chế riêng biệt; tuy nhiên, trong Java, cấu trúc duy nhất được JVM nhận ra là tương đương về mặt ngữ nghĩa với "lỗi goto", một mẫu hỗ trợ trực tiếp try catchnhưng không try finally; mã sử dụng mã sau được chuyển đổi thành mã chỉ sử dụng mã trước, bằng cách sao chép nội dung của finallykhối trong tất cả các điểm của mã nơi có thể cần thực thi.
supercat

@supercat tốt đẹp, cảm ơn vì thông tin thêm về Java.
Pieter B

1

Vì câu hỏi này không chỉ định C ++ là ngôn ngữ, tôi sẽ xem xét kết hợp C ++ và Java, vì chúng có cách tiếp cận khác nhau để phá hủy đối tượng, được đề xuất là một trong những lựa chọn thay thế.

Lý do bạn có thể sử dụng khối cuối cùng, thay vì mã sau khối thử bắt

  • bạn trở về sớm từ khối thử: Hãy xem xét điều này

    Database db = null;
    try {
     db = open_database();
     if(db.isSomething()) {
       return 7;
     }
     return db.someThingElse();
    } finally {
      if(db!=null)
        db.close();
    }
    

    so sánh với:

    Database db = null;
    int returnValue = 0;
    try {
     db = open_database();
     if(db.isSomething()) {
       returnValue = 7;
     } else {
       returnValue = db.someThingElse();
     }
    } catch(Exception e) {
      if(db!=null)
        db.close();
    }
    return returnValue;
    
  • bạn trở về sớm từ (các) khối bắt: So sánh

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      return 7;
    } catch (DBIsADonkeyException e ) {
      return 11;
    } finally {
      if(db!=null)
        db.close();
    }
    

    vs

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      if(db!=null) 
        db.close();
      return 7;
    } catch (DBIsADonkeyException e ) {
      if(db!=null)
        db.close();
      return 11;
    }           
    db.close();
    
  • Bạn nghĩ lại ngoại lệ. Đối chiếu:

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      throw convertToRuntimeException(e,"DB was wonkey");
    } finally {
      if(db!=null)
        db.close();
    }
    

    vs

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      if(db!=null)
        db.close();
      throw convertToRuntimeException(e,"DB was wonkey");
    } 
    if(db!=null)
      db.close();
    

Các ví dụ này không làm cho nó có vẻ quá tệ, nhưng thường thì bạn có một vài trường hợp tương tác và nhiều hơn một loại tài nguyên / ngoại lệ đang chơi. finallycó thể giúp giữ cho mã của bạn không trở thành một cơn ác mộng bảo trì rối.

Bây giờ trong C ++, chúng có thể được xử lý với các đối tượng dựa trên phạm vi. Nhưng IMO có hai nhược điểm của phương pháp này 1. cú pháp kém thân thiện. 2. Trật tự xây dựng là mặt trái của trật tự hủy diệt có thể làm cho mọi thứ không rõ ràng.

Trong Java, bạn không thể kết nối phương thức hoàn thiện để dọn dẹp vì bạn không biết khi nào nó sẽ xảy ra - (bạn cũng có thể nhưng đó là một con đường chứa đầy các điều kiện cuộc đua thú vị - JVM có rất nhiều phạm vi trong việc quyết định khi nào nó phá hủy mọi thứ - thường không phải khi bạn mong đợi - sớm hơn hoặc muộn hơn bạn mong đợi - và điều đó có thể thay đổi khi trình biên dịch điểm nóng khởi động trong ... thở dài ...)


1

Tất cả những gì hợp lý "cần thiết" trong ngôn ngữ lập trình là các hướng dẫn:

assignment a = b
subtract a from b
goto label
test a = 0
if true goto label

Bất kỳ thuật toán nào cũng có thể được thực hiện chỉ bằng các hướng dẫn ở trên, tất cả các cấu trúc ngôn ngữ khác đều có để làm cho các chương trình dễ viết hơn và dễ hiểu hơn đối với các lập trình viên khác.

Xem máy tính thế giới cũ cho phần cứng thực tế bằng cách sử dụng một bộ hướng dẫn tối thiểu như vậy.


1
Câu trả lời của bạn chắc chắn là đúng, nhưng tôi không viết mã; đau quá Tôi đang hỏi tại sao sử dụng một tính năng mà tôi không thấy điểm của các ngôn ngữ hỗ trợ tính năng đó, chứ không phải tập lệnh tối thiểu của ngôn ngữ là gì.
Búa búa Agi

1
Vấn đề là bất kỳ ngôn ngữ nào thực hiện chỉ 5 thao tác này có thể thực hiện bất kỳ thuật toán nào - mặc dù khá quanh co. Hầu hết các toán tử / toán tử trong các ngôn ngữ cấp cao không "cần thiết" nếu mục tiêu chỉ đơn thuần là thực hiện một thuật toán. Nếu mục tiêu là phát triển nhanh mã có thể đọc được thì hầu hết là cần thiết nhưng "có thể đọc được" và "có thể bảo trì" thì không thể đo lường được và cực kỳ chủ quan. Các nhà phát triển ngôn ngữ tốt đẹp đưa vào rất nhiều tính năng: nếu bạn không sử dụng cho một số trong số họ thì đừng sử dụng chúng.
James Anderson

0

Trên thực tế, khoảng cách lớn hơn đối với tôi thường là ở các ngôn ngữ hỗ trợ finallynhưng thiếu các hàm hủy, bởi vì bạn có thể mô hình hóa tất cả logic liên quan đến "dọn dẹp" (mà tôi sẽ tách thành hai loại) thông qua các hàm hủy ở cấp trung tâm mà không xử lý thủ công logic trong mọi chức năng có liên quan. Khi tôi thấy mã C # hoặc Java thực hiện những việc như mở khóa thủ công các tệp đột biến và đóng các tệp trong finallycác khối, cảm giác đó đã lỗi thời và giống như mã C khi tất cả điều đó được tự động hóa trong C ++ thông qua các hàm hủy theo cách giải phóng trách nhiệm của con người.

Tuy nhiên, tôi vẫn sẽ tìm thấy một sự thuận tiện nhẹ nếu bao gồm C ++ finallyvà đó là vì có hai loại dọn dẹp:

  1. Phá hủy / giải phóng / mở khóa / đóng / tài nguyên địa phương (công cụ phá hủy là hoàn hảo cho việc này).
  2. Hoàn tác / đẩy lùi các tác dụng phụ bên ngoài (chất phá hủy là đủ cho việc này).

Cái thứ hai ít nhất không ánh xạ trực quan đến ý tưởng phá hủy tài nguyên, mặc dù bạn có thể làm điều đó tốt với các bộ bảo vệ phạm vi tự động khôi phục các thay đổi khi chúng bị phá hủy trước khi được cam kết. Có finallythể cung cấp ít nhất một chút (chỉ một chút tuổi teen) cơ chế đơn giản hơn cho công việc so với các nhân viên bảo vệ phạm vi.

Tuy nhiên, một cơ chế thậm chí đơn giản hơn sẽ là một rollbackkhối mà tôi chưa từng thấy trong bất kỳ ngôn ngữ nào trước đây. Đó là một giấc mơ xa vời của tôi nếu tôi từng thiết kế một ngôn ngữ liên quan đến xử lý ngoại lệ. Nó sẽ giống như thế này:

try
{
    // Cause external side effects. These side effects should
    // be undone if we don't finish successfully.
}
rollback
{
    // Reverse external side effects. This block is *only* executed 
    // if the 'try' block above faced a premature return out 
    // of the function. It is different from 'finally' which 
    // gets executed regardless of whether or not the function 
    // exited prematurely. This block *only* gets executed if we 
    // exited prematurely from  the try block so that we can undo 
    // whatever side effects it failed to finish making. If the try 
    // block succeeded and didn't face a premature exit, then we 
    // don't want this block to execute.
}

Đó sẽ là cách đơn giản nhất để mô hình hóa các hiệu ứng phụ, trong khi các hàm hủy là cơ chế hoàn hảo để dọn dẹp tài nguyên cục bộ. Bây giờ nó chỉ lưu thêm một vài dòng mã từ giải pháp bảo vệ phạm vi, nhưng lý do tôi rất muốn thấy một ngôn ngữ trong đó là việc khôi phục hiệu ứng phụ có xu hướng bị lãng quên (nhưng khó nhất) trong xử lý ngoại lệ trong các ngôn ngữ xoay quanh khả năng biến đổi. Tôi nghĩ rằng tính năng này sẽ khuyến khích các nhà phát triển suy nghĩ về việc xử lý ngoại lệ theo cách phù hợp trong các giao dịch quay ngược lại bất cứ khi nào các chức năng gây ra tác dụng phụ và không hoàn thành và, như một phần thưởng phụ khi mọi người thấy khó khăn như thế nào khi thực hiện quay ngược lại, họ có thể thích viết nhiều chức năng hơn, không có tác dụng phụ ngay từ đầu.

Ngoài ra còn có một số trường hợp tối nghĩa mà bạn chỉ muốn làm những việc linh tinh bất kể khi thoát khỏi một chức năng, bất kể nó đã thoát như thế nào, như có thể đăng nhập một dấu thời gian. Có finallythể nói là giải pháp đơn giản và hoàn hảo nhất cho công việc, vì cố gắng khởi tạo một đối tượng chỉ để sử dụng hàm hủy của nó cho mục đích duy nhất là ghi lại dấu thời gian chỉ cảm thấy thực sự kỳ lạ (mặc dù bạn có thể làm điều đó rất tốt và khá thuận tiện với lambdas ).


-9

Giống như rất nhiều điều khác thường về ngôn ngữ C ++, việc thiếu try/finallycấu trúc là một lỗ hổng thiết kế, nếu bạn thậm chí có thể gọi nó là ngôn ngữ thường xuất hiện mà không có bất kỳ công việc thiết kế thực tế nào được thực hiện.

RAII (việc sử dụng lời gọi hàm hủy xác định dựa trên phạm vi trên các đối tượng dựa trên ngăn xếp để dọn dẹp) có hai lỗ hổng nghiêm trọng. Đầu tiên là nó yêu cầu sử dụng các đối tượng dựa trên ngăn xếp , là một sự gớm ghiếc vi phạm Nguyên tắc thay thế Liskov. Có rất nhiều lý do chính đáng tại sao không có ngôn ngữ OO nào khác trước hoặc kể từ khi C ++ sử dụng chúng - trong epsilon; D không được tính vì nó dựa nhiều vào C ++ và dù sao cũng không có thị phần - và giải thích những vấn đề họ gây ra nằm ngoài phạm vi của câu trả lời này.

Thứ hai, những gì finallycó thể làm là một siêu khối của sự hủy diệt đối tượng. Phần lớn những gì được thực hiện với RAII trong C ++ sẽ được mô tả bằng ngôn ngữ Delphi, không có bộ sưu tập rác, với mẫu sau:

myObject := MyClass.Create(arguments);
try
   doSomething(myObject);
finally
   myObject.Free();
end;

Đây là mẫu RAII được làm rõ ràng; nếu bạn tạo một thói quen C ++ chỉ chứa tương đương với dòng đầu tiên và dòng thứ ba ở trên, trình biên dịch sẽ tạo ra sẽ trông giống như những gì tôi đã viết trong cấu trúc cơ bản của nó. Và bởi vì đó là quyền truy cập duy nhất vào try/finallycấu trúc mà C ++ cung cấp, nên các nhà phát triển C ++ kết thúc với một cái nhìn khá cận thị về try/finally: khi tất cả những gì bạn có là một cái búa, mọi thứ bắt đầu trông giống như một kẻ hủy diệt, có thể nói.

Nhưng có những thứ khác mà một nhà phát triển có kinh nghiệm có thể làm với một finallycấu trúc. Đó không phải là về sự hủy diệt mang tính quyết định, ngay cả khi đối mặt với một ngoại lệ được nêu ra; đó là về việc thực thi mã xác định , ngay cả khi đối mặt với một ngoại lệ được nêu ra.

Đây là một điều khác mà bạn thường thấy trong mã Delphi: Một đối tượng bộ dữ liệu với các điều khiển người dùng bị ràng buộc với nó. Bộ dữ liệu chứa dữ liệu từ một nguồn bên ngoài và các điều khiển phản ánh trạng thái của dữ liệu. Nếu bạn sắp tải một loạt dữ liệu vào tập dữ liệu của mình, bạn sẽ muốn tạm thời vô hiệu hóa liên kết dữ liệu để nó không làm những điều lạ với UI của bạn, cố gắng cập nhật nó nhiều lần với mỗi bản ghi mới được nhập , vì vậy bạn sẽ mã nó như thế này:

dataset.DisableControls();
try
   LoadData(dataset);
finally
   dataset.EnableControls();
end;

Rõ ràng, không có đối tượng bị phá hủy ở đây, và không cần một. Mã này đơn giản, ngắn gọn, rõ ràng và hiệu quả.

Làm thế nào điều này sẽ được thực hiện trong C ++? Chà, trước tiên bạn sẽ phải viết mã cho cả lớp . Nó có thể sẽ được gọi DatasetEnablerhoặc điều somesuch. Toàn bộ sự tồn tại của nó sẽ là một người trợ giúp RAII. Sau đó, bạn sẽ cần phải làm một cái gì đó như thế này:

dataset.DisableControls();
{
   raiiGuard = DatasetEnabler(dataset);
   LoadData(dataset);
}

Có, những dấu ngoặc nhọn rõ ràng không cần thiết đó là cần thiết để quản lý phạm vi phù hợp và đảm bảo rằng tập dữ liệu được kích hoạt lại ngay lập tức và không ở cuối phương thức. Vì vậy, những gì bạn kết thúc với không mất ít dòng mã (trừ khi bạn sử dụng niềng răng Ai Cập). Nó đòi hỏi một đối tượng không cần thiết được tạo ra, có chi phí hoạt động. (Không phải mã C ++ được cho là nhanh sao?) Nó không rõ ràng, mà thay vào đó dựa vào phép thuật của trình biên dịch. Mã được thực thi được mô tả không ở đâu trong phương thức này, nhưng thay vào đó nằm trong một lớp hoàn toàn khác, có thể trong một tệp hoàn toàn khác . Nói tóm lại, đây không phải là một giải pháp tốt hơn là có thể tự viết try/finallykhối.

Loại vấn đề này đủ phổ biến trong thiết kế ngôn ngữ có một tên cho nó: đảo ngược trừu tượng. Nó xảy ra khi một cấu trúc cấp cao được xây dựng trên đỉnh của một cấu trúc cấp thấp, và sau đó, cấu trúc cấp thấp không được hỗ trợ trực tiếp trong ngôn ngữ, yêu cầu những người muốn sử dụng nó phải triển khai lại theo thuật ngữ xây dựng mức cao, thường ở mức phạt cao cả về khả năng đọc và hiệu quả của mã.


Nhận xét có nghĩa là để làm rõ hoặc cải thiện một câu hỏi và câu trả lời. Nếu bạn muốn có một cuộc thảo luận về câu trả lời này thì hãy đến phòng chat. Cảm ơn bạn.
maple_shaft
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.