Czy istnieje sposób na dostęp do argumentów metod w Rubim?

104

Nowy w Ruby i ROR i uwielbiam to każdego dnia, więc oto moje pytanie, ponieważ nie mam pojęcia, jak to wygooglować (i próbowałem :))

mamy metodę

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

Więc czego szukam sposobu, aby uzyskać wszystkie argumenty przekazane do metody, bez wymieniania każdego z nich. Ponieważ to jest Ruby, zakładam, że jest sposób :) Gdyby to była java, po prostu je wymieniłbym :)

Wynik byłby:

Method has failed, here are all method arguments {"Mario", "Super", 40, true, true}
Haris Krajina
źródło
1
Reha kralj svegami!
mrówka
1
Myślę, że wszystkie odpowiedzi powinny wskazywać, że jeśli „jakiś kod” zmieni wartości argumentów przed uruchomieniem metody wykrywania argumentów, to pokaże nowe wartości, a nie wartości, które zostały przekazane. Więc należy je pobrać poprawnie dla pewności. To powiedziawszy, moim ulubionym method(__method__).parameters.map { |_, v| [v, binding.local_variable_get(v)] }
tematem w tej kwestii

Odpowiedzi:

160

W Rubim 1.9.2 i nowszych wersjach możesz użyć tej parametersmetody na metodzie, aby uzyskać listę parametrów tej metody. To zwróci listę par wskazujących nazwę parametru i czy jest on wymagany.

na przykład

Jeśli zrobisz

def foo(x, y)
end

następnie

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

Możesz użyć specjalnej zmiennej, __method__aby uzyskać nazwę bieżącej metody. Tak więc w ramach metody nazwy jej parametrów można uzyskać za pośrednictwem

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

Następnie możesz wyświetlić nazwę i wartość każdego parametru za pomocą

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

Uwaga: ponieważ ta odpowiedź została pierwotnie napisana, w obecnych wersjach Rubiego evalnie można już wywołać za pomocą symbolu. Aby rozwiązać ten problem, to_spodczas budowania listy nazw parametrów, tjparameters.map { |arg| arg[1].to_s }

mikej
źródło
4
Potrzebuję trochę czasu, żeby to rozszyfrować :)
Haris Krajina
3
Dajcie znać, które bity należy rozszyfrować, a ja dodam wyjaśnienie :)
mikej
3
+1 to jest naprawdę niesamowite i eleganckie; zdecydowanie najlepsza odpowiedź.
Andrew Marshall
5
Próbowałem z Ruby 1.9.3 i musisz zrobić # {eval arg.to_s}, aby to zadziałało, w przeciwnym razie otrzymasz TypeError: nie można przekonwertować symbolu na ciąg
Javid Jamae
5
W międzyczasie poprawiłem swoje umiejętności i teraz rozumiem ten kod.
Haris Krajina,
55

Od Ruby 2.1 możesz używać binding.local_variable_get do odczytywania wartości dowolnej zmiennej lokalnej, w tym parametrów metody (argumentów). Dzięki temu możesz poprawić zaakceptowaną odpowiedź, której chcesz uniknąćzło 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
źródło
Czy wersja 2.1 jest najwcześniejsza?
uchuugaka
@uchuugaka Tak, ta metoda nie jest dostępna w wersji <2.1.
Jakub Jirutka
Dzięki. To sprawia, że ​​jest to przyjemne: logger.info metoda __ + "# {args.inspect}" metoda ( _method ) .parameters.map do | , imię | logger.info "# {name} =" + binding.local_variable_get (name) end
Martin Cleaver
To jest odpowiednie rozwiązanie.
Ardee Aram
1
Potencjalnie przydatne - przekształcenie argumentów w nazwany skrót:Hash[method(__method__).parameters.map.collect { |_, name| [name, binding.local_variable_get(name)] }]
sheba
19

Jednym ze sposobów rozwiązania tego problemu jest:

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
źródło
2
Działa i zostanie uznany za zaakceptowany, chyba że będą lepsze odpowiedzi, moim jedynym problemem jest to, że nie chcę stracić sygnatury metody, niektóre będą miały sens Inteli i nie chciałbym go stracić.
Haris Krajina
7

To ciekawe pytanie. Może używając local_variables ? Ale musi być inny sposób niż użycie eval. Szukam w dokumencie 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
źródło
To nie jest takie złe, dlaczego to złe rozwiązanie?
Haris Krajina
W tym przypadku nie jest źle - użycie funkcji eval () może czasami prowadzić do luk w zabezpieczeniach. Tylko myślę, że może być lepszy sposób :) ale przyznaję, że Google nie jest naszym przyjacielem w tym przypadku
Raffaele
Mam zamiar iść z tym, minusem jest to, że nie można zrobić helpera (modułu), który by się tym zajął, ponieważ jak tylko opuści oryginalną metodę, nie będzie mógł wykonywać ewaluacji lokalnych zmiennych. Dzięki wszystkim za informacje.
Haris Krajina
To daje mi komunikat „TypeError: nie można przekonwertować symbolu na łańcuch”, chyba że zmienię go na eval var.to_s. Zastrzeżeniem jest również to, że jeśli zdefiniujesz jakiekolwiek zmienne lokalne przed uruchomieniem tej pętli, zostaną one uwzględnione jako dodatek do parametrów metody.
Andrew Marshall
6
Nie jest to najbardziej eleganckie i bezpieczne podejście - jeśli zdefiniujesz zmienną lokalną wewnątrz swojej metody, a następnie wywołasz local_variables, zwróci ona argumenty metody + wszystkie zmienne lokalne. Może to powodować błędy, gdy twój kod.
Aliaksei Kliuchnikau
5

To może być pomocne ...

  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
źródło
1
Zamiast tej nieco hakerskiej callers_nameimplementacji, możesz również przekazać __method__razem z binding.
Tom Lord
3

Możesz zdefiniować stałą, taką jak:

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

I użyj go w swoim kodzie, na przykład:

args = eval(ARGS_TO_HASH)
another_method_that_takes_the_same_arguments(**args)
Al Johri
źródło
2

Zanim przejdę dalej, przekazujesz zbyt wiele argumentów do foo. Wygląda na to, że wszystkie te argumenty są atrybutami modelu, prawda? Naprawdę powinieneś mijać sam obiekt. Koniec wypowiedzi.

Możesz użyć argumentu „splat”. Wsuwa wszystko do tablicy. Wyglądałoby to tak:

def foo(*bar)
  ...
  log.error "Error with arguments #{bar.joins(', ')}"
end
Tom L.
źródło
Nie zgadzam się z tym, sygnatura metody jest ważna dla czytelności i ponownego wykorzystania kodu. Sam obiekt jest w porządku, ale musisz gdzieś utworzyć instancję, więc przed wywołaniem metody lub w metodzie. Moim zdaniem lepsza metoda. np. utwórz metodę użytkownika.
Haris Krajina
Cytując mądrzejszego człowieka ode mnie, Bob Martin, w swojej książce Clean Code "idealna liczba argumentów dla funkcji wynosi zero (zero). Następnie pojawia się jeden (monoadyczny), a tuż za nim dwa (dwójka). Trzy argumenty Jeśli to możliwe, należy unikać (triadycznych). Więcej niż trzy (poliadyczne) wymagają bardzo specjalnego uzasadnienia - i nie powinny być używane ”. Następnie mówi, co powiedziałem, wiele powiązanych argumentów powinno być opakowanych w klasę i przekazanych jako obiekt. To dobra książka, bardzo ją polecam.
Tom L
Nie mówiąc o tym zbyt dobrze, ale rozważ to: jeśli okaże się, że potrzebujesz więcej / mniej / różnych argumentów, zepsujesz swoje API i będziesz musiał aktualizować każde wywołanie tej metody. Z drugiej strony, jeśli przekażesz obiekt, inne części Twojej aplikacji (lub konsumenci Twojej usługi) mogą się bawić.
Tom L
Zgadzam się z twoimi punktami i np. W Javie zawsze będę egzekwował twoje podejście. Ale myślę, że z ROR jest inaczej i oto dlaczego:
Haris Krajina
Zgadzam się z twoimi punktami i np. W Javie zawsze będę egzekwował twoje podejście. Ale myślę, że z ROR jest inaczej i oto dlaczego: Jeśli chcesz zapisać ActiveRecord w DB i masz metodę, która go zapisuje, musisz zebrać hash, zanim przekażę go do metody save. Na przykład użytkownik ustawiamy imię, nazwisko, nazwę użytkownika itp., A następnie przekazujemy hash, aby zapisać metodę, która coś zrobi i ją zapisze. I tu jest problem, skąd każdy programista wie, co umieścić w hashu? Jest to rekord aktywny, więc musiałbyś zobaczyć schemat db, a nie zebrać hash i bardzo uważać, aby nie przegapić żadnych symboli.
Haris Krajina,
2

Jeśli chcesz zmienić sygnaturę metody, możesz zrobić coś takiego:

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

Lub:

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

W tym przypadku interpolowana argslub opts.valuesbędzie tablicą, ale możesz, joinjeśli na przecinku. Twoje zdrowie

Simon Bagreev
źródło
2

Wygląda na to, że to, co próbuje osiągnąć to pytanie, można zrobić za pomocą klejnotu, który właśnie wydałem, https://github.com/ericbeland/exception_details . Wyświetli zmienne lokalne i wartości (i zmienne instancji) z uratowanych wyjątków. Może warto rzucić na to okiem...

ebeland
źródło
1
To niezła perełka, użytkownikom Railsów poleciłbym również better_errorsgem.
Haris Krajina,
1

Jeśli potrzebujesz argumentów jako skrótu i ​​nie chcesz zanieczyszczać treści metody trudnym wyodrębnianiem parametrów, użyj tego:

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

Po prostu dodaj tę klasę do swojego projektu:

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
banknot
źródło
1

Jeśli funkcja znajduje się wewnątrz jakiejś klasy, możesz zrobić coś takiego:

class Car
  def drive(speed)
  end
end

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

p method.parameters #=> [[:req, :speed]] 
Nikhil Wagh
źródło