Gibt es eine Möglichkeit, auf Methodenargumente in Ruby zuzugreifen?

102

Neu bei Ruby und ROR und ich liebe es jeden Tag. Hier ist meine Frage, da ich keine Ahnung habe, wie ich es googeln soll (und ich habe es versucht :)).

Wir haben Methode

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

Also, was ich suche, um alle Argumente an die Methode zu übergeben, ohne jedes einzelne aufzulisten. Da dies Ruby ist, nehme ich an, dass es einen Weg gibt :) Wenn es Java wäre, würde ich sie einfach auflisten :)

Ausgabe wäre:

Method has failed, here are all method arguments {"Mario", "Super", 40, true, true}
Haris Krajina
quelle
1
Reha kralj svegami!
Ameise
1
Ich denke, alle Antworten sollten darauf hinweisen, dass, wenn "irgendein Code" die Werte der Argumente ändert, bevor die Argumenterkennungsmethode ausgeführt wird, die neuen Werte angezeigt werden, nicht die Werte, die übergeben wurden. Sie sollten sie also richtig greifen weg, um sicher zu sein. Das heißt, mein Lieblings-Einzeiler dafür (mit Anerkennung der vorherigen Antworten) ist:method(__method__).parameters.map { |_, v| [v, binding.local_variable_get(v)] }
Brian Deterling

Antworten:

159

In Ruby 1.9.2 und höher können Sie die parametersMethode für eine Methode verwenden, um die Liste der Parameter für diese Methode abzurufen. Dies gibt eine Liste von Paaren zurück, die den Namen des Parameters angeben und angeben, ob er erforderlich ist.

z.B

Wenn Sie tun

def foo(x, y)
end

dann

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

Sie können die spezielle Variable verwenden __method__, um den Namen der aktuellen Methode abzurufen. Innerhalb einer Methode können also die Namen ihrer Parameter über abgerufen werden

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

Sie können dann den Namen und den Wert jedes Parameters mit anzeigen

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

Hinweis: Da diese Antwort ursprünglich geschrieben wurde, evalkann Ruby in aktuellen Versionen nicht mehr mit einem Symbol aufgerufen werden. Um dies zu beheben, to_swurde beim Erstellen der Liste der Parameternamen eine explizite hinzugefügt, d. H.parameters.map { |arg| arg[1].to_s }

mikej
quelle
4
Ich werde einige Zeit brauchen, um dies zu entziffern :)
Haris Krajina
3
Lassen Sie mich wissen, welche Bits entschlüsselt werden müssen und ich werde eine Erklärung hinzufügen :)
Mikej
3
+1 das ist wirklich großartig und elegant; definitiv die beste Antwort.
Andrew Marshall
5
Ich habe es mit Ruby 1.9.3 versucht, und Sie müssen # {eval arg.to_s} ausführen, damit es funktioniert. Andernfalls erhalten Sie einen TypeError: Symbol kann nicht in String konvertiert werden
Javid Jamae
5
In der Zwischenzeit wurden meine Fähigkeiten besser und ich verstehe diesen Code jetzt.
Haris Krajina
55

Seit Ruby 2.1 können Sie binding.local_variable_get verwenden , um den Wert jeder lokalen Variablen einschließlich der Methodenparameter (Argumente) zu lesen. Dank dessen können Sie die akzeptierte Antwort verbessern , um dies zu vermeidenböse 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
quelle
ist 2.1 das früheste?
Uchuugaka
@uchuugaka Ja, diese Methode ist in <2.1 nicht verfügbar.
Jakub Jirutka
Vielen Dank. Das macht das schön: logger.info Methode __ + "# {args.inspect}" Methode ( _method ) .parameters.map do | , Name | logger.info "# {name} =" + binding.local_variable_get (name) end
Martin Cleaver
Dies ist der richtige Weg.
Ardee Aram
1
Auch potenziell nützlich - Umwandlung der Argumente in einen benannten Hash:Hash[method(__method__).parameters.map.collect { |_, name| [name, binding.local_variable_get(name)] }]
Sheba
19

Eine Möglichkeit, damit umzugehen, ist:

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
quelle
2
Funktioniert und wird als akzeptiert gewählt, es sei denn, es gibt bessere Antworten. Mein einziges Problem dabei ist, dass ich die Methodensignatur nicht verlieren möchte. Einige dort haben Inteli-Sinn und ich würde es hassen, sie zu verlieren.
Haris Krajina
7

Dies ist eine interessante Frage. Vielleicht mit local_variables ? Aber es muss einen anderen Weg geben als die Verwendung von eval. Ich suche in Kernel doc

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
quelle
Das ist nicht so schlimm, warum ist diese böse Lösung?
Haris Krajina
In diesem Fall ist es nicht schlecht - die Verwendung von eval () kann manchmal zu Sicherheitslücken führen. Ich denke nur, dass es einen besseren Weg gibt :) aber ich gebe zu, dass Google in diesem Fall nicht unser Freund ist
Raffaele
Ich werde damit weitermachen. Der Nachteil ist, dass Sie keinen Helfer (Modul) erstellen können, der sich darum kümmert, da er, sobald er die ursprüngliche Methode verlässt, keine Auswertungen lokaler Vars durchführen kann. Danke an alle für Infos.
Haris Krajina
Dies gibt mir "TypeError: Symbol kann nicht in String konvertiert werden", es sei denn, ich ändere es in eval var.to_s. Eine Einschränkung ist auch, dass lokale Variablen, die vor dem Ausführen dieser Schleife definiert wurden, zusätzlich zu den Methodenparametern enthalten sind.
Andrew Marshall
6
Dies ist nicht der eleganteste und sicherste Ansatz. Wenn Sie eine lokale Variable in Ihrer Methode definieren und dann aufrufen local_variables, werden Methodenargumente + alle lokalen Variablen zurückgegeben. Dies kann zu Fehlern bei Ihrem Code führen.
Aliaksei Kliuchnikau
5

Dies kann hilfreich sein ...

  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
quelle
1
Anstelle dieser etwas hackigen callers_nameImplementierung könnten Sie auch __method__die weitergebenbinding .
Tom Lord
3

Sie können eine Konstante definieren wie:

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

Und verwenden Sie es in Ihrem Code wie:

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

Bevor ich weiter gehe, geben Sie zu viele Argumente an foo weiter. Es sieht so aus, als wären alle diese Argumente Attribute in einem Modell, richtig? Sie sollten das Objekt selbst wirklich übergeben. Ende der Rede.

Sie könnten ein "splat" -Argument verwenden. Es verschiebt alles in ein Array. Es würde so aussehen:

def foo(*bar)
  ...
  log.error "Error with arguments #{bar.joins(', ')}"
end
Tom L.
quelle
Nicht einverstanden, ist die Methodensignatur wichtig für die Lesbarkeit und Wiederverwendbarkeit von Code. Das Objekt selbst ist in Ordnung, aber Sie müssen irgendwo eine Instanz erstellen, bevor Sie die Methode oder in der Methode aufrufen. Meiner Meinung nach methodisch besser. zB Benutzermethode erstellen.
Haris Krajina
Um einen klügeren Mann als mich, Bob Martin, in seinem Buch Clean Code zu zitieren: "Die ideale Anzahl von Argumenten für eine Funktion ist Null (niladisch). Als nächstes kommt eins (monoadisch), dicht gefolgt von zwei (dyadisch). Drei Argumente (triadisch) sollte nach Möglichkeit vermieden werden. Mehr als drei (polyadisch) erfordern eine ganz besondere Begründung - und sollten dann sowieso nicht verwendet werden. " Er fährt fort zu sagen, was ich gesagt habe, viele verwandte Argumente sollten in eine Klasse eingeschlossen und als Objekt übergeben werden. Es ist ein gutes Buch, ich kann es nur empfehlen.
Tom L
Um es nicht allzu genau zu sagen, aber bedenken Sie Folgendes: Wenn Sie feststellen, dass Sie mehr / weniger / unterschiedliche Argumente benötigen, haben Sie Ihre API beschädigt und müssen jeden Aufruf dieser Methode aktualisieren. Wenn Sie dagegen an einem Objekt vorbeikommen, können andere Teile Ihrer App (oder Verbraucher Ihres Dienstes) fröhlich tuckern.
Tom L
Ich stimme Ihren Punkten zu und z. B. würde ich in Java Ihren Ansatz immer durchsetzen. Aber ich denke mit ROR ist anders und hier ist warum:
Haris Krajina
Ich stimme Ihren Punkten zu und z. B. würde ich in Java Ihren Ansatz immer durchsetzen. Aber ich denke, mit ROR ist das anders und hier ist der Grund: Wenn Sie ActiveRecord in DB speichern möchten und eine Methode haben, die es speichert, müssten Sie Hash zusammenstellen, bevor ich es an die Speichermethode übergebe. Zum Beispiel setzen wir Vor- und Nachnamen, Benutzernamen usw. und übergeben dann den Hash, um die Methode zu speichern, die etwas bewirken und speichern würde. Und hier ist das Problem, woher weiß jeder Entwickler, was er in Hash einfügen soll? Es handelt sich um eine aktive Aufzeichnung, sodass Sie das Datenbankschema anzeigen und dann den Hash zusammenstellen müssen. Achten Sie darauf, dass Sie keine Symbole verpassen.
Haris Krajina
2

Wenn Sie die Methodensignatur ändern würden, können Sie Folgendes tun:

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

Oder:

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

In diesem Fall interpoliert argsoder opts.valueswird ein Array, aber Sie können, joinwenn auf Komma. Prost

Simon Bagreev
quelle
2

Es scheint, als könnte das, was mit dieser Frage erreicht werden soll, mit einem Juwel geschehen, das ich gerade veröffentlicht habe: https://github.com/ericbeland/exception_details . Es werden lokale Variablen und Werte (und Instanzvariablen) aus geretteten Ausnahmen aufgelistet. Könnte einen Blick wert sein ...

ebeland
quelle
1
Das ist ein schönes Juwel, für Rails-Benutzer würde ich auch ein better_errorsJuwel empfehlen .
Haris Krajina
1

Wenn Sie Argumente als Hash benötigen und den Körper der Methode nicht mit einer schwierigen Extraktion von Parametern verschmutzen möchten, verwenden Sie Folgendes:

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

Fügen Sie diese Klasse einfach Ihrem Projekt hinzu:

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
quelle
0

Wenn sich die Funktion in einer Klasse befindet, können Sie Folgendes tun:

class Car
  def drive(speed)
  end
end

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

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