C'è un modo per accedere agli argomenti del metodo in Ruby?


102

Nuovo su Ruby e ROR e lo adoro ogni giorno, quindi ecco la mia domanda poiché non ho idea di come cercarlo su Google (e ho provato :))

abbiamo metodo

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

Quindi quello che sto cercando è un modo per ottenere tutti gli argomenti passati al metodo, senza elencarli tutti. Dato che questo è Ruby, presumo che ci sia un modo :) se fosse java li avrei semplicemente elencati :)

L'output sarebbe:

Method has failed, here are all method arguments {"Mario", "Super", 40, true, true}

1
Reha kralj importsami!
formica

1
Penso che tutte le risposte dovrebbero indicare che se "un po 'di codice" cambia i valori degli argomenti prima che il metodo di scoperta degli argomenti venga eseguito, mostrerà i nuovi valori, non i valori che sono stati passati. Quindi dovresti prenderli correttamente via per essere sicuri. Detto questo, la mia battuta preferita per questo (con merito alle risposte precedenti) è:method(__method__).parameters.map { |_, v| [v, binding.local_variable_get(v)] }
Brian Deterling

Risposte:


159

In Ruby 1.9.2 e versioni successive è possibile utilizzare il parametersmetodo su un metodo per ottenere l'elenco dei parametri per quel metodo. Ciò restituirà un elenco di coppie che indicano il nome del parametro e se è richiesto.

per esempio

Se fate

def foo(x, y)
end

poi

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

È possibile utilizzare la variabile speciale __method__per ottenere il nome del metodo corrente. Quindi all'interno di un metodo i nomi dei suoi parametri possono essere ottenuti tramite

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

È quindi possibile visualizzare il nome e il valore di ogni parametro con

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

Nota: poiché questa risposta è stata scritta originariamente, nelle versioni correnti di Ruby evalnon può più essere chiamata con un simbolo. Per risolvere questo problema, to_sè stato aggiunto un esplicito durante la creazione dell'elenco dei nomi dei parametri, ad esparameters.map { |arg| arg[1].to_s }


4
Avrò bisogno di un po 'di tempo per decifrarlo :)
Haris Krajina

3
Fammi sapere quali bit devono essere decifrati e aggiungerò qualche spiegazione :)
mikej

3
+1 questo è davvero fantastico ed elegante; sicuramente la migliore risposta.
Andrew Marshall,

5
Ho provato con Ruby 1.9.3 e devi fare # {eval arg.to_s} per farlo funzionare, altrimenti ottieni un TypeError: impossibile convertire il simbolo in una stringa
Javid Jamae

5
Nel frattempo, sono migliorato e le mie capacità e capire questo codice ora.
Haris Krajina

55

A partire da Ruby 2.1 puoi usare binding.local_variable_get per leggere il valore di qualsiasi variabile locale, inclusi i parametri del metodo (argomenti). Grazie a ciò puoi migliorare la risposta accettata da evitareil male eval.

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

foo(1, 2)  # => 1, 2

2.1 è il primo?
uchuugaka

@uchuugaka Sì, questo metodo non è disponibile in <2.1.
Jakub Jirutka

Grazie. Questo lo rende carino: logger.info metodo __ + "# {args.inspect}" metodo ( _method ) .parameters.map do | , nome | logger.info "# {name} =" + binding.local_variable_get (name) end
Martin Cleaver

Questa è la strada da percorrere.
Ardee Aram

1
Anche potenzialmente utile - trasformare gli argomenti in un hash denominato:Hash[method(__method__).parameters.map.collect { |_, name| [name, binding.local_variable_get(name)] }]
sheba

19

Un modo per gestirlo è:

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

2
Funzionando e sarà votato come accettato a meno che non ci siano risposte migliori, il mio unico problema con questo è che non voglio perdere la firma del metodo, alcuni ci saranno Inteli sense e mi dispiacerebbe perderlo.
Haris Krajina

7

Questa è una domanda interessante. Forse usando local_variables ? Ma ci deve essere un modo diverso dall'usare eval. Sto cercando nel documento del 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

Non è così male, perché è questa soluzione malvagia?
Haris Krajina

Non è male in questo caso: l'uso di eval () a volte può portare a falle di sicurezza. Penso solo che potrebbe esserci un modo migliore :) ma ammetto che Google non è nostro amico in questo caso
Raffaele

Vado con questo, lo svantaggio è che non puoi creare un helper (modulo) che si prenda cura di questo, poiché non appena lascia il metodo originale non può fare le valutazioni delle variabili locali. Grazie a tutti per le informazioni.
Haris Krajina

Questo mi dà "TypeError: impossibile convertire il simbolo in stringa" a meno che non lo cambi in eval var.to_s. Inoltre, un avvertimento a questo è che se si definiscono variabili locali prima di eseguire questo ciclo, verranno incluse oltre ai parametri del metodo.
Andrew Marshall,

6
Questo non è l'approccio più elegante e sicuro: se definisci la variabile locale all'interno del tuo metodo e poi chiami local_variables, restituirà gli argomenti del metodo + tutte le variabili locali. Ciò potrebbe causare errori quando il codice.
Aliaksei Kliuchnikau

5

Questo può essere utile ...

  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

1
Piuttosto che questa callers_nameimplementazione leggermente hacker , potresti anche passare __method__con il binding.
Tom Lord

3

Puoi definire una costante come:

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

E usalo nel tuo codice come:

args = eval(ARGS_TO_HASH)
another_method_that_takes_the_same_arguments(**args)

2

Prima di andare oltre, stai passando troppi argomenti in foo. Sembra che tutti questi argomenti siano attributi su un modello, giusto? Dovresti davvero passare l'oggetto stesso. Fine del discorso.

Potresti usare un argomento "splat". Spinge tutto in una matrice. Sarebbe come:

def foo(*bar)
  ...
  log.error "Error with arguments #{bar.joins(', ')}"
end

Non sono d'accordo su questo, la firma del metodo è importante per la leggibilità e la riutilizzabilità del codice. L'oggetto stesso va bene, ma devi creare un'istanza da qualche parte, quindi prima di chiamare il metodo o nel metodo. Metodo migliore secondo me. es. crea metodo utente.
Haris Krajina

Per citare un uomo più intelligente di me, Bob Martin, nel suo libro Clean Code, "il numero ideale di argomenti per una funzione è zero (niladico). Segue uno (monoadico), seguito da vicino da due (diadico). Tre argomenti (triadico) dovrebbe essere evitato ove possibile. Più di tre (poliadico) richiede una giustificazione molto speciale - e quindi non dovrebbe essere usato comunque. " Continua dicendo quello che ho detto, molti argomenti correlati dovrebbero essere racchiusi in una classe e passati come oggetto. È un buon libro, lo consiglio vivamente.
Tom L

Non per mettere un punto troppo fine, ma considera questo: se trovi che hai bisogno di più / meno / argomenti diversi, allora avrai rotto la tua API e dovrai aggiornare ogni chiamata a quel metodo. D'altra parte, se passi un oggetto, altre parti della tua app (o consumatori del tuo servizio) possono sbuffare allegramente.
Tom L

Sono d'accordo con i tuoi punti e, ad esempio, in Java vorrei sempre applicare il tuo approccio. Ma penso che con ROR sia diverso ed ecco perché:
Haris Krajina

Sono d'accordo con i tuoi punti e, ad esempio, in Java vorrei sempre applicare il tuo approccio. Ma penso che con ROR sia diverso ed ecco perché: se vuoi salvare ActiveRecord su DB e hai un metodo che lo salva, dovresti assemblare l'hash prima di passarlo per salvare il metodo. Ad esempio, impostiamo nome, cognome, nome utente, ecc. E poi passiamo l'hash per salvare il metodo che farebbe qualcosa e lo salverebbe. Ed ecco il problema come fa ogni sviluppatore a sapere cosa mettere nell'hash? È un record attivo, quindi dovresti andare a vedere lo schema db piuttosto che assemblare l'hash e stare molto attento a non perdere alcun simbolo.
Haris Krajina

2

Se desideri modificare la firma del metodo, puoi fare qualcosa del genere:

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

O:

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

In questo caso, interpolato argso opts.valuessarà un array, ma puoi farlo joinse su virgola. Saluti


2

Sembra che ciò che questa domanda sta cercando di ottenere possa essere fatto con una gemma che ho appena rilasciato, https://github.com/ericbeland/exception_details . Elencherà le variabili e le variabili locali (e le variabili di istanza) dalle eccezioni salvate. Potrebbe meritare un'occhiata...


1
Questo è un bel gioiello, per gli utenti Rails consiglierei anche better_errorsgem.
Haris Krajina

1

Se hai bisogno di argomenti come hash e non vuoi inquinare il corpo del metodo con un'estrazione complicata dei parametri, usa questo:

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

Basta aggiungere questa classe al tuo progetto:

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

0

Se la funzione è all'interno di una classe, puoi fare qualcosa del genere:

class Car
  def drive(speed)
  end
end

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

p method.parameters #=> [[:req, :speed]] 
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.