2010-11-04 30 views
13

Tôi đã đọc một bài báo về lập trình meta và nó cho thấy rằng bạn có thể định nghĩa một phương thức trong một phương thức khác. Đây là điều mà tôi đã biết một lúc, nhưng nó khiến tôi tự hỏi mình câu hỏi: liệu điều này có ứng dụng thực tế nào không? Có sử dụng thực tế nào trong việc định nghĩa một phương thức trong một phương thức không?Ruby: Việc định nghĩa một phương thức bên trong phương thức khác có thực sự sử dụng không?

Ex:

def outer_method 
    def inner_method 
    # ... 
    end 
    # ... 
end 
+0

cộng đồng nhỏ các nhà phát triển ruby ​​ở đây tôi đoán. – zengr

+5

Bạn có biết rằng một khi bạn gọi 'outer_method', ai cũng có thể gọi' inner_method'? –

Trả lời

11

Ví dụ về lập trình meta ưa thích của tôi như thế này là xây dựng động một phương pháp mà bạn sẽ sử dụng trong một vòng lặp. Ví dụ, tôi có một công cụ truy vấn mà tôi đã viết trong Ruby và một trong các hoạt động của nó là lọc. Có nhiều dạng bộ lọc khác nhau (chuỗi con, bằng, < =,> =, giao lộ, v.v.). Cách tiếp cận ngây thơ là như thế này:

def process_filter(working_set,filter_type,filter_value) 
    working_set.select do |item| 
    case filter_spec 
     when "substring" 
     item.include?(filter_value) 
     when "equals" 
     item == filter_value 
     when "<=" 
     item <= filter_value 
     ... 
    end 
    end 
end 

Nhưng nếu bộ làm việc của bạn có thể nhận được lớn, bạn đang làm tuyên bố này lớn trường hợp 1000s hoặc 1000000s lần cho mỗi hoạt động mặc dù nó sẽ mất chi nhánh giống nhau trên mỗi lặp lại. Trong trường hợp của tôi, logic có liên quan nhiều hơn chỉ là một tuyên bố trường hợp, do đó, chi phí thậm chí còn tồi tệ hơn. Thay vào đó, bạn có thể làm điều đó như thế này:

def process_filter(working_set,filter_type,filter_value) 
    case filter_spec 
    when "substring" 
     def do_filter(item,filter_value) 
     item.include?(filter_value) 
     end 
    when "equals" 
     def do_filter(item,filter_value) 
     item == filter_value 
     end 
    when "<=" 
     def do_filter(item,filter_value) 
     item <= filter_value 
     end 
    ... 
    end 
    working_set.select {|item| do_filter(item,filter_value)} 
end 

Bây giờ một lần phân nhánh được thực hiện một lần, lên phía trước, và kết quả là chức năng duy nhất mục đích là được sử dụng trong các vòng trong. Trong thực tế, ví dụ thực tế của tôi thực hiện ba cấp độ này, vì có các biến thể trong việc giải thích cả bộ làm việc và giá trị bộ lọc, không chỉ là hình thức của thử nghiệm thực tế. Vì vậy, tôi xây dựng một chức năng chuẩn bị mặt hàng và chức năng lọc-giá trị chuẩn bị, và sau đó xây dựng một hàm do_filter sử dụng các hàm đó.

(Và tôi thực sự sử dụng lambdas, không defs.)

+0

Ví dụ tuyệt vời, cảm ơn rất nhiều! – agentbanks217

+6

Như đã lưu ý ở phần cuối, đây là trường hợp tuyệt vời cho * lambdas *, không phải để xác định phương thức. Xác định một phương pháp mới trên lớp cho mục đích này là quá mức cần thiết. – Chuck

+3

Tôi có thể hỏi tại sao đó là 'quá mức nghiêm trọng'? Theo sự hiểu biết của tôi, nó chỉ là một sự khác biệt trong việc biến phương thức thành một biến đúng không? – lulalala

5

Có, có. Trong thực tế, tôi sẽ đặt cược bạn sử dụng ít nhất một phương pháp xác định phương pháp khác mỗi ngày: attr_accessor. Nếu bạn sử dụng Rails, sẽ có thêm một tấn sử dụng liên tục, chẳng hạn như belongs_tohas_many. Nó cũng thường hữu ích cho các cấu trúc kiểu AOP.

+0

Các phương pháp này xác định các phương pháp khác, nhưng các phương thức của chúng không giống như ví dụ trong câu hỏi. Các phương thức này là các phương thức * class * sử dụng 'define_method' để định nghĩa các phương thức * instance *. Nếu họ sử dụng 'def', họ sẽ định nghĩa nhiều phương thức * class * hơn, điều này sẽ không hữu ích. Họ cũng không thể lấy tên của phương thức mới làm đối số, vì 'def' không phải là một phương thức lấy đối số, đó là một cấu trúc cú pháp cần một tên chữ trong mã nguồn. – Peeja

+0

(Trên thực tế, một sửa đổi nhỏ: trong một số trường hợp, các macro như thế này không sử dụng 'define_method' nhưng thay vì xây dựng một chuỗi có chứa' def' construct và 'class_eval' chuỗi. Nhưng điều đó vẫn không giống như chỉ lồng một 'def' bên trong' def'.) – Peeja

+0

(Tôi không nhận thấy rằng câu hỏi về mặt kỹ thuật không phải là về 'def' trong 'def', nhưng về việc định nghĩa các phương thức từ các phương thức khác. đúng, bạn không nên làm theo cách OP được minh họa trong ví dụ của họ.) – Peeja

0

Tôi đã nghĩ đến một tình huống đệ quy, nhưng tôi không nghĩ rằng nó sẽ làm cho đủ ý nghĩa.

5

Tôi nghĩ có một lợi ích khác khi sử dụng các phương pháp bên trong là rõ ràng. Hãy nghĩ về nó: một lớp với danh sách các phương thức là một danh sách các phương thức phẳng, không cấu trúc. Nếu bạn quan tâm đến việc tách mối quan tâm và giữ nội dung ở cùng một mức trừu tượng VÀ đoạn mã chỉ được sử dụng ở một nơi, các phương thức bên trong sẽ giúp đỡ trong khi gợi ý mạnh mẽ rằng chúng chỉ được sử dụng trong phương pháp kèm theo.

Giả sử bạn có phương pháp này trong một lớp học:

class Scoring 
    # other code 
    def score(dice) 
    same, rest = split_dice(dice) 

    set_score = if same.empty? 
     0 
    else 
     die = same.keys.first 
     case die 
     when 1 
     1000 
     else 
     100 * die 
     end 
    end 
    set_score + rest.map { |die, count| count * single_die_score(die) }.sum 
    end 

    # other code 
end 

Bây giờ, đó là loại đơn giản chuyển đổi cấu trúc dữ liệu và nhiều hơn nữa mã cấp cao hơn, thêm số điểm xúc xắc hình thành một bộ và những người mà không thuộc về bộ. Nhưng không phải là rất rõ ràng những gì đang xảy ra. Hãy làm cho nó mô tả hơn. Một refactoring đơn giản sau:

class Scoring 
    # other methods... 
    def score(dice) 
    same, rest = split_dice(dice) 

    set_score = same.empty? ? 0 : get_set_score(same) 
    set_score + get_rest_score(rest) 
    end 

    def get_set_score(dice) 
    die = dice.keys.first 
    case die 
    when 1 
     1000 
    else 
     100 * die 
    end 
    end 

    def get_rest_score(dice) 
    dice.map { |die, count| count * single_die_score(die) }.sum 
    end 

    # other code... 
end 

Idea của get_set_score() và get_rest_score() là tài liệu bằng cách sử dụng một mô tả (mặc dù không phải là rất tốt trong việc này được pha chế chẳng hạn) những gì những mảnh làm.Nhưng nếu bạn có nhiều phương thức như thế này, mã trong điểm() không dễ làm theo, và nếu bạn cấu trúc lại một trong hai phương pháp bạn có thể cần phải kiểm tra những phương pháp khác sử dụng chúng (ngay cả khi chúng là riêng tư - khác các phương thức của cùng một lớp có thể sử dụng chúng).

Thay vào đó, tôi bắt đầu thích này:

class Scoring 
    # other code 
    def score(dice) 
    def get_set_score(dice) 
     die = dice.keys.first 
     case die 
     when 1 
     1000 
     else 
     100 * die 
     end 
    end 

    def get_rest_score(dice) 
     dice.map { |die, count| count * single_die_score(die) }.sum 
    end 

    same, rest = split_dice(dice) 

    set_score = same.empty? ? 0 : get_set_score(same) 
    set_score + get_rest_score(rest) 
    end 

    # other code 
end 

Ở đây, nó phải được rõ ràng hơn rằng get_rest_score() và get_set_score() được gói vào phương pháp để giữ logic của điểm() chính nó trong mức độ trừu tượng tương tự, không có sự can thiệp với băm, vv

Lưu ý rằng về mặt kỹ thuật bạn có thể gọi chấm điểm # get_set_score và chấm điểm # get_rest_score, nhưng trong trường hợp này nó sẽ là phong cách xấu IMO, bởi vì ngữ nghĩa họ là phương pháp chỉ riêng cho điểm số phương pháp đơn lẻ()

Vì vậy, có cấu trúc này bạn luôn có thể đọc toàn bộ việc thực hiện điểm số() mà không tìm kiếm bất kỳ phương pháp nào khác được xác định bên ngoài Điểm số điểm số. Mặc dù tôi không thấy mã Ruby như vậy thường xuyên, tôi nghĩ rằng tôi sẽ chuyển đổi nhiều hơn thành phong cách có cấu trúc này với các phương pháp bên trong.

CHÚ Ý: Một tùy chọn khác không trông sạch sẽ nhưng tránh vấn đề xung đột tên chỉ đơn giản là sử dụng lambdas, đã được xung quanh trong Ruby từ get. Sử dụng ví dụ, nó sẽ biến thành

get_rest_score = -> (dice) do 
    dice.map { |die, count| count * single_die_score(die) }.sum 
end 
... 
set_score + get_rest_score.call(rest) 

Nó isn là như khá - một người nào đó nhìn vào các mã có thể tự hỏi tại sao tất cả những lambdas, trong khi sử dụng phương pháp nội là khá tự tài liệu. Tôi vẫn nghiêng về phía lambdas hơn, vì họ không có vấn đề rò rỉ những tên xung đột tiềm tàng với phạm vi hiện tại.

+0

tôi thích thực hành tương tự. định nghĩa các phương thức bên trong một phương thức để mã hóa đơn giản và rõ ràng. nó rất hữu ích khi phương pháp của bạn trở nên nặng nề (phức tạp) và bạn không muốn difine outer_method cho những người gọi. – ajahongir

+0

Đây không phải là một ý tưởng hay. Ruby không nhận thấy 'def' bên trong' # score' và định nghĩa phương thức * once *, nó định nghĩa nó mỗi lần (và chỉ khi) '# score' chạy.Điều đó có nghĩa là '# get_set_score' không tồn tại cho đến khi' # score' được gọi là * và * nó được định nghĩa lại mỗi khi '# get_set_score' được gọi. Không chỉ là điều kỳ lạ, nó cũng làm mất hiệu lực bộ nhớ cache toàn cục của Ruby, điều này sẽ làm chậm chương trình của bạn một cách đáng kể. – Peeja

+0

Hoàn toàn đồng ý với Peeja, vì vậy trừ khi ngôn ngữ hỗ trợ các phương thức bên trong thực sự, có thể không khôn ngoan khi sử dụng chúng (có lẽ Ruby sẽ phát ra cảnh báo nếu phát hiện ra, hoặc thậm chí là lỗi?) – EdvardM

3

Không sử dụng def. Không có ứng dụng thực tế cho điều đó, và trình biên dịch có thể sẽ gây ra lỗi.

Có các lý do để xác định phương thức động trong quá trình thực thi phương pháp khác. Cân nhắc attr_reader, được thực hiện trong C, nhưng có thể được thực hiện tương đương trong Ruby như:

class Module 
    def attr_reader(name) 
    define_method(name) do 
     instance_variable_get("@#{name}") 
    end 
    end 
end 

Ở đây, chúng tôi sử dụng #define_method để xác định phương pháp này. #define_method là một phương pháp thực tế; def thì không. Điều đó cho chúng ta hai đặc tính quan trọng. Đầu tiên, cần một đối số, cho phép chúng ta chuyển nó thành biến số name để đặt tên cho phương thức. Thứ hai, phải mất một khối, mà đóng trên biến của chúng tôi name cho phép chúng ta sử dụng nó từ bên trong định nghĩa phương thức.

Vì vậy, điều gì sẽ xảy ra nếu chúng tôi sử dụng def thay thế?

class Module 
    def attr_reader(name) 
    def name 
     instance_variable_get("@#{name}") 
    end 
    end 
end 

Điều này không có tác dụng gì cả. Đầu tiên, từ khóa def được theo sau bởi một tên chữ, không phải là một biểu thức. Điều đó có nghĩa là chúng tôi đang xác định một phương thức có tên, theo nghĩa đen là #name, đó không phải là những gì chúng tôi muốn. Thứ hai, phần thân của phương thức đề cập đến một biến cục bộ được gọi là name, nhưng Ruby sẽ không nhận ra nó là biến tương tự như đối số cho #attr_reader. Cấu trúc def không sử dụng một khối, vì vậy nó không đóng trên biến name nữa.

Cấu trúc def không cho phép bạn "chuyển vào" bất kỳ thông tin nào để tham số hóa định nghĩa phương pháp bạn đang xác định. Điều đó làm cho nó vô dụng trong một bối cảnh năng động. Không có lý do gì để xác định phương thức sử dụng def từ bên trong một phương thức. Bạn luôn có thể di chuyển cùng một bên trong def xây dựng ra khỏi bên ngoài def và kết thúc bằng cùng một phương pháp.


Ngoài ra, xác định phương thức tự động có chi phí. Ruby lưu trữ các vị trí bộ nhớ của các phương thức, giúp cải thiện hiệu suất. Khi bạn thêm hoặc xóa một phương thức từ một lớp, Ruby phải loại bỏ bộ đệm đó. (Trước khi Ruby 2.1, bộ nhớ đệm đó là toàn cầu. Tính đến 2,1, bộ nhớ cache là mỗi lớp.)

Nếu bạn xác định phương thức bên trong phương thức khác, mỗi lần phương thức bên ngoài được gọi, nó sẽ làm mất hiệu lực bộ nhớ cache. Điều đó tốt cho các macro cấp cao nhất như attr_reader và Rails 'belongs_to, bởi vì tất cả các macro này đều được gọi khi chương trình bắt đầu và sau đó (hy vọng) không bao giờ trở lại. Xác định các phương pháp trong quá trình thực hiện liên tục của chương trình sẽ làm chậm bạn xuống một chút.

Các vấn đề liên quan