Khi phạm vi năng động sẽ hữu ích?


9

Với phạm vi động, một callee có thể truy cập các biến của trình gọi của nó. Mã giả C:

void foo()
{
    print(x);
}

void bar()
{
    int x = 42;
    foo();
}

Vì tôi chưa bao giờ lập trình bằng một ngôn ngữ hỗ trợ phạm vi động, tôi tự hỏi một số trường hợp sử dụng trong thế giới thực cho phạm vi động sẽ là gì.


có lẽ nó dễ thực hiện hơn khi trình thông dịch được thực hiện bằng một số ngôn ngữ cụ thể khác?
Alf P. Steinbach

2
Nó sử dụng đủ ít mà hầu hết các ngôn ngữ đã từng sử dụng nó (ví dụ: Lisp) không còn nữa. Chỉ cần một ví dụ rõ ràng, hầu hết các triển khai Lisp ban đầu đều sử dụng phạm vi động, nhưng bây giờ tất cả các biến thể chính (ví dụ: CL, Scheme) đều sử dụng phạm vi từ vựng.
Jerry Coffin

1
@JerryCoffin: Các ngoại lệ đáng chú ý bao gồm Perl và Emacs Lisp, cả hai đều sử dụng phạm vi động ban đầu, và bây giờ (Perl 5, Emacs 24) có hỗ trợ cho cả phạm vi động và từ vựng. Thật tuyệt khi có thể lựa chọn.
Jon Purdy

@JerryCoffin Nó không khớp chính xác với ví dụ được mã hóa nhưng javascript vẫn sử dụng phạm vi động rộng nếu tôi hiểu đúng câu hỏi. Vẫn đang cố gắng nghĩ về một lợi thế chung, nó cung cấp rằng không chỉ đơn thuần là bù đắp cho một ngôn ngữ ngắn.
Adrian

@JerryCoffin Vẫn còn một số phạm vi động được sử dụng tích cực trong Common Lisp, chủ yếu xoay quanh việc kiểm soát đọc và in động.
Vatine

Câu trả lời:


15

Một ứng dụng rất hữu ích của phạm vi động là để truyền các tham số theo ngữ cảnh mà không cần phải thêm các tham số mới một cách rõ ràng vào mọi chức năng trong ngăn xếp cuộc gọi

Ví dụ, Clojure hỗ trợ phạm vi động thông qua liên kết , có thể được sử dụng để tạm thời gán lại giá trị của *out*việc in. Nếu bạn liên kết lại *out*thì mọi cuộc gọi để in trong phạm vi động của liên kết sẽ in ra luồng đầu ra mới của bạn. Rất hữu ích nếu, ví dụ, bạn muốn chuyển hướng tất cả đầu ra được in sang một loại nhật ký gỡ lỗi nào đó.

Ví dụ: trong đoạn mã bên dưới, hàm do-Stuff sẽ in ra đầu ra gỡ lỗi thay vì ra ngoài tiêu chuẩn, nhưng lưu ý rằng tôi không cần thêm tham số đầu ra vào công cụ để kích hoạt điều này ....

(defn do-stuff [] 
  (do-other-stuff)
  (print "stuff done!"))

(binding [*out* my-debug-output-writer]
  (do-stuff))

Lưu ý rằng các ràng buộc của Clojure cũng là luồng cục bộ, do đó bạn không gặp vấn đề gì với việc sử dụng đồng thời khả năng này. Điều này làm cho các ràng buộc an toàn hơn đáng kể so với (ab) sử dụng các biến toàn cục cho cùng một mục đích.


2

(Tuyên bố miễn trừ trách nhiệm: Tôi chưa bao giờ lập trình bằng ngôn ngữ phạm vi động)

Phạm vi dễ thực hiện hơn nhiều và có khả năng nhanh hơn. Với phạm vi động, chỉ cần một bảng biểu tượng (các biến hiện có). Nó chỉ đọc từ bảng biểu tượng này cho tất cả mọi thứ.

Hãy tưởng tượng trong Python cùng chức năng.

def bar():
    x = 42;
    foo(42)

def foo(x):
    print x

Khi tôi gọi thanh, tôi đặt x vào bảng ký hiệu. Khi tôi gọi foo, tôi lấy bảng biểu tượng hiện được sử dụng cho thanh và đẩy nó lên ngăn xếp. Sau đó tôi gọi foo, có x được truyền cho nó (có khả năng đã được đưa vào bảng ký hiệu mới khi gọi hàm). Sau khi thoát khỏi chức năng, tôi phải hủy phạm vi mới và khôi phục lại phạm vi cũ.

Với phạm vi năng động, điều này là không cần thiết. Tôi chỉ cần biết hướng dẫn tôi cần quay lại khi hàm kết thúc vì không có gì phải làm với bảng ký hiệu.


"Có khả năng nhanh hơn" chỉ trong một thông dịch viên (ngây thơ); trình biên dịch có thể làm tốt hơn nhiều với phạm vi từ vựng hơn so với phạm vi động. Ngoài ra, "Với phạm vi động, điều này không cần thiết ...." là sai: với phạm vi động, khi phạm vi của một biến kết thúc (ví dụ: hàm trả về), bạn cần cập nhật bảng ký hiệu để khôi phục giá trị trước đó. Trên thực tế, tôi nghĩ rằng mã thường sẽ đề cập trực tiếp đến một "đối tượng ký hiệu" có trường có thể thay đổi cho giá trị hiện tại của biến, nhanh hơn nhiều so với thực hiện tra cứu bảng mỗi lần. Tuy nhiên, công việc cập nhật không chỉ biến mất.
Ryan Culpepper

Ah, tôi không biết phạm vi động vẫn sẽ khôi phục giá trị từ trước khi hàm được gọi. Tôi nghĩ rằng tất cả chúng đề cập đến cùng một giá trị.
jsternberg

2
vâng, vẫn còn làm tổ Nếu không, nó sẽ tương đương với việc biến mọi biến toàn cục và chỉ thực hiện một nhiệm vụ đơn giản cho nó.
Ryan Culpepper

2

Xử lý ngoại lệ trong hầu hết các ngôn ngữ sử dụng phạm vi động; khi một ngoại lệ xảy ra, điều khiển sẽ được chuyển trở lại trình xử lý gần nhất trên ngăn xếp kích hoạt (động).


Hãy bình luận về lý do của downvote. Cảm ơn!
Eyvind

Đây là một mất thú vị. Bạn cũng có thể nói rằng các returncâu lệnh trong hầu hết các ngôn ngữ sử dụng phạm vi động, bởi vì chúng trả lại quyền điều khiển cho người gọi trên ngăn xếp?
ruakh

1

Không chắc chắn 100% nếu đây là một trận đấu chính xác, nhưng tôi nghĩ ít nhất nó cũng đủ gần theo nghĩa chung để chỉ ra nơi nào có thể được sử dụng để phá vỡ hoặc thay đổi quy tắc phạm vi.

Ngôn ngữ Ruby xuất hiện lớp ERB templating, ví dụ trong Rails được sử dụng để tạo các tệp html. Nếu bạn sử dụng nó trông như thế này:

require 'erb'

x = 42
template = ERB.new <<-EOF
  The value of x is: <%= x %>
EOF
puts template.result(binding)

Các bindingtay truy cập vào các biến cục bộ vào lệnh gọi phương thức ERB, vì vậy nó có thể truy cập chúng và sử dụng chúng để điền vào mẫu. (Mã giữa các EOF là một chuỗi, phần nằm giữa <% =%> được đánh giá là mã Ruby bởi ERB và sẽ khai báo phạm vi của chính nó giống như một hàm)

Một ví dụ Rails thậm chí còn chứng minh điều này tốt hơn. Trong một trình điều khiển bài viết, bạn sẽ tìm thấy một cái gì đó như thế này:

def index
  @articles = Article.all

  respond_to do |format|
    format.html
    format.xml  { render :xml => @posts }
  end
end

Sau đó, tệp index.html.erb có thể sử dụng biến cục bộ @articlesnhư thế này (trong trường hợp này việc tạo đối tượng ERB và ràng buộc được xử lý bởi khung Rails, vì vậy bạn không thấy nó ở đây):

<ul>
<% @articles.each do |article| %>
  <li><%= article.name</li>
<% end %>
</ul>

Vì vậy, bằng cách sử dụng một biến liên kết, Ruby cho phép chạy một và cùng một mã mẫu trong các bối cảnh khác nhau.

Lớp ERB chỉ là một ví dụ về việc sử dụng. Ruby nói chung cho phép có được trạng thái thực thi thực tế với các ràng buộc biến và phương thức bằng cách sử dụng ràng buộc Kernel #, rất hữu ích trong bất kỳ bối cảnh nào bạn muốn đánh giá một phương thức trong ngữ cảnh khác nhau hoặc muốn giữ bối cảnh để sử dụng sau này.


1

Các trường hợp sử dụng cho phạm vi động là IHMO giống như đối với các biến toàn cục. Phạm vi động tránh một số vấn đề với các biến toàn cục cho phép cập nhật biến được kiểm soát nhiều hơn.

Một số trường hợp sử dụng mà tôi có thể nghĩ ra:

  • Ghi nhật ký : Có ý nghĩa hơn đối với nhật ký nhóm theo ngữ cảnh thời gian chạy từ bối cảnh từ vựng. Bạn có thể liên kết một cách ngẫu nhiên bộ ghi nhật ký tại một trình xử lý yêu cầu để tất cả các hàm được gọi từ đó chia sẻ cùng một "requestID" trong các mục nhật ký.
  • Chuyển hướng đầu ra
  • Phân bổ tài nguyên : Để theo dõi các tài nguyên được phân bổ cho một hành động / yêu cầu cụ thể.
  • Xử lý ngoại lệ .
  • Hành vi bối cảnh nói chung, ví dụ Emacs thay đổi ánh xạ khóa trong các bối cảnh nhất định .

Tất nhiên, phạm vi động không phải là "hoàn toàn bắt buộc" nhưng giảm bớt gánh nặng phải truyền dữ liệu của người đi theo chuỗi cuộc gọi hoặc phải thực hiện các proxy và quản lý bối cảnh toàn cầu thông minh.

Nhưng một lần nữa, phạm vi động lại có ích khi xử lý các loại yếu tố được coi là toàn cầu (ghi nhật ký, đầu ra, phân bổ / quản lý tài nguyên, v.v.).


-2

Có khá nhiều không có. Tôi chắc chắn hy vọng rằng bạn, ví dụ, không bao giờ sử dụng cùng một biến hai lần.

void foo() {
    print_int(x);
}
void bar() {
    print_string(x);
}

Bây giờ làm thế nào để bạn gọi cả foo và bar từ cùng một chức năng?

Điều này thực sự giống với việc sử dụng một biến toàn cục và nó không tốt cho tất cả các lý do tương tự.


2
x = 42; foo(); x = '42'; bar();?
Luc Danton
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.