Apakah ada cara untuk mengakses argumen metode di Ruby?

102

Baru mengenal Ruby dan ROR dan menyukainya setiap hari, jadi inilah pertanyaan saya karena saya tidak tahu bagaimana cara meng-google-nya (dan saya sudah mencobanya :))

kami punya metode

def foo(first_name, last_name, age, sex, is_plumber)
    # some code
    # error happens here
    logger.error "Method has failed, here are all method arguments #{SOMETHING}"    
end

Jadi apa yang saya cari cara untuk mendapatkan semua argumen diteruskan ke metode, tanpa mendaftar masing-masing. Karena ini Ruby, saya berasumsi ada cara :) jika itu adalah java saya hanya akan mencantumkannya :)

Outputnya adalah:

Method has failed, here are all method arguments {"Mario", "Super", 40, true, true}
Haris Krajina
sumber
1
Reha kralj svegami!
semut
1
Saya pikir semua jawaban harus menunjukkan bahwa jika "beberapa kode" mengubah nilai argumen sebelum metode penemuan argumen dijalankan, itu akan menunjukkan nilai baru, bukan nilai yang diteruskan. Jadi Anda harus mengambilnya dengan benar pergi untuk memastikan. Yang mengatakan, method(__method__).parameters.map { |_, v| [v, binding.local_variable_get(v)] }
kalimat

Jawaban:

159

Di Ruby 1.9.2 dan yang lebih baru, Anda dapat menggunakan parametersmetode pada suatu metode untuk mendapatkan daftar parameter untuk metode itu. Ini akan mengembalikan daftar pasangan yang menunjukkan nama parameter dan apakah itu diperlukan.

misalnya

Jika kamu melakukan

def foo(x, y)
end

kemudian

method(:foo).parameters # => [[:req, :x], [:req, :y]]

Anda dapat menggunakan variabel khusus __method__untuk mendapatkan nama metode saat ini. Jadi dalam suatu metode, nama parameternya dapat diperoleh melalui

args = method(__method__).parameters.map { |arg| arg[1].to_s }

Anda kemudian dapat menampilkan nama dan nilai setiap parameter dengan

logger.error "Method failed with " + args.map { |arg| "#{arg} = #{eval arg}" }.join(', ')

Catatan: karena jawaban ini aslinya ditulis, di versi Ruby saat evalini tidak bisa lagi disebut dengan simbol. Untuk mengatasi ini, sebuah eksplisit to_stelah ditambahkan saat membangun daftar nama parameter yaituparameters.map { |arg| arg[1].to_s }

mikej
sumber
4
Saya akan membutuhkan waktu untuk menguraikan ini :)
Haris Krajina
3
Beri tahu saya bit mana yang perlu diuraikan dan saya akan menambahkan beberapa penjelasan :)
mikej
3
+1 ini benar-benar luar biasa dan elegan; pasti jawaban terbaik.
Andrew Marshall
5
Saya mencoba dengan Ruby 1.9.3, dan Anda harus melakukan # {eval arg.to_s} untuk membuatnya berfungsi, jika tidak, Anda akan mendapatkan TypeError: tidak dapat mengubah Simbol menjadi String
Javid Jamae
5
Sementara itu, menjadi lebih baik dan keterampilan saya dan pahami kode ini sekarang.
Haris Krajina
55

Sejak Ruby 2.1 Anda dapat menggunakan binding.local_variable_get untuk membaca nilai variabel lokal apa pun, termasuk parameter metode (argumen). Berkat itu Anda dapat meningkatkan jawaban yang diterima untuk dihindarijahat eval.

def foo(x, y)
  method(__method__).parameters.map do |_, name|
    binding.local_variable_get(name)
  end
end

foo(1, 2)  # => 1, 2
Jakub Jirutka
sumber
apakah 2.1 paling awal?
uchuugaka
@uchuugaka Ya, metode ini tidak tersedia di <2.1.
Jakub Jirutka
Terima kasih. Itu membuat ini bagus: metode logger.info __ + "# {args.inspect}" method ( _method ) .parameters.map do | , nama | logger.info "# {name} =" + binding.local_variable_get (name) end
Martin Cleaver
Inilah cara untuk pergi.
Ardee Aram
1
Juga berpotensi berguna - mengubah argumen menjadi hash bernama:Hash[method(__method__).parameters.map.collect { |_, name| [name, binding.local_variable_get(name)] }]
sheba
19

Salah satu cara untuk mengatasinya adalah:

def foo(*args)
    first_name, last_name, age, sex, is_plumber = *args
    # some code
    # error happens here
    logger.error "Method has failed, here are all method arguments #{args.inspect}"    
end
Arun Kumar Arjunan
sumber
2
Bekerja dan akan dipilih sebagai diterima kecuali ada jawaban yang lebih baik, satu-satunya masalah saya dengan ini adalah saya tidak ingin kehilangan tanda tangan metode, beberapa di sana akan ada perasaan Inteli dan saya tidak akan kehilangannya.
Haris Krajina
7

Ini pertanyaan yang menarik. Mungkin menggunakan local_variables ? Tetapi harus ada cara selain menggunakan eval. Saya mencari di dokumen Kernel

class Test
  def method(first, last)
    local_variables.each do |var|
      puts eval var.to_s
    end
  end
end

Test.new().method("aaa", 1) # outputs "aaa", 1
Raffaele
sumber
Ini tidak terlalu buruk, mengapa solusi jahat ini?
Haris Krajina
Tidak buruk dalam kasus ini - menggunakan eval () terkadang dapat menyebabkan lubang keamanan. Hanya saya pikir mungkin ada cara yang lebih baik :) tapi saya akui Google bukan teman kita dalam hal ini
Raffaele
Saya akan pergi dengan ini, downside adalah Anda tidak dapat membuat helper (modul) yang akan mengurus ini, karena begitu ia meninggalkan metode aslinya, ia tidak dapat melakukan evaluasi terhadap vars lokal. Terima kasih semua atas infonya.
Haris Krajina
Ini memberi saya "TypeError: tidak dapat mengubah Simbol menjadi String" kecuali saya mengubahnya menjadi eval var.to_s. Selain itu, peringatan untuk hal ini adalah jika Anda mendefinisikan variabel lokal sebelum menjalankan loop ini, variabel tersebut akan disertakan sebagai tambahan pada parameter metode.
Andrew Marshall
6
Ini bukan pendekatan yang paling elegan dan aman - jika Anda mendefinisikan variabel lokal di dalam metode Anda dan kemudian memanggilnya local_variables, ini akan mengembalikan argumen metode + semua variabel lokal. Ini dapat menyebabkan kesalahan saat kode Anda.
Aliaksei Kliuchnikau
5

Ini mungkin membantu ...

  def foo(x, y)
    args(binding)
  end

  def args(callers_binding)
    callers_name = caller[0][/`.*'/][1..-2]
    parameters = method(callers_name).parameters
    parameters.map { |_, arg_name|
      callers_binding.local_variable_get(arg_name)
    }    
  end
Jon Jagger
sumber
1
Daripada callers_namepenerapan yang sedikit meretas ini , Anda juga bisa meneruskan __method__dengan binding.
Tom Lord
3

Anda dapat menentukan konstanta seperti:

ARGS_TO_HASH = "method(__method__).parameters.map { |arg| arg[1].to_s }.map { |arg| { arg.to_sym => eval(arg) } }.reduce Hash.new, :merge"

Dan gunakan dalam kode Anda seperti:

args = eval(ARGS_TO_HASH)
another_method_that_takes_the_same_arguments(**args)
Al Johri
sumber
2

Sebelum saya melangkah lebih jauh, Anda menyampaikan terlalu banyak argumen ke foo. Sepertinya semua argumen tersebut adalah atribut pada Model, benar? Anda harus benar-benar melewati objek itu sendiri. Akhir pidato.

Anda bisa menggunakan argumen "percikan". Ini mendorong semuanya menjadi sebuah array. Ini akan terlihat seperti:

def foo(*bar)
  ...
  log.error "Error with arguments #{bar.joins(', ')}"
end
Tom L.
sumber
Tidak setuju dengan ini, tanda tangan metode penting untuk keterbacaan dan kegunaan kembali kode. Objek itu sendiri baik-baik saja, tetapi Anda harus membuat instance di suatu tempat, jadi sebelum Anda memanggil metode atau metode. Lebih baik dalam metode menurut saya. misalnya buat metode pengguna.
Haris Krajina
Mengutip orang yang lebih pintar daripada saya, Bob Martin, dalam bukunya, Clean Code, "jumlah argumen ideal untuk suatu fungsi adalah nol (niladic). Berikutnya adalah satu (monoadik), diikuti oleh dua (diadik). Tiga argumen (triadic) harus dihindari jika memungkinkan. Lebih dari tiga (polyadic) membutuhkan pembenaran yang sangat khusus - dan kemudian tidak boleh digunakan. " Dia melanjutkan dengan mengatakan apa yang saya katakan, banyak argumen terkait harus dibungkus dalam kelas dan diteruskan sebagai objek. Ini buku yang bagus, saya sangat merekomendasikannya.
Tom L
Bukan untuk memberikan poin yang terlalu halus, tetapi pertimbangkan ini: jika Anda menemukan bahwa Anda membutuhkan lebih banyak / lebih sedikit / argumen berbeda maka Anda akan merusak API Anda dan harus memperbarui setiap panggilan ke metode itu. Di sisi lain, jika Anda meneruskan sebuah objek, maka bagian lain dari aplikasi Anda (atau konsumen layanan Anda) dapat bekerja dengan riang.
Tom L
Saya setuju dengan poin Anda dan misalnya di Jawa saya akan selalu menerapkan pendekatan Anda. Tapi saya pikir dengan ROR berbeda dan inilah alasannya:
Haris Krajina
Saya setuju dengan poin Anda dan misalnya di Jawa saya akan selalu menerapkan pendekatan Anda. Tetapi saya pikir dengan ROR berbeda dan inilah alasannya: Jika Anda ingin menyimpan ActiveRecord ke DB dan Anda memiliki metode yang menyimpannya, Anda harus mengumpulkan hash sebelum saya meneruskannya untuk menyimpan metode. Sebagai contoh pengguna kami menetapkan pertama, nama belakang, nama pengguna, dll. Dan kemudian meneruskan hash untuk menyimpan metode yang akan melakukan sesuatu dan menyimpannya. Dan inilah masalahnya bagaimana setiap pengembang tahu apa yang harus dimasukkan ke dalam hash? Ini adalah catatan aktif sehingga Anda harus melihat skema db daripada merakit hash, dan berhati-hatilah agar tidak melewatkan simbol apa pun.
Haris Krajina
2

Jika Anda ingin mengubah tanda tangan metode, Anda dapat melakukan sesuatu seperti ini:

def foo(*args)
  # some code
  # error happens here
  logger.error "Method has failed, here are all method arguments #{args}"    
end

Atau:

def foo(opts={})
  # some code
  # error happens here
  logger.error "Method has failed, here are all method arguments #{opts.values}"    
end

Dalam hal ini, diinterpolasi argsatau opts.valuesakan menjadi sebuah array, tetapi Anda bisa joinjika menggunakan koma. Bersulang

Simon Bagreev
sumber
2

Sepertinya apa yang ingin dicapai oleh pertanyaan ini dapat dilakukan dengan permata yang baru saja saya rilis, https://github.com/ericbeland/exception_details . Ini akan mencantumkan variabel dan variabel lokal (dan variabel instan) dari pengecualian yang diselamatkan. Mungkin layak untuk dilihat ...

ebeland
sumber
1
Itu permata yang bagus, untuk pengguna Rails saya juga merekomendasikan better_errorspermata.
Haris Krajina
1

Jika Anda memerlukan argumen sebagai Hash, dan Anda tidak ingin mencemari tubuh metode dengan ekstraksi parameter yang rumit, gunakan ini:

def mymethod(firstarg, kw_arg1:, kw_arg2: :default)
  args = MethodArguments.(binding) # All arguments are in `args` hash now
  ...
end

Cukup tambahkan kelas ini ke proyek Anda:

class MethodArguments
  def self.call(ext_binding)
    raise ArgumentError, "Binding expected, #{ext_binding.class.name} given" unless ext_binding.is_a?(Binding)
    method_name = ext_binding.eval("__method__")
    ext_binding.receiver.method(method_name).parameters.map do |_, name|
      [name, ext_binding.local_variable_get(name)]
    end.to_h
  end
end
greenback
sumber
0

Jika fungsinya ada di dalam beberapa kelas maka Anda dapat melakukan sesuatu seperti ini:

class Car
  def drive(speed)
  end
end

car = Car.new
method = car.method(:drive)

p method.parameters #=> [[:req, :speed]] 
Nikhil Wagh
sumber