簡體   English   中英

為什么 ruby 能識別 class 外部的方法,但不能識別內部的方法?

[英]Why does ruby recognise a method outside of a class, but not inside?

我正在嘗試構建一個簡單的語言翻譯程序。 我導入了“language_converter”gem 來幫助實現這個目標。 我寫了以下代碼:

require 'language_converter'

class Translator
    def initialize
        @to = 'ja';  
        @from = 'en';   
    end

    def translate text
        lc(text, @to,@from)
    end
end

#puts lc('welcome to Japan!', 'ja','en');
 
t = Translator.new

p t.translate('welcome to Japan!');

此代碼導致錯誤: undefined method 'lc' for #<Translator:0x0000000101167a90 @to="ja", @from="en"> (NoMethodError)當我取消注釋第 15 行的代碼時,ruby 可以訪問lc方法並返回一些日語。 有誰知道為什么該方法是在 class 之外而不是在內部“定義”的?

編輯:語言轉換器gem 不是我自己的。 另外,我在它的主頁上找不到源代碼。

我還嘗試在lc方法之前添加兩個分號,如下所示: ::lc(text, @to,@from) 這導致錯誤: syntax error, unexpected local variable or method, expecting constant

寶石已有10多年歷史,只有一種方法。 該方法實現為 class 方法。

使用現代 Ruby 語法和適當的錯誤處理在您的應用程序中重寫該方法會更好。

作為參考,這就是 gem 中的lib/language_converter.rb的樣子:

require 'net/http'
require 'rubygems'
require "uri"
require 'json'

class UnSupportedLanguage < RuntimeError

  def initialize(message='')
    @msg = "not supported."
  end
end


  def self.lc( text, to, from='en' )

    begin

      uri = URI.parse("http://mymemory.translated.net/api/get")

      response = Net::HTTP.post_form(uri, {"q" => text,"langpair"=>"#{from.to_s.downcase}|#{to.to_s.downcase}", "per_page" => "50"})

      json_response_body = JSON.parse( response.body )

      if json_response_body['responseStatus'] == 200
        json_response_body['responseData']['translatedText']
      else
        puts json_response_body['responseDetails']
        raise StandardError, response['responseDetails']
      end
    rescue UnSupportedLanguage
      raise UnSupportedLanguage.new
    rescue => err_msg
      puts "#{err_msg}"
    end

  end

有誰知道為什么該方法是在 class 之外而不是在內部“定義”的?

Ruby中的方法查找算法比較簡單。 (至少在引入Module#prepend / Module#prepend_features之前是這樣,所以我們暫時忽略它)。 事情是這樣的:

  1. 檢索接收器的隱藏class指針的值。
  2. 查看方法表中是否包含該方法。
    1. 如果是,你就完成了。
    2. 如果沒有,請繼續步驟#3。
  3. 檢索超類指針的值。
    1. 如果沒有超類,go 到步驟#4。
    2. 如果有超類,請從步驟 #2 開始重復。
  4. 從消息選擇器method_missing開始算法,將原始消息的名稱及其 arguments 作為 arguments 傳遞。

就是這樣。 這總是保證終止,因為頂級 class 總是有一個名為method_missing的方法。

現在,您可能會問:但是等等,singleton 個類呢?

答案是: class的隱藏指針 class 指向其 singleton class,singleton 類的超類指針(直接或間接)指向“實際”883981121951 88.

現在,您可能會問:等等,為什么Object#class不返回 singleton class?

答案是: Object#class不直接返回隱藏的class指針。 相反,它遵循超類鏈,直到找到第一個“正常”class。

現在,您可能會問:但是等等,使用Module#include mixins 怎么樣?

答案是: Module#include (或者更確切地說Module#append_features )合成了一個新的 class (有時稱為include class ),它與模塊共享其方法表、常量表、class 變量表和實例變量表,並使 class 成為模塊混入的 class 的超類(並且它對混入該模塊的所有模塊遞歸執行此操作)。

這種將 singleton 類和包含類插入正常 class 鏈的“技巧”意味着經常發生的操作快速簡單,即方法查找 復雜性轉移到調用Object#classModule#include等操作上,但這沒關系,因為它們很少被調用,通常只在調試或應用程序啟動期間發生。

所以,現在我們知道了確定方法是否可調用所需的兩條信息:

  1. 我們需要知道在哪個方法表中,即在哪個模塊中定義了該方法。
  2. 我們需要知道接收者的超類鏈是什么。

一旦我們知道了這兩件事,我們就可以檢查定義該方法的模塊是否是接收方查找鏈的一部分。

回答#1 很容易。 我們可以使用Object#method為我們想知道的方法創建一個Method object(反射代理),然后我們可以使用Method#owner來詢問方法定義在哪里。

所以,我們知道在頂層調用方法是有效的,這意味着我們可以向頂層 object 請求方法:

m = method(:lc)

現在我們可以為它的所有者詢問方法:

m.owner
#=> #<Class:#<Object:0x0000000104c3cd00>>

呃。 好的。 那不是很有幫助。 這告訴我們以下內容:

  • 該方法在 singleton class 中定義。
  • object singleton class 所屬的是Object的直接實例。
  • 就是這樣。

所以,我們所知道的是,該方法定義在某個 object 的 singleton class 中。我們必須深入挖掘一下。

一種可能性是使用Method#source_location來查找方法的源代碼所在的位置。 但是,這不能保證有效:例如,C 中的 YARV 擴展方法或 Java 中 JRuby 或 TruffleRuby 中編寫的方法沒有“(Ruby)源代碼”,因此該方法將返回nil

在這種情況下,該方法將起作用,並將給出 Gem 內文件的路徑和定義該方法的行號。

但是,讓我們看看是否可以找到更多信息。 Ruby 中的每個 object 都有Object#inspect方法,其目的是提供人類可讀的調試信息。 好吧,我們正在調試並且我們是人類(我假設),所以讓我們看看我們能得到什么。

我們不應該抱太大希望,因為對於典型的 singleton 方法,它看起來像這樣:

foo = Object.new
def foo.bar(a, b = 23, *c, d, e:, f: 42, **g, &h) end

b = foo.method(:bar)
b.inspect
#=> '#<Method: #<Object:0x0000000109053ca0>.bar(a, b=..., *c, d, e:, f: ..., **g, &h) /path/to/source/file.rb:2>'

這基本上只是給了我們同樣的信息,我們也可以從Method#ownerMethod#source_locationMethod#parameters獲得相同的信息,但是是以一種漂亮而緊湊的方式。

但無論如何讓我們嘗試一下:

m.inspect
#=> '#<Method: main.lc(text, to, from=...) /usr/lib/ruby/gems/3.2.0/gems/language-converter-1.0.0/lib/language_converter.rb:14>'

那很有意思! 該方法實際上顯示為main.lc(text, to, from=...)而不是像#<Object:0x0000000109053ca0>.lc(text, to, from=...)東西。 那么,什么是main

main是所謂的頂層 object非正式名稱,即 object 是腳本頂層代碼中的self ,不包含在任何模塊中:

toplevel = self

toplevel.inspect
#=> 'main'

這意味着方法lcmain的 singleton class 中定義,如下所示:

def self.lc(text, to, from='en')
  # …
end

最后,這解釋了為什么你不能從頂層以外的任何地方調用它:

lc

有效,因為這里的隱式接收者selfmainmain的隱藏 class 指針指向main.singleton_class ,這就是定義lc的地方。

t.translate(…)

不起作用,因為在translate內部,在沒有接收者的情況下調用lc ,隱式接收者selft其方法查找鏈如下所示:

  1. t隱藏了 class 指針,即t.singleton_class
  2. t.singleton_class的隱藏超類指針是t.singleton_class.superclasst.classTranslator
  3. Translator的隱藏超類指針是Translator.superclass ,它是Object
  4. Object的隱藏超類指針即Object包含 class 對應Kernel
  5. Object包含 class 用於Kernel的隱藏超類指針,即Object.superclass ,即BasicObject

您可以使用 Ruby 反射粗略估計:

t.singleton_class.ancestors
#=> [
#     #<Class:#<Translator:0x00000001023c8ef0>>,
#     Translator,
#     Object,
#     Kernel,
#     BasicObject
#   ]

如您所見, main.singleton_class沒有出現在方法查找鏈中的任何位置,因此,將找不到lc

如果您真的、絕對必須使用此方法,則有一些可能的解決方法。

您可以獲取對該方法的引用並將其保留以備后用:

LC = method(:lc)

class Translator
  # …

  def translate text
    LC.(text, @to, @from)
  end
end

您可以獲取對main引用並將其保留以備后用:

MAIN = self

class Translator
  # …

  def translate text
    MAIN.lc(text, @to, @from)
  end
end

事實上, Ruby 有一個預定義的常量用於整個頂級Binding ,稱為TOPLEVEL_BINDING 您可以使用Binding#receiver方法獲取頂級 object:

class Translator
  # …

  def translate text
    TOPLEVEL_BINDING.receiver.lc(text, @to, @from)
  end
end

這是一種定義方法的奇怪方式。 這使得這種方法非常難以使用且令人討厭。

通常,像這樣的方法,旨在通過隱式接收器在頂層調用,被定義為Kernel的實例方法。 您可能認識的一些方法是Kernel#putsKernel#require

事實上,順便說一句,該方法在代碼中是縮進的,我什至不確定這是故意的。

這讓我們想到了您的另一個問題:

我在它的主頁上找不到源代碼

如上所示,你總是可以通過簡單地詢問它來找到方法源代碼的位置:

m.source_location
#=> '/usr/lib/ruby/gems/3.2.0/gems/language-converter-1.0.0/lib/language_converter.rb:14'

因此,在我的系統上, lc的源代碼位於路徑/usr/lib/ruby/gems/3.2.0/gems/language-converter-1.0.0/lib/language_converter.rb從第 14 行開始的文件中. 在你的系統上,路徑顯然會略有不同。

訪問整個 gem 源代碼的最簡單方法是使用gem open命令:

gem open language-server

這將在您的默認編輯器中打開整個 gem 目錄。

另一個有時有用的命令是gem unpack ,它將安裝的 gem 解包到當前目錄中。 如果您只想閱讀源代碼,那太過分了,因為 gem 在您安裝時已經解壓到您的 gem 目錄中。 但是如果您想進行更改,您顯然不應該在 gem 目錄本身中執行此操作。

請注意:如果您決定實際執行此操作並閱讀此 gem 的源代碼,不要從中學到任何東西,除了可能如何寫 Ruby。這太可怕了。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2025 STACKOOM.COM