Tầm vực

Lịch sử

Tầm vực tĩnh (từ vựng) đã được dùng trong ALGOL và từ đó được nhiều ngôn ngữ khác sử dụng. Tầm vực tĩnh cũng có mặt trong LISP 1.5 (thông qua thiết bị Funarg Steve Russell, làm với John McCarthy phát triển). Trình thông dịch Lisp gốc (1960) và ngôn ngữ Lisp đầu tiên dùng tầm vực động, nhưng các ngôn ngữ có tầm vực động sau này thường đưa vào cả tầm vực tĩnh; Common Lisp có cả tầm vực tĩnh và động trong khi Scheme chỉ dùng tầm vực tĩnh. Perl là một ngôn ngữ tầm vực động khác, sau này bổ sung tầm vực tĩnh vào. Các ngôn ngữ như Pascal và C luôn luôn dùng tầm vực tĩnh, vì chúng đều bị ảnh hưởng từ ý tưởng của ALGOL 60 (mặc dù C không có hàm lồng nhau một cách từ vựng).

Ví dụ

Ví dụ sau thể hiện nhiều tầm vực khác nhau được định nghĩa trong ngôn ngữ C#:
namespace N{    // tầm vực namespace, chỉ để nhóm các danh hiệu
 class C
 {    // tầm vực class, định nghĩa/khai báo biến và hàm thành viên
 void f (bool b)
 {   // tầm vực của khối (hàm) ngoài cùng, chứa các câu lệnh thực thi
  if (b)
  {   // tầm vực của khối bên trong của câu lệnh điều kiện
     // (Chú ý, cả hai tầm vực khối đều không có tên.)...
  }
 }
 }}

Tầm vực từ vựng và tầm vực động

Một trong các mục tiêu cho việc tạo ra các tầm vực là để tách bạch cho các biến nằm trong những phần khác nhau của chương trình. Vì số lượng tên biến ngắn không nhiều, và các lập trình viên lại có cùng sở thích đặt tên biến (như i để đánh chỉ số mảng), nên trong một chương trình có độ dài vừa phải, có thể sẽ có nhiều biến cùng tên được dùng trong các tầm vực khác nhau. Tầm vực từ vựng cũng đóng vai trò bảo vệ các biến mà người ngoài không cần để ý tới. Câu hỏi về việc làm thế nào để biết từng lần xuất hiện của biến tương ứng với khu vực gắn kết (binding) nào thường sẽ được trả lời theo hai cách: tầm vực từ vực và tầm vực động.

Tầm vực từ vựng

Với tầm vực từ vựng, một tên gọi bao giờ cũng tham chiếu đến môi trường từ vựng cục bộ (ít hay nhiều) của nó. Đây là thuộc tính của nội dung mã chương trình và ngôn ngữ sẽ hiện thực sao cho nó độc lập với chồng gọi (call stack) xuất hiện khi chạy chương trình. Vì chỉ cần phân tích nội dung chương trình tĩnh là có thể biết được sự kết hợp này, nên loại tầm vực này còn được gọi là tầm vực tĩnh. Tầm vực từ vựng là cách hiện thực tầm vực chuẩn của các ngôn ngữ nền ALGOL như Pascal, Ada và C, cũng như các ngôn ngữ hàm hiện đại như ML và Haskell, vì nó cho phép lập trình viên suy luận về các giá trị, thông số, và tham khảo đối tượng (như biến, hằng, hàm, v.v.) chỉ bằng cách thay thế tên đơn giản. Nó giúp dễ thực hiện các dòng mã chia theo mô-đun rồi suy luận ý nghĩa, vì cấu trúc đặt tên cục bộ có thể hiểu tách bạch với nhau. Ngược lại, tầm vực động khiến cho lập trình viên phải dự tính mọi tình huống động có thể xảy ra để xem đoạn mã của mô-đun này có được gọi hay không.
Ví dụ, xem đoạn chương trình sau trong Pascal:
program A;var I:integer;
 K:char;

 procedure B;
 var K:real;
  L:integer;

  procedure C;
  var M:real;
  begin
  (*tam vuc A+B+C*)
  end;
 begin
  (*tam vuc A+B*)
 end;begin
 (*tam vuc A*)end.
Biến I có thể được nhìn thấy từ mọi điểm, vì nó không bị che bởi bất kỳ biến trùng tên nào khác. Biến K kiểu char chỉ được nhìn thấy trong chương trình chính vì nó bị che bởi biến K kiểu real chỉ được nhìn thấy trong các thủ tục B và C. Biến L cũng chỉ được nhìn thấy trong thủ tục B và C nhưng nó không che biến nào khác. Biến M chỉ nhìn thấy được trong thủ tục C và do đó không thể truy cập nó từ thủ tục B hoặc chương trình chính. Tương tự như vậy, chỉ có nhìn thấy thủ tục C trong thủ tục B và do đó không thể được gọi từ chương trình chính.
Hơn nữa, vẫn có thể định nghĩa một thủ tục C khác nằm bên ngoài thủ tục B. Vị trí gọi C trong chương trình sẽ quyết định xem thủ tục C nào được gọi, do đó tầm vực thủ tục cũng giống như tầm vực của biến.
Việc hiện thực chính xác tầm vực tĩnh trong các ngôn ngữ có hàm lồng nhau hạng nhất sẽ hơi khó khăn, vì nó đòi hỏi mỗi giá trị của hàm phải mang theo một bảng ghi tất cả các giá trị của biến mà nó phụ thuộc (cặp đôi hàm và môi trường như vậy đi kèm với nhau được gọi là một closure). Tùy thuộc vào cách hiện thực và kiến trúc máy tính, việc tra cứu biến có thể khá bất tiện khi sử dụng các hàm lồng nhau về từ vựng quá sâu. Tuy nhiên, đối với các hàm lồng nhau mà không tham khảo đến giá trị biến bên trong bao đóng của nó, mà chỉ truyền tham số và ngay lập tức trở thành biến cục bộ, vị trí tương đối của mỗi giá trị có thể biết được vào thời điểm dịch. Vì vậy không cần phải sử dụng nhìn trước khi dùng loại hàm lồng nhau kiểu như vậy. Thông thường, nó cũng tương tự cho các chương trình cụ thể không sử dụng hàm lồng nhau, và, dĩ nhiên, cho các chương trình được viết bằng ngôn ngữ không cho phép hàm lồng nhau (như ngôn ngữ C chẳng hạn).

Tầm vực động

Với tầm vực động, mỗi một danh hiệu đều có một chồng toàn cục ghi lại sự gắn kết giá trị. Khai báo một biến cục bộ có tên x sẽ đẩy một gắn kết vào chồng x toàn cục (có thể đang trống), rồi sẽ được lấy ra khi luồng điều khiển rời khởi tầm vực. Tính toán x trong tình huống nào cũng cho ra giá trị gắn kết nằm trên cùng. Nói một cách khác, một danh hiệu toàn cục sẽ tham chiếu đến danh hiệu đi kèm với môi trường gần nhất. Lưu ý rằng điều này không thể thực hiện được vào thời điểm dịch vì chồng gắn kết giá trị chỉ tồn tại vào Thời điểm chạy, đó là lý do tại sao loại tầm vực này được gọi là tầm vực động.
Nói chung, một vài khối (block) được định nghĩa sẽ tạo ra gắn kết giá trị có thời gian sống là khi thực thi khối đó; việc này chính là đưa vài tính năng của tầm vực tĩnh vào quy trình tầm vực động. Tuy nhiên, vì đoạn mã đó có được gọi từ nhiều vị trí và tình huống khác nhau, sẽ rất khó để xác định từ ban đầu gắn kết nào sẽ được áp dụng khi sử dụng một biến (hay là xem xem nó có tồn tại không). Điều này có thể có ích; ứng dụng nguyên lý tri thức tối thiểu nói rằng mã nguồn tránh phụ thuộc vào lý do của một giá trị biến, mà chỉ đơn giản là dùng giá nào tương ứng với định nghĩa biến. Cách diễn giải hẹp dữ liệu dùng chung này có thể tạo ra một hệ thống uyển chuyển để chuyển đổi hành vi của một hàm vào trạng thái hiện tại của hệ thống. Tuy nhiên, cái lợi này tùy thuộc vào sự ghi chép cần thận tất cả các biến được dùng theo cách này cũng như tránh giả định về một hành vi của biến, và không cung cấp cơ chết để phát hiện can thiệp giữa những phần khác nhau của chương trình. Tầm vực động cũng tránh mọi điểm lợi của trong suốt tham chiếu. Do đó, tầm vực động có thể nguy hiểm và chỉ có một ít ngôn ngữ lập trình hiện đại sử dụng nó. Một số ngôn ngữ, như Perl và Common Lisp, cho phép lập trình viên lựa chọn giữa tầm vực động và tĩnh khi định nghĩa hoặc định nghĩa lại một biến. Logo và Emacs lisp là các ví dụ về ngôn ngữ lập tình sử dụng tầm vực động.
Tầm vực động khá dễ hiện thực. Để tìm một giá trị của danh hiệu, chương trình có thể duyệt qua chồng thời điểm chạy, kiểm tra mỗi bảng ghi hoạt động (mỗi khung chồng của hàm) để tìm giá trị của danh hiệu. Trên thực tế, việc này còn được hiện thực dễ hơn nữa bằng cách sử dụng danh sách kết hợp, là một chồng ghi lại các cặp tên/giá trị. Những cặp này được đẩy vào stack này bất cứ khi nào khai báo, rồi được đẩy ra khi biến ra khỏi tầm vực. Một chiến thuật khác còn nhanh hơn là tận dụng bảng tham chiếu trung tâm, gắn mỗi tên gọi với ngữ nghĩa hiện thời của nó. Làm như vậy sẽ tránh phải tìm kiếm tuyến tính khi chạy để tìm một tên cụ thể, nhưng khiến cho bảng này phức tạp hơn. Chú ý rằng cả hai chiến thuật này để giả thiết về một thứ tự gắn kết biến vào-sau-ra-trước (LIFO); trên thực tế mọi gắn biến giá trị đều theo tình tự như vậy.

Ví dụ

Ví dụ này sẽ so sánh kết quả tạo ra khi dùng tầm vực tĩnh và tầm vực động. Xem đoạn mã sau, viết bằng ngôn ngữ tựa C:
int x = 0;int f() { return x; }int g() { int x = 1; return f(); }
Với tầm vực tĩnh, gọi g sẽ trả về 0 vì chương trình xác định vào lúc dịch rằng biểu thức x bất cứ khi nào thực thi f đều gắn kết với x toàn cục mà không bị ảnh hưởng bởi việc khởi tạo biến cục bộ có cùng tên trong g.
Với tầm vực động, chồng gắn kết đối với danh hiệu x sẽ chứa hai mục riêng biệt khi f được gọi từ g: gắn kết toàn cục với 0, và gắn kết với 1 được gán trong g (vẫn hiện hữu trên chồng vì luồng điều khiển chưa rời khỏi g). Vì khi đánh giá biểu thức danh hiệu theo định nghĩa sẽ luôn là gắn kết trên cùng, nên kết quả sẽ là 1.
Trong ngôn ngữ Perl, các biến có thể định nghĩa là tầm vực động hoặc tầm vực tĩnh. Từ khóa "my" trong Perl định nghĩa một biến cục bộ có tầm vực tĩnh, còn từ khóa "local" định nghĩa một biến cục bộ có tầm vực động. Cách làm này cho phép ta làm rõ ví dụ hơn cho từng mô hình tầm vực.
$x = 0;sub f { return $x; }sub g { my $x = 1; return f(); }print g()."\n";
Ví dụ trên dùng "my" cho biến cục bộ $x có tầm vực tĩnh của g. Như ở trên, gọi g sẽ trả về 0 vì f không nhìn thấy được biến $x của g, do đó nó tìm $x toàn cục.
$x = 0;sub f { return $x; }sub g { local $x = 1; return f(); }print g()."\n";
Trong đoạn mã này, "local" được sử dụng để làm cho $x của g là tầm vực động. Bây giờ, gọi g sẽ cho ra 1 vì f nhìn thấy biến cục bộ của g bằng cách nhìn xuống chồng thực thi.
Nói cách khác, biến có tầm vực động $x được quyết định trong môi trường thực thi, chứ không phải môi trường định nghĩa.