Tại sao các ngôn ngữ lập trình cho phép tạo bóng / ẩn các biến và hàm?


31

Nhiều trong số các langug lập trình phổ biến nhất (như C ++, Java, Python, v.v.) có khái niệm ẩn / tạo bóng các biến hoặc hàm. Khi tôi gặp phải việc ẩn hoặc che giấu, chúng là nguyên nhân gây ra lỗi khó tìm và tôi chưa bao giờ thấy trường hợp nào tôi thấy cần phải sử dụng các tính năng này của các ngôn ngữ.

Đối với tôi có vẻ tốt hơn là không cho phép che giấu và che giấu.

Có ai biết sử dụng tốt các khái niệm này?

Cập nhật:
Tôi không đề cập đến việc đóng gói các thành viên trong lớp (tư nhân / thành viên được bảo vệ).


Đó là lý do tại sao tất cả các tên trường của tôi bắt đầu bằng F.
Pieter B

7
Tôi nghĩ Eric Lippert đã có một bài viết hay về điều này. Oh chờ đợi, ở đây là: blogs.msdn.com/b/ericlippert/archive/2008/05/21/...
Lescai Ionel

1
Hãy làm rõ câu hỏi của bạn. Bạn đang hỏi về việc ẩn thông tin nói chung, hoặc về trường hợp cụ thể được mô tả trong bài viết Lippert trong đó một lớp dẫn xuất ẩn các hàm của lớp cơ sở?
Aaron Kurtzhals

Lưu ý quan trọng: Rất nhiều lỗi gây ra bởi ẩn / đổ bóng liên quan đến đột biến (đặt biến sai và tự hỏi tại sao thay đổi "không bao giờ xảy ra" chẳng hạn). Khi làm việc chủ yếu với các tài liệu tham khảo bất biến, ẩn / tạo bóng gây ra ít vấn đề hơn và ít có khả năng gây ra lỗi hơn.
Jack

Câu trả lời:


26

Nếu bạn không cho phép ẩn và tạo bóng, những gì bạn có là một ngôn ngữ trong đó tất cả các biến là toàn cục.

Điều đó rõ ràng tồi tệ hơn việc cho phép các biến hoặc hàm cục bộ có thể ẩn các biến hoặc hàm toàn cục.

Nếu bạn không cho phép ẩn và tạo bóng, bạn cố gắng "bảo vệ" một số biến toàn cục nhất định, bạn sẽ tạo một tình huống trong đó trình biên dịch nói với lập trình viên "Tôi xin lỗi, Dave, nhưng bạn không thể sử dụng tên đó, nó đã được sử dụng . " Kinh nghiệm với COBOL cho thấy các lập trình viên gần như ngay lập tức dùng đến sự thô tục trong tình huống này.

Vấn đề cơ bản không phải là ẩn / bóng, mà là các biến toàn cục.


19
Một nhược điểm khác của việc cấm tạo bóng là việc thêm một biến toàn cục có thể phá vỡ mã vì biến này đã được sử dụng trong một khối cục bộ.
Giorgio

19
"Nếu bạn không cho phép ẩn và tạo bóng, những gì bạn có là một ngôn ngữ trong đó tất cả các biến là toàn cầu." - không nhất thiết: bạn có thể có các biến trong phạm vi mà không bị bóng và bạn đã giải thích nó.
Thiago Silva

@ThiagoSilva: Và sau đó ngôn ngữ của bạn phải có một cách để nói với trình biên dịch rằng mô-đun này IS phép truy cập rằng "frammis" biến mô-đun của. Bạn có cho phép ai đó che giấu / che giấu một đối tượng mà anh ta thậm chí không biết tồn tại hay bạn nói với anh ta về điều đó để nói với anh ta tại sao anh ta không được phép sử dụng tên đó?
John R. Strohm

9
@Phil, xin lỗi vì tôi không đồng ý với bạn, nhưng OP đã hỏi về "ẩn / che giấu các biến hoặc hàm" và các từ "cha mẹ", "con", "lớp" và "thành viên" không xuất hiện trong câu hỏi của anh ấy. Điều đó dường như làm cho nó một câu hỏi chung về phạm vi tên.
John R. Strohm

3
@dylnmc, tôi không mong đợi sống đủ lâu để gặp một người đánh cá đủ trẻ để không nhận được một tài liệu tham khảo "2001: A Space Odyssey" rõ ràng.
John R. Strohm

15

Có ai biết sử dụng tốt các khái niệm này?

Sử dụng các định danh mô tả chính xác, luôn luôn là một sử dụng tốt.

Tôi có thể lập luận rằng việc ẩn biến không gây ra nhiều lỗi do có hai biến được đặt tên rất giống nhau có cùng loại / giống nhau (những gì bạn làm nếu ẩn biến không được phép) có thể gây ra nhiều lỗi và / hoặc giống như lỗi nặng. Tôi không biết liệu lập luận đó có đúng không , nhưng ít nhất nó cũng gây tranh cãi.

Sử dụng một số loại ký hiệu tiếng Đức để phân biệt các trường so với các biến cục bộ xung quanh vấn đề này, nhưng có tác động riêng của nó đối với việc bảo trì (và sự tỉnh táo của lập trình viên).

Và (có lẽ rất có thể là lý do mà khái niệm này được biết đến ngay từ đầu), các ngôn ngữ dễ dàng thực hiện việc ẩn / tạo bóng hơn là không cho phép nó. Thực hiện dễ dàng hơn có nghĩa là trình biên dịch ít có khả năng có lỗi. Việc thực hiện dễ dàng hơn có nghĩa là các trình biên dịch mất ít thời gian hơn để viết, khiến việc áp dụng nền tảng sớm hơn và rộng hơn.


3
Trên thực tế, không, nó không dễ dàng hơn để thực hiện ẩn và bóng. Nó thực sự dễ dàng hơn để thực hiện "tất cả các biến là toàn cầu". Bạn chỉ cần một không gian tên và bạn LUÔN xuất tên, trái ngược với việc có nhiều không gian tên và phải quyết định cho mỗi tên có xuất hay không.
John R. Strohm

5
@ JohnR.Strohm - Chắc chắn, nhưng ngay khi bạn có bất kỳ loại phạm vi nào (đọc: các lớp) thì việc có các phạm vi ẩn phạm vi thấp hơn là có miễn phí.
Telastyn

Phạm vi và các lớp học là những thứ khác nhau. Ngoại trừ BASIC, mọi ngôn ngữ tôi đã lập trình đều có phạm vi, nhưng không phải tất cả chúng đều có bất kỳ khái niệm nào về các lớp hoặc đối tượng.
Michael Shaw

@michaelshaw - tất nhiên, tôi nên đã rõ ràng hơn.
Telastyn

7

Chỉ để đảm bảo chúng ta ở trên cùng một trang, phương thức "ẩn" là khi một lớp dẫn xuất định nghĩa một thành viên cùng tên với một lớp trong lớp cơ sở (nếu đó là một phương thức / thuộc tính, không được đánh dấu ảo / overridable ) và khi được gọi từ một thể hiện của lớp dẫn xuất trong "bối cảnh dẫn xuất", thành viên dẫn xuất được sử dụng, trong khi nếu được gọi bởi cùng một thể hiện trong ngữ cảnh của lớp cơ sở của nó, thì thành viên lớp cơ sở được sử dụng. Điều này khác với trừu tượng / ghi đè thành viên trong đó thành viên lớp cơ sở mong muốn lớp dẫn xuất xác định thay thế và từ các bộ sửa đổi phạm vi / khả năng hiển thị "ẩn" một thành viên khỏi người tiêu dùng ngoài phạm vi mong muốn.

Câu trả lời ngắn gọn về lý do tại sao nó được cho phép là không làm như vậy sẽ buộc các nhà phát triển vi phạm một số nguyên lý chính của thiết kế hướng đối tượng.

Đây là câu trả lời dài hơn; trước tiên, hãy xem xét cấu trúc lớp sau trong một vũ trụ thay thế nơi C # không cho phép thành viên ẩn:

public interface IFoo
{
   string MyFooString {get;}
   int FooMethod();
}

public class Foo:IFoo
{
   public string MyFooString {get{return "Foo";}}
   public int FooMethod() {//incredibly useful code here};
}

public class Bar:Foo
{
   //public new string MyFooString {get{return "Bar";}}
}

Chúng tôi muốn bỏ ghi chú thành viên trong Bar và bằng cách đó, cho phép Bar cung cấp MyFooString khác. Tuy nhiên, chúng tôi không thể làm như vậy bởi vì nó sẽ vi phạm các quy định cấm thực tế thay thế đối với việc che giấu thành viên. Ví dụ cụ thể này sẽ đầy rẫy các lỗi và là một ví dụ điển hình về lý do tại sao bạn có thể muốn cấm nó; chẳng hạn, đầu ra giao diện điều khiển nào bạn sẽ nhận được nếu bạn làm như sau?

Bar myBar = new Bar();
Foo myFoo = myBar;
IFoo myIFoo = myFoo;

Console.WriteLine(myFoo.MyFooString);
Console.WriteLine(myBar.MyFooString);
Console.WriteLine(myIFoo.MyFooString);

Ngoài đỉnh đầu, tôi thực sự không chắc là bạn sẽ nhận được "Foo" hay "Bar" trên dòng cuối cùng đó. Bạn chắc chắn sẽ nhận được "Foo" cho dòng đầu tiên và "Bar" cho dòng thứ hai, mặc dù cả ba biến tham chiếu chính xác cùng một thể hiện với cùng một trạng thái.

Vì vậy, các nhà thiết kế của ngôn ngữ, trong vũ trụ thay thế của chúng ta, không khuyến khích mã rõ ràng xấu này bằng cách ngăn chặn tài sản che giấu. Bây giờ, bạn là một lập trình viên có nhu cầu thực sự để làm chính xác điều này. Làm thế nào để bạn có được xung quanh giới hạn? Chà, một cách là đặt tên tài sản của Bar khác nhau:

public class Bar:Foo
{
   public string MyBarString {get{return "Bar";}}       
}

Hoàn toàn hợp pháp, nhưng đó không phải là hành vi chúng ta muốn. Một phiên bản của Bar sẽ luôn tạo ra "Foo" cho thuộc tính MyFooString, khi chúng tôi muốn nó tạo ra "Bar". Chúng tôi không chỉ phải biết rằng IFoo của chúng tôi cụ thể là một Bar, chúng tôi còn phải biết sử dụng các bộ truy cập khác nhau.

Chúng ta cũng có thể, khá có lý, quên mối quan hệ cha-con và trực tiếp thực hiện giao diện:

public class Bar:IFoo
{
   public string MyFooString {get{return "Bar";}}
   public int FooMethod() {...}
}

Đối với ví dụ đơn giản này, đó là một câu trả lời hoàn hảo, miễn là bạn chỉ quan tâm rằng Foo và Bar đều là IFoos. Mã sử ​​dụng một vài ví dụ trở lên sẽ không thể biên dịch vì Bar không phải là Foo và không thể được chỉ định như vậy. Tuy nhiên, nếu Foo có một số phương thức hữu ích "FooMethod" mà Bar cần, thì bây giờ bạn không thể kế thừa phương thức đó; bạn sẽ phải sao chép mã của nó trong Bar hoặc sáng tạo:

public class Bar:IFoo
{
   public string MyFooString {get{return "Bar";}}
   private readonly theFoo = new Foo();

   public int FooMethod(){return theFoo.FooMethod();}
}

Đây là một vụ hack rõ ràng và trong khi một số triển khai thông số kỹ thuật ngôn ngữ OO ít hơn số này, về mặt khái niệm thì nó đã sai; nếu người tiêu dùng của Bar cần tiết lộ chức năng của Foo, thì Bar phải Foo, không Foo.

Rõ ràng, nếu chúng ta điều khiển Foo, chúng ta có thể biến nó thành ảo, sau đó ghi đè lên nó. Đây là cách thực hành tốt nhất về mặt khái niệm trong vũ trụ hiện tại của chúng ta khi một thành viên dự kiến ​​sẽ bị ghi đè và sẽ nắm giữ trong bất kỳ vũ trụ thay thế nào không cho phép ẩn náu:

public class Foo:IFoo
{
   public virtual string MyFooString {get{return "Foo";}}
   //...
}

public class Bar:Foo
{
   public override string MyFooString {get{return "Bar";}}
}

Vấn đề với điều này là việc truy cập thành viên ảo, dưới mui xe, tương đối tốn kém hơn để thực hiện, và vì vậy bạn thường chỉ muốn làm điều đó khi bạn cần. Tuy nhiên, việc không che giấu buộc bạn phải bi quan về các thành viên mà một lập trình viên khác không kiểm soát mã nguồn của bạn có thể muốn thực hiện lại; "cách thực hành tốt nhất" cho bất kỳ lớp không niêm phong nào sẽ là biến mọi thứ thành ảo trừ khi bạn đặc biệt không muốn nó trở thành. Nó cũng vẫn không cung cấp cho bạn hành vi che giấu chính xác; chuỗi sẽ luôn là "Bar" nếu thể hiện là Bar. Đôi khi nó thực sự hữu ích để tận dụng các lớp dữ liệu trạng thái ẩn, dựa trên mức độ kế thừa mà bạn đang làm việc.

Tóm lại, cho phép ẩn thành viên là ít tệ nạn hơn. Không có nó nói chung sẽ dẫn đến sự tàn bạo tồi tệ hơn cam kết chống lại các nguyên tắc hướng đối tượng hơn là cho phép nó.


+1 để giải quyết câu hỏi thực tế. Một ví dụ điển hình về việc sử dụng ẩn thành viên trong thế giới thực là giao diện IEnumerableIEnumerable<T>giao diện, được mô tả trong bài đăng trên blog của Eric Libbert về chủ đề này.
Phil

Ghi đè là không che giấu. Tôi không đồng ý với @Phil rằng điều này giải quyết câu hỏi.
Jan Hudec

Quan điểm của tôi là ghi đè sẽ thay thế cho việc ẩn khi ẩn không phải là một lựa chọn. Tôi đồng ý, nó không che giấu, và tôi nói nhiều như vậy trong đoạn đầu tiên. Không có cách giải quyết nào cho kịch bản thực tế thay thế của tôi về việc không ẩn trong C # đang ẩn; đó là điểm.
KeithS

Tôi không thích việc bạn sử dụng bóng / ẩn. Các lợi ích chính mà tôi thấy là (1) bao gồm tình huống trong đó một phiên bản mới của lớp cơ sở bao gồm một thành viên mâu thuẫn với mã tiêu dùng được thiết kế xung quanh một phiên bản cũ hơn [xấu nhưng cần thiết]; (2) giả mạo những thứ như hiệp phương sai kiểu trả về; (3) xử lý các trường hợp trong đó phương thức lớp cơ sở có thể gọi được trên một kiểu con cụ thể nhưng không hữu ích . LSP yêu cầu cái trước, nhưng không yêu cầu cái sau nếu hợp đồng lớp cơ sở quy định rằng một số phương thức có thể không hữu ích trong một số điều kiện.
supercat

2

Thành thật mà nói, Eric Lippert, nhà phát triển chính của nhóm biên dịch C #, giải thích điều đó khá tốt (cảm ơn Lescai Ionel về liên kết). .NET IEnumerableIEnumerable<T>giao diện là những ví dụ điển hình về việc ẩn thành viên là hữu ích.

Trong những ngày đầu của .NET, chúng tôi không có thuốc generic. Vì vậy, các IEnumerablegiao diện trông như thế này:

public interface IEnumerable
{
    IEnumerator GetEnumerator();
}

Giao diện này là thứ cho phép chúng ta foreachvượt qua một bộ sưu tập các đối tượng, tuy nhiên chúng ta phải bỏ tất cả các đối tượng đó để sử dụng chúng đúng cách.

Rồi đến thuốc generic. Khi chúng tôi có thuốc generic, chúng tôi cũng có một giao diện mới:

public interface IEnumerable<T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
}

Bây giờ chúng ta không phải đúc các đối tượng trong khi chúng ta đang lặp qua chúng! Khốn nạn! Bây giờ, nếu không cho phép ẩn thành viên, giao diện sẽ phải trông giống như thế này:

public interface IEnumerable<T> : IEnumerable
{
    IEnumerator<T> GetEnumeratorGeneric();
}

Điều này sẽ là loại ngớ ngẩn, bởi vì GetEnumerator()GetEnumeratorGeneric()trong cả hai trường hợp thực hiện khá nhiều chính xác cùng một điều , nhưng chúng có giá trị trả về hơi khác nhau. Trên thực tế, chúng giống nhau đến mức bạn luôn muốn mặc định ở dạng chung GetEnumerator, trừ khi bạn làm việc với mã kế thừa được viết trước khi thuốc generic được đưa vào .NET.

Đôi khi việc ẩn thành viên không cho phép nhiều chỗ hơn cho các mã khó chịu và các lỗi khó tìm. Tuy nhiên, đôi khi nó hữu ích, chẳng hạn như khi bạn muốn thay đổi loại trả về mà không vi phạm mã kế thừa. Đó chỉ là một trong những quyết định mà các nhà thiết kế ngôn ngữ phải đưa ra: Chúng ta có gây bất tiện cho các nhà phát triển cần hợp pháp tính năng này và loại bỏ nó hay chúng ta đưa tính năng này vào ngôn ngữ và bắt những kẻ là nạn nhân của việc lạm dụng nó?


Trong khi chính thức IEnumerable<T>.GetEnumerator()ẩn IEnumerable.GetEnumerator(), điều này chỉ là do C # không có kiểu trả về covariant khi ghi đè. Về mặt logic, nó là một ghi đè, hoàn toàn phù hợp với LSP. Ẩn là khi bạn có biến cục bộ maptrong hàm trong tệp đó using namespace std(trong C ++).
Jan Hudec

2

Câu hỏi của bạn có thể được đọc theo hai cách: hoặc bạn đang hỏi về phạm vi biến / hàm nói chung hoặc bạn đang hỏi một câu hỏi cụ thể hơn về phạm vi trong hệ thống phân cấp thừa kế. Bạn đã không đề cập cụ thể đến việc thừa kế, nhưng bạn đã đề cập đến các lỗi khó tìm, có vẻ giống phạm vi trong bối cảnh thừa kế hơn phạm vi đơn giản, vì vậy tôi sẽ trả lời cả hai câu hỏi.

Phạm vi nói chung là một ý tưởng tốt, bởi vì nó cho phép chúng tôi tập trung sự chú ý của chúng tôi vào một phần cụ thể (hy vọng nhỏ) của chương trình. Bởi vì nó cho phép tên địa phương luôn giành chiến thắng, nếu bạn chỉ đọc phần của chương trình trong một phạm vi nhất định, thì bạn sẽ biết chính xác phần nào được xác định cục bộ và phần nào được xác định ở nơi khác. Tên này đề cập đến một cái gì đó cục bộ, trong trường hợp đó, mã xác định nó ở ngay trước mặt bạn, hoặc đó là một tham chiếu đến một cái gì đó bên ngoài phạm vi cục bộ. Nếu không có bất kỳ tài liệu tham khảo phi địa phương nào có thể thay đổi từ bên dưới chúng tôi (đặc biệt là các biến toàn cục, có thể thay đổi từ bất kỳ đâu), thì chúng tôi có thể đánh giá xem phần của chương trình trong phạm vi cục bộ có chính xác hay không mà không cần tham khảo cho bất kỳ phần nào của phần còn lại của chương trình .

Nó có thể đôi khi có thể dẫn đến một vài lỗi, nhưng nó bù đắp nhiều hơn bằng cách ngăn chặn một lượng lớn các lỗi có thể có. Khác với việc tạo một định nghĩa cục bộ có cùng tên với chức năng thư viện (không làm như vậy), tôi không thể thấy một cách dễ dàng để giới thiệu các lỗi có phạm vi cục bộ, nhưng phạm vi cục bộ là thứ cho phép nhiều phần của cùng một chương trình sử dụng i với tư cách là bộ đếm chỉ số cho một vòng lặp mà không ghi đè lên nhau và để Fred xuống hội trường viết một hàm sử dụng một chuỗi có tên str sẽ không ghi lại chuỗi của bạn cùng tên.

Tôi tìm thấy một bài viết thú vị của Bertrand Meyer nói về sự quá tải trong bối cảnh thừa kế. Anh ta đưa ra một sự khác biệt thú vị, giữa cái mà anh ta gọi là quá tải cú pháp (nghĩa là có hai thứ khác nhau có cùng tên) và quá tải ngữ nghĩa (có nghĩa là có hai cách thực hiện khác nhau của cùng một ý tưởng trừu tượng). Quá tải ngữ nghĩa sẽ tốt, vì bạn có nghĩa là thực hiện nó khác nhau trong lớp con; quá tải cú pháp sẽ là sự va chạm tên ngẫu nhiên gây ra lỗi.

Sự khác biệt giữa quá tải trong một tình huống thừa kế được dự định và một lỗi là ngữ nghĩa (ý nghĩa), vì vậy trình biên dịch không có cách nào để biết liệu những gì bạn đã làm là đúng hay sai. Trong một tình huống phạm vi đơn giản, câu trả lời đúng luôn luôn là điều cục bộ, vì vậy trình biên dịch có thể tìm ra điều gì là đúng.

Đề xuất của Bertrand Meyer sẽ là sử dụng một ngôn ngữ như Eiffel, không cho phép các cuộc đụng độ tên như thế này và buộc lập trình viên phải đổi tên một hoặc cả hai, do đó tránh hoàn toàn vấn đề. Đề nghị của tôi sẽ là tránh sử dụng hoàn toàn thừa kế, cũng tránh hoàn toàn vấn đề. Nếu bạn không thể hoặc không muốn làm một trong những điều đó, vẫn còn những điều bạn có thể làm để giảm xác suất gặp vấn đề với thừa kế: tuân theo LSP (Nguyên tắc thay thế Liskov), thích sáng tác hơn kế thừa, giữ hệ thống phân cấp thừa kế của bạn nông và giữ cho các lớp trong một hệ thống phân cấp thừa kế nhỏ. Ngoài ra, một số ngôn ngữ có thể đưa ra cảnh báo, ngay cả khi chúng không gây ra lỗi, như một ngôn ngữ như Eiffel.


2

Đây là hai xu của tôi.

Các chương trình có thể được cấu trúc thành các khối (chức năng, thủ tục) là các đơn vị độc lập của logic chương trình. Mỗi khối có thể đề cập đến "điều" (biến, hàm, thủ tục) bằng cách sử dụng tên / mã định danh. Ánh xạ này từ tên đến sự vật được gọi là ràng buộc .

Các tên được sử dụng bởi một khối rơi vào ba loại:

  1. Tên được xác định cục bộ, ví dụ các biến cục bộ, chỉ được biết bên trong khối.
  2. Các đối số được liên kết với các giá trị khi khối được gọi và có thể được người gọi sử dụng để chỉ định tham số đầu vào / đầu ra của khối.
  3. Tên / ràng buộc bên ngoài được xác định trong môi trường chứa khối và nằm trong phạm vi trong khối.

Xem xét ví dụ chương trình C sau đây

#include<stdio.h>

void print_double_int(int n)
{
  int d = n * 2;

  printf("%d\n", d);
}

int main(int argc, char *argv[])
{
  print_double_int(4);
}

Hàm print_double_intnày có một tên cục bộ (biến cục bộ) dvà đối số nvà sử dụng tên toàn cầu bên ngoài, printfnằm trong phạm vi nhưng không được xác định cục bộ.

Lưu ý rằng printfcũng có thể được thông qua như là một đối số:

#include<stdio.h>

void print_double_int(int n, int printf(const char *, ...))
{
  int d = n * 2;

  printf("%d\n", d);
}

int main(int argc, char *argv[])
{
  print_double_int(4, printf);
}

Thông thường, một đối số được sử dụng để chỉ định tham số đầu vào / đầu ra của hàm (thủ tục, khối), trong khi tên toàn cầu được sử dụng để chỉ những thứ như hàm thư viện "tồn tại trong môi trường", và do đó thuận tiện hơn khi đề cập đến chúng chỉ khi họ cần thiết Sử dụng các đối số thay vì tên toàn cầu là ý tưởng chính của phép nội xạ phụ thuộc , được sử dụng khi các phụ thuộc phải được làm rõ ràng thay vì được giải quyết bằng cách xem xét bối cảnh.

Một cách sử dụng tương tự khác của các tên được xác định bên ngoài có thể được tìm thấy trong các bao đóng. Trong trường hợp này, một tên được xác định trong ngữ cảnh từ vựng của một khối có thể được sử dụng trong khối và giá trị được liên kết với tên đó sẽ (tiếp tục) tồn tại miễn là khối đó đề cập đến nó.

Lấy ví dụ mã Scala này:

object ClosureExample
{
  def createMultiplier(n: Int) = (m: Int) => m * n

  def main(args: Array[String])
  {
    val multiplier3 = createMultiplier(3)
    val multiplier5 = createMultiplier(5)

    // Prints 6.
    println(multiplier3(2))

    // Prints 10.
    println(multiplier5(2))
  }
}

Giá trị trả về của hàm createMultiplierlà bao đóng (m: Int) => m * n, chứa đối số mvà tên bên ngoài n. Tên nđược giải quyết bằng cách xem xét bối cảnh trong đó bao đóng được xác định: tên được liên kết với đối số ncủa hàm createMultiplier. Lưu ý rằng ràng buộc này được tạo khi đóng được tạo, tức là khi createMultiplierđược gọi. Vì vậy, tên nđược liên kết với giá trị thực tế của một đối số cho một lệnh gọi cụ thể của hàm. Tương phản điều này với trường hợp của một chức năng thư viện như printfđược giải quyết bởi trình liên kết khi chương trình thực thi của chương trình được xây dựng.

Tóm tắt, có thể hữu ích khi tham khảo các tên bên ngoài trong một khối mã cục bộ để bạn

  • không cần / muốn chuyển các tên được xác định bên ngoài một cách rõ ràng làm đối số và
  • bạn có thể đóng băng các liên kết trong thời gian chạy khi một khối được tạo, và sau đó truy cập nó sau khi khối được gọi.

Shadowing xuất hiện khi bạn xem xét rằng trong một khối bạn chỉ quan tâm đến các tên có liên quan được xác định trong môi trường, ví dụ như trong printfhàm mà bạn muốn sử dụng. Nếu tình cờ bạn muốn sử dụng một tên địa phương ( getc, putc, scanf, ...) mà đã được sử dụng trong môi trường, bạn đơn giản muốn bỏ qua (shadow) tên toàn cầu. Vì vậy, khi suy nghĩ cục bộ, bạn không muốn xem xét toàn bộ bối cảnh (có thể rất lớn).

Theo hướng khác, khi suy nghĩ toàn cầu, bạn muốn bỏ qua các chi tiết nội bộ của bối cảnh địa phương (đóng gói). Do đó, bạn cần tạo bóng, nếu không, việc thêm tên toàn cầu có thể phá vỡ mọi khối cục bộ đang sử dụng tên đó.

Tóm lại, nếu bạn muốn một khối mã đề cập đến các ràng buộc được xác định bên ngoài, bạn cần tạo bóng để bảo vệ các tên cục bộ khỏi các tên chung.

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.