Tại sao một chương trình sẽ sử dụng một đóng cửa?


58

Sau khi đọc nhiều bài viết giải thích về việc đóng cửa ở đây, tôi vẫn còn thiếu một khái niệm chính: Tại sao lại viết một bao đóng? Nhiệm vụ cụ thể nào mà một lập trình viên sẽ thực hiện có thể được phục vụ tốt nhất bằng cách đóng?

Ví dụ về các lần đóng trong Swift là các truy cập của NSUrl và sử dụng trình mã hóa địa lý ngược. Đây là một ví dụ như vậy. Thật không may, những khóa học chỉ trình bày việc đóng cửa; họ không giải thích tại sao giải pháp mã được viết dưới dạng đóng.

Một ví dụ về một vấn đề lập trình trong thế giới thực có thể khiến não tôi nói: "aha, tôi nên viết một kết thúc cho việc này", sẽ có nhiều thông tin hơn là một cuộc thảo luận lý thuyết. Không thiếu các cuộc thảo luận lý thuyết có sẵn trên trang web này.


7
"Gần đây đã xem xét việc mở rộng các Đóng cửa ở đây" - bạn có đang thiếu một liên kết không?
Dan Pichelman

2
Bạn nên đặt sự làm rõ về nhiệm vụ không đồng bộ trong một bình luận, bên dưới câu trả lời có liên quan. Đây không phải là một phần của câu hỏi ban đầu của bạn và người trả lời sẽ không bao giờ được thông báo về chỉnh sửa của bạn.
Robert Harvey

5
Chỉ là một mẩu tin: Điểm quan trọng của Closures là phạm vi từ vựng (trái ngược với phạm vi động), đóng cửa mà không có phạm vi từ vựng là vô dụng. Đóng cửa đôi khi được gọi là Đóng cửa từ điển.
Hoffmann

1
Đóng cửa cho phép bạn khởi tạo các chức năng .
dùng253751

1
Đọc bất kỳ cuốn sách tốt về lập trình chức năng. Có lẽ bắt đầu với SICP
Basile Starynkevitch

Câu trả lời:


33

Trước hết, không có gì là không thể nếu không sử dụng các bao đóng. Bạn luôn có thể thay thế một bao đóng bởi một đối tượng thực hiện một giao diện cụ thể. Đó chỉ là vấn đề ngắn gọn và giảm khớp nối.

Thứ hai, hãy nhớ rằng các bao đóng thường được sử dụng không phù hợp, trong đó một tham chiếu hàm đơn giản hoặc cấu trúc khác sẽ rõ ràng hơn. Bạn không nên lấy mọi ví dụ bạn thấy là một cách thực hành tốt nhất.

Trường hợp các bao đóng thực sự tỏa sáng so với các cấu trúc khác là khi sử dụng các hàm bậc cao hơn, khi bạn thực sự cần giao tiếp trạng thái và bạn có thể biến nó thành một lớp lót, như trong ví dụ JavaScript này từ trang wikipedia về các bao đóng :

// Return a list of all books with at least 'threshold' copies sold.
function bestSellingBooks(threshold) {
  return bookList.filter(
      function (book) { return book.sales >= threshold; }
    );
}

Ở đây, thresholdđược truyền đạt rất ngắn gọn và tự nhiên từ nơi nó được xác định đến nơi nó được sử dụng. Phạm vi của nó được giới hạn chính xác là nhỏ nhất có thể. filterkhông phải viết để cho phép khả năng truyền dữ liệu do khách hàng xác định như ngưỡng. Chúng ta không phải xác định bất kỳ cấu trúc trung gian nào cho mục đích duy nhất là truyền đạt ngưỡng trong một chức năng nhỏ này. Nó hoàn toàn khép kín.

Bạn có thể viết cái này mà không cần đóng, nhưng nó sẽ đòi hỏi nhiều mã hơn và khó theo dõi hơn. Ngoài ra, JavaScript có cú pháp lambda khá dài dòng. Trong Scala, ví dụ, toàn bộ cơ thể chức năng sẽ là:

bookList filter (_.sales >= threshold)

Tuy nhiên, nếu bạn có thể sử dụng ECMAScript 6 , nhờ các hàm mũi tên béo, ngay cả mã JavaScript cũng trở nên đơn giản hơn nhiều và thực sự có thể được đặt trên một dòng.

const bestSellingBooks = (threshold) => bookList.filter(book => book.sales >= threshold);

Trong mã của riêng bạn, hãy tìm những nơi bạn tạo ra nhiều bản tóm tắt chỉ để truyền đạt các giá trị tạm thời từ nơi này sang nơi khác. Đây là những cơ hội tuyệt vời để xem xét thay thế bằng việc đóng cửa.


Làm thế nào chính xác làm đóng cửa giảm khớp nối?
sbichenko

Không có sự đóng cửa, bạn phải áp đặt các hạn chế đối với cả bestSellingBooksmã và filtermã, chẳng hạn như một giao diện cụ thể hoặc đối số dữ liệu người dùng, để có thể giao tiếp thresholddữ liệu. Điều đó gắn kết hai chức năng với nhau theo những cách ít tái sử dụng hơn.
Karl Bielefeldt

51

Bằng cách giải thích, tôi sẽ mượn một số mã từ bài đăng blog tuyệt vời này về việc đóng cửa . Đó là JavaScript, nhưng đó là ngôn ngữ mà hầu hết các bài đăng trên blog nói về việc đóng cửa sử dụng, bởi vì việc đóng cửa rất quan trọng trong JavaScript.

Giả sử bạn muốn kết xuất một mảng dưới dạng bảng HTML. Bạn có thể làm như thế này:

function renderArrayAsHtmlTable (array) {
  var table = "<table>";
  for (var idx in array) {
    var object = array[idx];
    table += "<tr><td>" + object + "</td></tr>";
  }
  table += "</table>";
  return table;
}

Nhưng bạn có thể hiểu được JavaScript về cách mỗi phần tử trong mảng sẽ được hiển thị. Nếu bạn muốn kiểm soát kết xuất, bạn có thể làm điều này:

function renderArrayAsHtmlTable (array, renderer) {
  var table = "<table>";
  for (var idx in array) {
    var object = array[idx];
    table += "<tr><td>" + renderer(object) + "</td></tr>";
  }
  table += "</table>";
  return table;
}

Và bây giờ bạn có thể chỉ cần truyền một hàm trả về kết xuất mà bạn muốn.

Điều gì xảy ra nếu bạn muốn hiển thị tổng số đang chạy trong mỗi Hàng Bảng? Bạn sẽ cần một biến để theo dõi tổng số đó, phải không? Một bao đóng cho phép bạn viết một hàm kết xuất đóng trên tổng biến đang chạy và cho phép bạn viết một trình kết xuất có thể theo dõi tổng chạy:

function intTableWithTotals (intArray) {
  var total = 0;
  var renderInt = function (i) {
    total += i;
    return "Int: " + i + ", running total: " + total;
  };
  return renderObjectsInTable(intArray, renderInt);
}

Điều kỳ diệu đang xảy ra ở đây là renderInt duy trì quyền truy cập vào totalbiến, mặc dù renderIntđược gọi và thoát liên tục.

Trong một ngôn ngữ hướng đối tượng truyền thống hơn JavaScript, bạn có thể viết một lớp có chứa tổng biến này và chuyển nó xung quanh thay vì tạo một bao đóng. Nhưng đóng cửa là một cách mạnh mẽ hơn, sạch sẽ và thanh lịch hơn để làm điều đó.

Đọc thêm


10
Nói chung, bạn có thể nói rằng "đối tượng hàm hạng nhất == chỉ có một phương thức", "Đối tượng đóng == chỉ có một phương thức và trạng thái", "object == bó đóng".
Jörg W Mittag

Có vẻ tốt. Một điều, và điều này có thể là thầy giáo, là rằng Javascript hướng đối tượng, và bạn có thể tạo ra một đối tượng có chứa các biến tổng và thông qua đó xung quanh. Các bao đóng vẫn mạnh mẽ hơn, sạch sẽ và thanh lịch hơn và cũng làm cho Javascript thành ngữ hơn nhiều, nhưng cách nó được viết có thể ám chỉ rằng Javascript không thể làm điều này theo cách hướng đối tượng.
KRyan

2
Trên thực tế, để thực sự mang tính mô phạm: đóng cửa là điều làm cho JavaScript hướng đối tượng ngay từ đầu! OO là về trừu tượng hóa dữ liệu và đóng cửa là cách để thực hiện trừu tượng hóa dữ liệu trong JavaScript.
Jörg W Mittag

@ JörWWitt là các bao đóng được xác định bên trong phạm vi của hàm tạo và được tạo sẵn để được gọi sau. Hàm constructor trả về một hàm bậc cao hơn (đối tượng) có thể gửi trên mỗi bao đóng theo tên phương thức được sử dụng cho lệnh gọi.
Giorgio

@Giorgio: Thật vậy, đó là cách các đối tượng thường được triển khai trong Scheme, và thực tế cũng là cách các đối tượng được triển khai trong JavaScript (không ngạc nhiên khi xem xét mối quan hệ chặt chẽ của nó với Scheme, sau tất cả, Brendan Eich ban đầu được thuê để thiết kế Phương ngữ lược đồ và triển khai trình thông dịch Scheme nhúng trong Netscape Navigator và chỉ sau đó mới được yêu cầu tạo ngôn ngữ "với các đối tượng trông giống như C ++", sau đó anh ta đã thực hiện số lượng thay đổi tối thiểu tuyệt đối để tuân thủ các yêu cầu tiếp thị đó).
Jörg W Mittag

22

Mục đích của closureschỉ đơn giản là để bảo tồn nhà nước; do đó tên closure- nó đóng cửa trên trạng thái. Để dễ giải thích thêm, tôi sẽ sử dụng Javascript.

Thông thường bạn có một chức năng

function sayHello(){
    var txt="Hello";
    return txt;
}

trong đó phạm vi của (các) biến được liên kết với hàm này. Vì vậy, sau khi thực hiện các biến txtđi ra khỏi phạm vi. Không có cách nào để truy cập hoặc sử dụng nó sau khi chức năng đã thực hiện xong.

Đóng là cấu trúc ngôn ngữ, cho phép - như đã nói trước đó - để duy trì trạng thái của các biến và do đó kéo dài phạm vi.

Điều này có thể hữu ích trong các trường hợp khác nhau. Một trường hợp sử dụng là việc xây dựng các chức năng bậc cao .

Trong toán học và khoa học máy tính, một hàm bậc cao hơn (cũng là dạng hàm, hàm hoặc hàm functor) là một hàm thực hiện ít nhất một trong các cách sau: 1

  • nhận một hoặc nhiều chức năng làm đầu vào
  • xuất ra một chức năng

Một ví dụ đơn giản, nhưng không phải tất cả quá hữu ích là:

 makeadder=function(a){
     return function(b){
         return a+b;
     }
 }

 add5=makeadder(5);
 console.log(add5(10)); 

Bạn xác định một hàm makedadder, lấy một tham số làm đầu vào và trả về một hàm . Có một chức năng bên ngoàifunction(a){}bên trong. function(b){}{} Ngoài ra, bạn xác định (ngầm) một chức năng khác add5là kết quả của việc gọi chức năng bậc cao makeadder. makeadder(5)trả về một hàm ẩn danh ( bên trong ), lần lượt lấy 1 tham số và trả về tổng tham số của hàm ngoài và tham số của hàm bên trong .

Thủ thuật là, trong khi trả về hàm bên trong , bổ sung thực tế, phạm vi của tham số của hàm ngoài ( a) được giữ nguyên. add5 nhớ rằng , tham số a5.

Hoặc để hiển thị một ví dụ ít nhất bằng cách nào đó hữu ích:

  makeTag=function(openTag, closeTag){
     return function(content){
         return openTag +content +closeTag;
     }
 }

 table=makeTag("<table>","</table>")
 tr=makeTag("<tr>", "</tr>");
 td=makeTag("<td>","</td>");
 console.log(table(tr(td("I am a Row"))));

Một usecase phổ biến khác là cái gọi là IIFE = biểu thức hàm được gọi ngay lập tức. Nó là rất phổ biến trong javascript để giả mạo các biến thành viên tư nhân. Điều này được thực hiện thông qua một hàm, tạo ra một phạm vi riêng = closure, bởi vì nó ngay lập tức sau khi định nghĩa được gọi. Cấu trúc là function(){}(). Lưu ý các dấu ngoặc ()sau định nghĩa. Điều này làm cho nó có thể sử dụng nó để tạo đối tượng với mô hình mô-đun tiết lộ . Thủ thuật là tạo ra một phạm vi và trả về một đối tượng, có quyền truy cập vào phạm vi này sau khi thực hiện IIFE.

Ví dụ của Addi trông như thế này:

 var myRevealingModule = (function () {

         var privateVar = "Ben Cherry",
             publicVar = "Hey there!";

         function privateFunction() {
             console.log( "Name:" + privateVar );
         }

         function publicSetName( strName ) {
             privateVar = strName;
         }

         function publicGetName() {
             privateFunction();
         }


         // Reveal public pointers to
         // private functions and properties

         return {
             setName: publicSetName,
             greeting: publicVar,
             getName: publicGetName
         };

     })();

 myRevealingModule.setName( "Paul Kinlan" );

Đối tượng được trả về có tham chiếu đến các hàm (ví dụ publicSetName), do đó có quyền truy cập vào các biến "riêng tư" privateVar.

Nhưng đây là những trường hợp sử dụng đặc biệt hơn cho Javascript.

Nhiệm vụ cụ thể nào mà một lập trình viên sẽ thực hiện có thể được phục vụ tốt nhất bằng cách đóng?

Có nhiều lý do cho điều đó. Người ta có thể, rằng đó là điều tự nhiên đối với anh ta, vì anh ta tuân theo một mô hình chức năng . Hoặc trong Javascript: chỉ cần dựa vào sự đóng cửa để phá vỡ một số điều kỳ quặc của ngôn ngữ.


"Mục đích của việc đóng cửa chỉ đơn giản là bảo tồn trạng thái, do đó việc đóng tên - nó đóng trên trạng thái.": Nó thực sự phụ thuộc vào ngôn ngữ. Đóng gần hơn với tên / biến bên ngoài. Chúng có thể biểu thị trạng thái (vị trí bộ nhớ có thể thay đổi) nhưng cũng có thể biểu thị các giá trị (không thay đổi). Đóng cửa trong Haskell không bảo tồn trạng thái: họ bảo tồn thông tin đã biết tại thời điểm này và trong bối cảnh đóng cửa được tạo ra.
Giorgio

1
»Họ lưu giữ thông tin đã biết vào lúc này và trong bối cảnh đóng cửa được tạo ra« một cách độc lập cho dù nó có thể thay đổi hay không, đó là trạng thái - có lẽ là trạng thái bất biến .
Thomas Junk

16

Có hai trường hợp sử dụng chính để đóng cửa:

  1. Không đồng bộ. Giả sử bạn muốn thực hiện một nhiệm vụ sẽ mất một lúc, và sau đó làm một cái gì đó khi nó hoàn thành. Bạn có thể làm cho mã của mình chờ nó được thực hiện, điều này sẽ chặn việc thực thi thêm và có thể làm cho chương trình của bạn không phản hồi hoặc gọi nhiệm vụ của bạn không đồng bộ và nói "bắt đầu tác vụ dài này trong nền và khi hoàn thành, hãy thực hiện việc đóng này", nơi đóng cửa chứa mã để thực thi khi hoàn thành.

  2. Gọi lại. Đây cũng được gọi là "đại biểu" hoặc "xử lý sự kiện" tùy thuộc vào ngôn ngữ và nền tảng. Ý tưởng là bạn có một đối tượng có thể tùy chỉnh, tại một số điểm được xác định rõ ràng, sẽ thực thi một sự kiện , chạy một bao đóng được truyền bởi mã thiết lập nó. Ví dụ: trong giao diện người dùng của chương trình, bạn có thể có một nút và bạn đóng cho nó một mã giữ mã được thực thi khi người dùng nhấp vào nút.

Có một số cách sử dụng khác để đóng cửa, nhưng đó là hai cách chính.


23
Vì vậy, về cơ bản gọi lại sau đó, vì ví dụ đầu tiên cũng là một cuộc gọi lại.
Robert Harvey

2
@RobertHarvey: Về mặt kỹ thuật, nhưng chúng là những mô hình tinh thần khác nhau. Ví dụ: (nói chung,) bạn mong muốn trình xử lý sự kiện được gọi nhiều lần, nhưng việc tiếp tục không đồng bộ của bạn chỉ được gọi một lần. Nhưng vâng, về mặt kỹ thuật, bất cứ điều gì bạn làm với việc đóng cửa đều là gọi lại. (Trừ khi bạn chuyển đổi nó thành cây biểu thức, nhưng đó là một vấn đề hoàn toàn khác.);)
Mason Wheeler

4
@RobertHarvey: Xem các lần đóng cửa khi các cuộc gọi lại đưa bạn vào một khung suy nghĩ sẽ thực sự ngăn bạn sử dụng chúng một cách hiệu quả. Đóng cửa là cuộc gọi lại trên steroid.
gnasher729

13
@MasonWheeler Đặt nó theo cách này, nghe có vẻ sai. Cuộc gọi lại không có gì để làm với đóng cửa; tất nhiên bạn có thể gọi lại một chức năng trong một bao đóng hoặc gọi một bao đóng như một cuộc gọi lại; nhưng một cuộc gọi lại là không cần thiết một đóng cửa. Một cuộc gọi lại chỉ là một cuộc gọi chức năng đơn giản. Một eventhandler không phải là một đóng cửa. Một đại biểu không cần thiết phải đóng: chủ yếu là cách con trỏ hàm C #. Tất nhiên bạn có thể sử dụng nó để xây dựng một đóng cửa. Lời giải thích của bạn là không chính xác. Điểm này là đóng cửa trên trạng thái và sử dụng nó.
Thomas Junk

5
Có thể có những ý nghĩa đặc thù của JS đang diễn ra ở đây đang khiến tôi hiểu lầm, nhưng câu trả lời này nghe có vẻ hoàn toàn sai đối với tôi. Không đồng bộ hoặc gọi lại có thể sử dụng bất cứ điều gì 'có thể gọi được'. Một đóng cửa không cần thiết cho một trong hai - một chức năng thông thường sẽ hoạt động tốt. Trong khi đó, một bao đóng, như được định nghĩa trong lập trình chức năng, hoặc như được sử dụng trong Python, là hữu ích, như Thomas nói, bởi vì nó 'đóng trên' một số trạng thái, tức là một biến và cung cấp một hàm trong phạm vi bên trong nhất quán truy cập vào biến đó giá trị ngay cả khi hàm được gọi và thoát nhiều lần.
Jonathan Hartley

13

Một vài ví dụ khác:

Sắp xếp
Hầu hết các chức năng sắp xếp hoạt động bằng cách so sánh các cặp đối tượng. Một số kỹ thuật so sánh là cần thiết. Hạn chế so sánh với một toán tử cụ thể có nghĩa là một loại khá không linh hoạt. Một cách tiếp cận tốt hơn nhiều là nhận một hàm so sánh làm đối số cho hàm sắp xếp. Đôi khi một hàm so sánh không trạng thái hoạt động tốt (ví dụ: sắp xếp danh sách các số hoặc tên), nhưng nếu so sánh cần trạng thái thì sao?

Ví dụ: xem xét sắp xếp danh sách các thành phố theo khoảng cách đến một số vị trí cụ thể. Một giải pháp xấu xí là lưu trữ tọa độ của vị trí đó trong một biến toàn cục. Điều này làm cho hàm so sánh tự nó không trạng thái, nhưng với giá của một biến toàn cục.

Cách tiếp cận này loại trừ việc có nhiều luồng đồng thời sắp xếp cùng một danh sách các thành phố theo khoảng cách của chúng đến hai địa điểm khác nhau. Một bao đóng bao quanh vị trí sẽ giải quyết vấn đề này và nó thoát khỏi sự cần thiết của một biến toàn cục.


Số ngẫu nhiên
Bản gốc rand()không có đối số. Trình tạo số giả ngẫu nhiên cần trạng thái. Một số (ví dụ, Mersenne Twister) cần rất nhiều trạng thái. Ngay cả rand()trạng thái đơn giản nhưng khủng khiếp cần thiết. Đọc một bài báo về toán học trên một trình tạo số ngẫu nhiên mới và chắc chắn bạn sẽ thấy các biến toàn cục. Điều đó tốt cho các nhà phát triển kỹ thuật, không tốt cho người gọi. Đóng gói trạng thái đó trong một cấu trúc và chuyển cấu trúc đến trình tạo số ngẫu nhiên là một cách xoay quanh vấn đề dữ liệu toàn cầu. Đây là cách tiếp cận được sử dụng trong nhiều ngôn ngữ không phải OO để tạo lại một trình tạo số ngẫu nhiên. Một đóng cửa ẩn trạng thái đó từ người gọi. Một bao đóng cung cấp trình tự gọi đơn giản rand()và sự tái lập trạng thái đóng gói.

Có nhiều số ngẫu nhiên hơn là chỉ PRNG. Hầu hết những người muốn ngẫu nhiên muốn nó phân phối một cách nhất định. Tôi sẽ bắt đầu với các số được rút ngẫu nhiên từ 0 đến 1 hoặc viết tắt là U (0,1). Bất kỳ PRNG nào tạo số nguyên từ 0 đến tối đa sẽ làm; chỉ cần chia (như một điểm nổi) số nguyên ngẫu nhiên cho mức tối đa. Một cách thuận tiện và chung chung để thực hiện điều này là tạo ra một bao đóng lấy một bao đóng (PRNG) và tối đa làm đầu vào. Bây giờ chúng ta có một trình tạo ngẫu nhiên chung và dễ sử dụng cho U (0,1).

Có một số phân phối khác ngoài U (0,1). Ví dụ, một phân phối bình thường với độ lệch trung bình và độ lệch chuẩn nhất định. Mỗi thuật toán trình tạo phân phối bình thường mà tôi chạy qua đều sử dụng trình tạo U (0,1). Một cách thuận tiện và chung chung để tạo ra một trình tạo bình thường là tạo một bao đóng đóng gói trình tạo U (0,1), giá trị trung bình và độ lệch chuẩn dưới dạng trạng thái. Điều này, ít nhất là về mặt khái niệm, một bao đóng lấy một bao đóng lấy một bao đóng làm đối số.


7

Các bao đóng tương đương với các đối tượng thực hiện một phương thức run () và ngược lại, các đối tượng có thể được mô phỏng với các bao đóng.

  • Ưu điểm của việc đóng cửa là chúng có thể được sử dụng dễ dàng ở bất cứ nơi nào bạn mong đợi một chức năng: còn gọi là các hàm bậc cao hơn, các cuộc gọi lại đơn giản (hoặc Mô hình chiến lược). Bạn không cần xác định giao diện / lớp để xây dựng các bao đóng đặc biệt.

  • Ưu điểm của các đối tượng là khả năng có các tương tác phức tạp hơn: nhiều phương thức và / hoặc các giao diện khác nhau.

Vì vậy, sử dụng đóng cửa hoặc các đối tượng chủ yếu là một vấn đề của phong cách. Dưới đây là một ví dụ về những điều đóng cửa dễ dàng nhưng không thuận tiện khi thực hiện với các đối tượng:

 (let ((seen))
    (defun register-name (name)
       (pushnew name seen :test #'string=))

    (defun all-names ()
       (copy-seq seen))

    (defun reset-name-registry ()
       (setf seen nil)))

Về cơ bản, bạn gói gọn một trạng thái ẩn chỉ được truy cập thông qua các bao đóng toàn cục: bạn không cần tham chiếu đến bất kỳ đối tượng nào, chỉ sử dụng giao thức được xác định bởi ba chức năng.


  • Tôi đang mở rộng câu trả lời để giải quyết nhận xét này từ supercat *.

Tôi tin tưởng nhận xét đầu tiên của supercat về thực tế là trong một số ngôn ngữ, có thể kiểm soát chính xác tuổi thọ của các vật thể, trong khi điều tương tự không đúng với việc đóng cửa. Tuy nhiên, trong trường hợp các ngôn ngữ được thu gom rác, tuổi thọ của các đối tượng thường không bị ràng buộc và do đó có thể xây dựng một bao đóng có thể được gọi trong một bối cảnh động, nơi nó không nên được gọi (đọc từ một bao đóng sau một luồng đã đóng cửa chẳng hạn).

Tuy nhiên, khá đơn giản để ngăn chặn việc sử dụng sai như vậy bằng cách nắm bắt một biến điều khiển sẽ bảo vệ việc thực hiện đóng. Chính xác hơn, đây là những gì tôi có trong tâm trí (trong Common Lisp):

(defun guarded (function)
  (let ((active t))
    (values (lambda (&rest args)
              (when active
                (apply function args)))
            (lambda ()
              (setf active nil)))))

Ở đây, chúng ta lấy một hàm chỉ định hàm functionvà trả về hai lần đóng, cả hai đều bắt một biến cục bộ có tên active:

  • đại biểu đầu tiên function, chỉ khi nào activeđúng
  • cái thứ hai đặt actionthành nil, aka false.

Thay vào đó (when active ...), tất nhiên có thể có một (assert active)biểu thức, có thể đưa ra một ngoại lệ trong trường hợp đóng cửa được gọi khi không nên. Ngoài ra, hãy nhớ rằng mã không an toàn có thể đã tự ném một ngoại lệ khi được sử dụng không tốt, vì vậy bạn hiếm khi cần một trình bao bọc như vậy.

Đây là cách bạn sẽ sử dụng nó:

(use-package :metabang-bind) ;; for bind

(defun example (obj1 obj2)
  (bind (((:values f f-deactivator)(guarded (lambda () (do-stuff obj1))))
         ((:values g g-deactivator)(guarded (lambda () (do-thing obj2)))))

    ;; ensure the closure are inactive when we exit
    (unwind-protect
         ;; pass closures to other functions
         (progn
           (do-work f)
           (do-work g))

      ;; cleanup code: deactivate closures
      (funcall f-deactivator)
      (funcall g-deactivator))))

Lưu ý rằng các đóng cửa khử lưu động cũng có thể được cung cấp cho các chức năng khác; ở đây, các activebiến cục bộ không được chia sẻ giữa fg; Ngoài ra, ngoài active, fchỉ đề cập đến obj1gchỉ đề cập đến obj2.

Điểm khác được đề cập bởi supercat là việc đóng cửa có thể dẫn đến rò rỉ bộ nhớ, nhưng thật không may, đó là trường hợp của hầu hết mọi thứ trong môi trường thu gom rác. Nếu chúng có sẵn, điều này có thể được giải quyết bằng các con trỏ yếu (bản thân việc đóng có thể được giữ trong bộ nhớ, nhưng không ngăn chặn việc thu gom rác các tài nguyên khác).


1
Một nhược điểm của các bao đóng như thường được thực hiện là một khi một bao đóng sử dụng một trong các biến cục bộ của một phương thức được đưa ra thế giới bên ngoài, phương thức xác định bao đóng có thể mất kiểm soát khi đọc và ghi biến đó. Trong hầu hết các ngôn ngữ, không có cách thuận tiện để xác định đóng cửa phù du, chuyển nó sang một phương thức và đảm bảo rằng nó sẽ không còn tồn tại khi phương thức nhận được nó trở lại, cũng không có cách nào trong các ngôn ngữ đa luồng đối với đảm bảo rằng việc đóng sẽ không được chạy trong bối cảnh luồng bất ngờ.
supercat

@supercat: Hãy xem Objective-C và Swift.
gnasher729

1
@supercat Sẽ không có nhược điểm tương tự tồn tại với các đối tượng trạng thái?
Andres F.

@AndresF.: Có thể gói gọn một đối tượng trạng thái trong một trình bao bọc hỗ trợ vô hiệu; nếu mã đóng gói riêng tư List<T>trong một (lớp giả định) TemporaryMutableListWrapper<T>và phơi bày nó ra mã bên ngoài, có thể đảm bảo rằng nếu nó vô hiệu hóa trình bao bọc, mã bên ngoài sẽ không còn cách nào để thao tác List<T>. Người ta có thể thiết kế các bao đóng để cho phép vô hiệu hóa một khi chúng phục vụ mục đích mong đợi của họ, nhưng điều đó hầu như không thuận tiện. Đóng cửa tồn tại để làm cho các mẫu nhất định thuận tiện, và nỗ lực cần thiết để bảo vệ chúng sẽ phủ nhận điều đó.
supercat

1
@coredump: Trong C #, nếu hai bao đóng có bất kỳ biến chung nào, cùng một đối tượng do trình biên dịch tạo sẽ phục vụ cả hai, vì các thay đổi được thực hiện cho một biến được chia sẻ bởi một bao đóng phải được nhìn thấy bởi một bao đóng khác. Tránh việc chia sẻ đó sẽ yêu cầu mỗi bao đóng là đối tượng riêng chứa các biến không chia sẻ riêng cũng như tham chiếu đến đối tượng chia sẻ chứa các biến được chia sẻ. Không phải là không thể, nhưng nó sẽ có thêm một mức độ hội nghị bổ sung cho hầu hết các truy cập khác nhau, làm chậm mọi thứ.
supercat

6

Không có gì chưa được nói, nhưng có lẽ là một ví dụ đơn giản hơn.

Đây là một ví dụ JavaScript sử dụng thời gian chờ:

// Example function that logs something to the browser's console after a given delay
function delayedLog(message, delay) {
  // this function will be called when the timer runs out
  var fire = function () {
    console.log(message); // closure magic!
  };

  // set a timeout that'll call fire() after a delay
  setTimeout(fire, delay);
}

Điều xảy ra ở đây, là khi delayedLog()được gọi, nó sẽ trả về ngay lập tức sau khi đặt thời gian chờ và thời gian chờ tiếp tục ở dưới nền.

Nhưng khi hết thời gian và gọi fire() chức năng, bàn điều khiển sẽ hiển thị cái messageban đầu được chuyển đến delayedLog(), bởi vì nó vẫn có sẵn fire()thông qua việc đóng. Bạn có thể gọi delayedLog()bao nhiêu tùy ý, với một tin nhắn khác nhau và trì hoãn mỗi lần, và nó sẽ làm điều đúng đắn.

Nhưng hãy tưởng tượng JavaScript không có sự đóng cửa.

Một cách sẽ là thực hiện setTimeout()chặn - giống như chức năng "ngủ" - vì vậy delayedLog()phạm vi sẽ không biến mất cho đến khi hết thời gian. Nhưng chặn mọi thứ không đẹp lắm.

Một cách khác là đặt message biến trong một số phạm vi khác có thể truy cập được sau khi hết delayedLog()phạm vi.

Bạn có thể sử dụng các biến toàn cục - hoặc ít nhất là "phạm vi rộng hơn" - nhưng bạn phải tìm ra cách theo dõi tin nhắn đi với thời gian chờ nào. Nhưng nó không thể là một hàng đợi theo thứ tự, hàng tuần, bởi vì bạn có thể đặt bất kỳ độ trễ nào bạn muốn. Vì vậy, nó có thể là "đầu vào, ra thứ ba" hoặc một cái gì đó. Vì vậy, bạn cần một số phương tiện khác để buộc một hàm tính thời gian vào các biến mà nó cần.

Bạn có thể khởi tạo một đối tượng hết thời gian "nhóm" bộ hẹn giờ với tin nhắn. Bối cảnh của một đối tượng ít nhiều là một phạm vi dính xung quanh. Sau đó, bạn sẽ có bộ định thời thực hiện trong ngữ cảnh của đối tượng, vì vậy nó sẽ có quyền truy cập vào đúng thông báo. Nhưng bạn phải lưu trữ đối tượng đó bởi vì không có bất kỳ tài liệu tham khảo nào, nó sẽ bị thu gom rác (không có bao đóng, cũng không có tài liệu tham khảo ngầm nào về nó). Và bạn sẽ phải xóa đối tượng một khi nó hết thời gian chờ, nếu không nó sẽ chỉ bám xung quanh. Vì vậy, bạn cần một số loại danh sách các đối tượng hết thời gian và định kỳ kiểm tra nó để tìm các đối tượng "đã chi" để xóa - hoặc các đối tượng sẽ tự thêm và xóa khỏi danh sách và ...

Vì vậy, vâng, điều này đang trở nên buồn tẻ.

Rất may, bạn không phải sử dụng phạm vi rộng hơn hoặc sắp xếp các đối tượng chỉ để giữ các biến nhất định xung quanh. Vì JavaScript đã đóng, bạn đã có chính xác phạm vi bạn cần. Một phạm vi cho phép bạn truy cập vào messagebiến khi bạn cần nó. Và vì điều đó, bạn có thể thoát khỏi việc viết delayedLog()như trên.


Tôi có một vấn đề gọi đó là đóng cửa . Có lẽ tôi sẽ đóngvô tình đóng cửa . messageđược bao trong phạm vi chức năng của firevà do đó được gọi trong các cuộc gọi tiếp theo; nhưng nó vô tình để nói như vậy. Về mặt kỹ thuật nó là một đóng cửa. Dù sao +1;)
Thomas Junk

@ThomasJunk Tôi không chắc là tôi khá theo dõi. Làm thế nào một " đóng cửa không chính thức" sẽ trông như thế nào? Tôi thấy makeadderví dụ của bạn ở trên, mà trong mắt tôi, xuất hiện nhiều như vậy. Bạn trả về một hàm "curried" mất một đối số thay vì hai; sử dụng cùng một phương tiện, tôi tạo một hàm không có đối số. Tôi chỉ không trả lại mà setTimeoutthay vào đó.
Flambino

»Tôi chỉ không trả lại« có lẽ, đó là điểm, điều làm nên sự khác biệt cho tôi. Tôi không thể nói rõ "mối quan tâm" của mình;) Về mặt kỹ thuật, bạn hoàn toàn đúng 100%. Tham chiếu messagetrong việc firetạo ra các đóng cửa. Và khi nó được gọi trong setTimeoutnó sử dụng trạng thái được bảo tồn.
Thomas Junk

1
@ThomasJunk Tôi thấy nó có thể có mùi hơi khác. Mặc dù vậy, tôi có thể không chia sẻ mối quan tâm của bạn :) Hoặc, bằng mọi giá, tôi sẽ không gọi đó là "tình cờ" - khá chắc chắn rằng tôi đã mã hóa nó theo cách đó;)
Flambino

3

PHP có thể được sử dụng để giúp hiển thị một ví dụ thực tế bằng một ngôn ngữ khác.

protected function registerRoutes($dic)
{
  $router = $dic['router'];

  $router->map(['GET','OPTIONS'],'/api/users',function($request,$response) use ($dic)
  {
    $controller = $dic['user_api_controller'];
    return $controller->findAllAction($request,$response);
  })->setName('api_users');
}

Vì vậy, về cơ bản tôi đang đăng ký một hàm sẽ được thực thi cho URI / api / users . Đây thực sự là một chức năng trung gian mà cuối cùng được lưu trữ trên một ngăn xếp. Các chức năng khác sẽ được bọc xung quanh nó. Khá giống Node.js / Express.js .

Các dependency injection container là có sẵn (thông qua các điều khoản sử dụng) bên trong hàm khi nó được gọi. Có thể tạo một số loại lớp hành động tuyến, nhưng mã này hóa ra đơn giản hơn, nhanh hơn và dễ bảo trì hơn.


-1

Một bao đóng là một đoạn mã tùy ý, bao gồm các biến, có thể được xử lý như dữ liệu hạng nhất.

Một ví dụ tầm thường là qsort cũ tốt: Đó là một chức năng để sắp xếp dữ liệu. Bạn phải cung cấp cho nó một con trỏ tới một hàm so sánh hai đối tượng. Vì vậy, bạn phải viết một chức năng. Hàm đó có thể cần được tham số hóa, có nghĩa là bạn cung cấp cho nó các biến tĩnh. Điều đó có nghĩa là nó không an toàn. Bạn đang ở DS. Vì vậy, bạn viết một thay thế có một bao đóng thay vì một con trỏ hàm. Bạn ngay lập tức giải quyết vấn đề tham số hóa vì các tham số trở thành một phần của bao đóng. Bạn làm cho mã của mình dễ đọc hơn vì bạn viết cách so sánh trực tiếp các đối tượng với mã gọi hàm sắp xếp.

Có rất nhiều tình huống mà bạn muốn thực hiện một số hành động đòi hỏi nhiều mã tấm nồi hơi, cộng với một đoạn mã nhỏ nhưng cần thiết cần được điều chỉnh. Bạn tránh mã soạn sẵn bằng cách viết một hàm một lần mà phải mất một tham số đóng cửa và làm tất cả các mã soạn sẵn xung quanh nó, và sau đó bạn có thể gọi chức năng này và vượt qua các mã được chuyển thể thành đóng cửa. Một cách rất nhỏ gọn và dễ đọc để viết mã.

Bạn có một hàm trong đó một số mã không tầm thường cần được thực hiện trong nhiều tình huống khác nhau. Điều này được sử dụng để tạo mã trùng lặp hoặc mã bị méo để mã không tầm thường chỉ xuất hiện một lần. Trivial: Bạn chỉ định một bao đóng cho một biến và gọi nó theo cách rõ ràng nhất bất cứ nơi nào cần thiết.

Đa luồng: iOS / MacOS X có các chức năng để thực hiện những việc như "thực hiện việc đóng này trên một luồng nền", "... trên luồng chính", "... trên luồng chính, 10 giây kể từ bây giờ". Nó làm cho đa luồng tầm thường .

Các cuộc gọi không đồng bộ: Đó là những gì OP thấy. Bất kỳ cuộc gọi nào truy cập internet hoặc bất kỳ cuộc gọi nào khác có thể mất thời gian (như đọc tọa độ GPS) là điều mà bạn không thể chờ đợi kết quả. Vì vậy, bạn có các hàm thực hiện mọi thứ trong nền, và sau đó bạn vượt qua một bao đóng để cho chúng biết phải làm gì khi chúng kết thúc.

Đó là một khởi đầu nhỏ. Năm tình huống đóng cửa là một cuộc cách mạng về mặt sản xuất mã nhỏ gọn, dễ đọc, tin cậy và hiệu quả.


-4

Một bao đóng là một cách viết nhanh của một phương thức mà nó sẽ được sử dụng. Nó giúp bạn tiết kiệm công sức khai báo và viết một phương thức riêng biệt. Nó rất hữu ích khi phương thức sẽ chỉ được sử dụng một lần và định nghĩa phương thức ngắn. Các lợi ích được giảm gõ vì không cần chỉ định tên của hàm, kiểu trả về của nó hoặc công cụ sửa đổi truy cập của nó. Ngoài ra, khi đọc mã, bạn không cần phải tìm định nghĩa của phương thức khác.

Trên đây là tóm tắt về Hiểu về Lambda Expressions của Dan Avidar.

Điều này làm rõ việc sử dụng các bao đóng cho tôi vì nó làm rõ các lựa chọn thay thế (đóng so với phương pháp) và lợi ích của mỗi cách đóng.

Đoạn mã sau được sử dụng một lần và chỉ một lần trong khi thiết lập. Viết nó vào vị trí dưới viewDidLoad giúp tiết kiệm rắc rối khi tìm kiếm nó ở nơi khác và rút ngắn kích thước của mã.

myPhoton!.getVariable("Temp", completion: { (result:AnyObject!, error:NSError!) -> Void in
  if let e = error {
    self.getTempLabel.text = "Failed reading temp"
  } else {
    if let res = result as? Float {
    self.getTempLabel.text = "Temperature is \(res) degrees"
    }
  }
})

Ngoài ra, nó cung cấp cho một quá trình không đồng bộ để hoàn thành mà không chặn các phần khác của chương trình, và, một bao đóng sẽ giữ lại một giá trị để sử dụng lại trong các lệnh gọi hàm tiếp theo.

Một đóng cửa khác; cái này ghi lại một giá trị ...

let animals = ["fish", "cat", "chicken", "dog"]
let sortedStrings = animals.sorted({ (one: String, two: String) -> Bool in return one > two
}) println(sortedStrings)

4
Thật không may, điều này gây nhầm lẫn đóng cửa và lambdas. Lambdas thường sử dụng các bao đóng, bởi vì chúng thường hữu ích hơn nhiều nếu được định nghĩa a) rất chặt chẽ và b) bên trong và tùy thuộc vào bối cảnh phương thức đã cho, bao gồm các biến. Tuy nhiên, việc đóng cửa thực tế không liên quan gì đến ý tưởng về lambda, về cơ bản là khả năng xác định hàm hạng nhất tại chỗ và chuyển nó xung quanh để sử dụng sau.
Nathan Tuggy

Trong khi bạn đang bỏ phiếu cho câu trả lời này, hãy đọc nó ... Khi nào tôi nên bỏ phiếu? Sử dụng downvote của bạn bất cứ khi nào bạn gặp phải một bài viết quá cẩu thả, không tốn công sức hoặc một câu trả lời rõ ràng và có lẽ nguy hiểm không chính xác. Câu trả lời của tôi có thể thiếu một số lý do cho việc sử dụng bao đóng, nhưng nó không quá cẩu thả cũng không nguy hiểm. Có nhiều bài báo và thảo luận về việc đóng cửa tập trung vào lập trình chức năng và ký hiệu lambda. Tất cả câu trả lời của tôi nói là giải thích về lập trình chức năng này đã giúp tôi hiểu được sự đóng cửa. Đó và giá trị bền bỉ.
Bendrix

1
Câu trả lời có lẽ không nguy hiểm , nhưng chắc chắn là "rõ ràng [không] không chính xác". Nó sử dụng các thuật ngữ sai cho các khái niệm có tính bổ sung cao và do đó thường được sử dụng cùng nhau - chính xác là nơi rõ ràng của sự phân biệt là điều cần thiết. Điều này có khả năng gây ra vấn đề thiết kế cho các lập trình viên đọc điều này nếu không được giải quyết. (Và từ đó, như xa như tôi có thể nói, ví dụ mã đơn giản không chứa bất kỳ đóng cửa, chỉ có một hàm lambda, nó không giúp giải thích tại sao việc đóng cửa sẽ là hữu ích.)
Nathan Tuggy

Nathan, ví dụ mã đó là một đóng cửa trong Swift. Bạn có thể hỏi người khác nếu bạn nghi ngờ tôi. Vì vậy, nếu nó là một đóng cửa, bạn sẽ bỏ phiếu?
Bendrix

1
Apple mô tả khá rõ ràng chính xác những gì họ có nghĩa là bằng cách đóng trong tài liệu của họ . "Các bao đóng là các khối chức năng khép kín có thể được truyền qua và sử dụng trong mã của bạn. Các bao đóng trong Swift tương tự như các khối trong C và Objective-C và với lambdas trong các ngôn ngữ lập trình khác." Khi bạn nhận được để thực hiện và thuật ngữ nó có thể gây nhầm lẫ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.