Есть ли способ получить доступ к аргументам метода в Ruby?

104

Новичок в Ruby и ROR, и мне он нравится каждый день, поэтому вот мой вопрос, так как я не знаю, как его использовать в Google (и я пробовал :))

у нас есть метод

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

Итак, я ищу способ передать все аргументы методу, не перечисляя каждый из них. Поскольку это Ruby, я предполагаю, что есть способ :) если бы это была java, я бы просто перечислил их :)

Результат будет:

Method has failed, here are all method arguments {"Mario", "Super", 40, true, true}
Харис Краина
источник
1
Reha kralj svegami!
ant
1
Я думаю, что все ответы должны указывать на то, что если «какой-то код» изменяет значения аргументов до запуска метода обнаружения аргументов, он покажет новые значения, а не значения, которые были переданы. Поэтому вы должны захватить их правильно прочь, чтобы убедиться. Тем не менее, мой любимый method(__method__).parameters.map { |_, v| [v, binding.local_variable_get(v)] }
однострочный

Ответы:

160

В Ruby 1.9.2 и более поздних версиях вы можете использовать parametersметод для метода, чтобы получить список параметров этого метода. Это вернет список пар с указанием имени параметра и того, требуется ли он.

например

Если вы это сделаете

def foo(x, y)
end

затем

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

Вы можете использовать специальную переменную, __method__чтобы получить имя текущего метода. Итак, внутри метода имена его параметров можно получить через

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

Затем вы можете отобразить имя и значение каждого параметра с помощью

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

Примечание: поскольку этот ответ был изначально написан, в текущих версиях Ruby evalбольше нельзя вызывать с помощью символа. Для решения этой проблемы to_sбыло добавлено явное при построении списка имен параметров, т.е.parameters.map { |arg| arg[1].to_s }

микей
источник
4
Мне понадобится время, чтобы это расшифровать :)
Харис Краина
3
Дайте мне знать, какие биты нужно расшифровать, и я добавлю некоторые пояснения :)
mikej
3
+1 это действительно круто и элегантно; определенно лучший ответ.
Эндрю Маршалл,
5
Я пробовал с Ruby 1.9.3, и вам нужно сделать # {eval arg.to_s}, чтобы заставить его работать, иначе вы получите ошибку TypeError: невозможно преобразовать символ в строку
Джавид Джамае
5
Между тем, улучшились и мои навыки и теперь разбираемся в этом коде.
Харис Краина
55

Начиная с Ruby 2.1, вы можете использовать binding.local_variable_get для чтения значения любой локальной переменной, включая параметры метода (аргументы). Благодаря этому вы можете улучшить принятый ответ, чтобы избежатьзлой 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 самая ранняя?
uchuugaka
@uchuugaka Да, этот метод недоступен в <2.1.
Якуб Джирутка,
Спасибо. Это делает это приятным: метод logger.info __ + "# {args.inspect}" метод ( _method ) .parameters.map do | , имя | logger.info "# {name} =" + binding.local_variable_get (name) end
Мартин Кливер
Это путь.
Ardee Aram
1
Также потенциально полезная - преобразования аргументов в имени хеш:Hash[method(__method__).parameters.map.collect { |_, name| [name, binding.local_variable_get(name)] }]
Вирсавия
19

Один из способов справиться с этим:

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
Работает и будет признан принятым, если не будет лучших ответов, моя единственная проблема с этим заключается в том, что я не хочу терять подпись метода, у некоторых там будет чувство Inteli, и я бы не хотел его потерять.
Харис Краина,
7

Это интересный вопрос. Может быть, используя local_variables ? Но должен быть другой способ, кроме использования eval. Я ищу в документе ядра

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
Рафаэле
источник
Это не так уж и плохо, почему это злое решение?
Харис Краина
В этом случае это неплохо - использование eval () иногда может привести к дырам в безопасности. Просто я думаю, что может быть лучший способ :), но я признаю, что Google не наш друг в этом случае
Раффаэле
Я собираюсь пойти с этим, недостатком является то, что вы не можете создать помощник (модуль), который позаботился бы об этом, поскольку, как только он покидает исходный метод, он не может вычислять локальные вары. Спасибо всем за информацию.
Харис Краина,
Это дает мне «TypeError: невозможно преобразовать символ в строку», если я не изменю его на eval var.to_s. Кроме того, следует предупредить о том, что если вы определите какие-либо локальные переменные перед запуском этого цикла, они будут включены в дополнение к параметрам метода.
Эндрю Маршалл,
6
Это не самый элегантный и безопасный подход - если вы определите локальную переменную внутри своего метода, а затем вызовете local_variables, он вернет аргументы метода + все локальные переменные. Это может вызвать ошибки, когда ваш код.
Алексей Ключников
5

Это может быть полезно ...

  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
Вместо этого немного Hacky callers_nameреализации, вы можете также пройти __method__вместе с binding.
Том Лорд
3

Вы можете определить такую ​​константу, как:

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

И используйте это в своем коде, например:

args = eval(ARGS_TO_HASH)
another_method_that_takes_the_same_arguments(**args)
Al Johri
источник
2

Прежде чем я продолжу, вы передаете в foo слишком много аргументов. Похоже, что все эти аргументы являются атрибутами модели, верно? Вы действительно должны передать сам объект. Конец выступления.

Вы можете использовать аргумент "splat". Запихивает все в массив. Это выглядело бы так:

def foo(*bar)
  ...
  log.error "Error with arguments #{bar.joins(', ')}"
end
Том Л
источник
С этим не согласен, подпись метода важна для читабельности и повторного использования кода. Сам объект в порядке, но вам нужно где-то создать экземпляр, поэтому перед вызовом метода или в методе. На мой взгляд, лучше по методу. например, создать метод пользователя.
Харис Краина
Процитируя более умного человека, чем я, Боба Мартина, в его книге «Чистый код», «идеальное количество аргументов для функции равно нулю (нилядические). Далее идет один (моноадический), за которым следуют два (диадические). Три аргумента (триадических) следует по возможности избегать. Более трех (полиадических) требует особого обоснования - и в любом случае их не следует использовать ". Он продолжает то, что я сказал, многие связанные аргументы должны быть заключены в класс и переданы как объект. Это хорошая книга, очень рекомендую.
Tom L
Чтобы не придавать этому большого значения, но учтите следующее: если вы обнаружите, что вам нужно больше / меньше / разных аргументов, то вы сломаете свой API и вам придется обновлять каждый вызов этого метода. С другой стороны, если вы передадите объект, другие части вашего приложения (или потребители вашего сервиса) смогут весело работать.
Tom L
Я согласен с вашими пунктами и, например, в Java я всегда буду применять ваш подход. Но я думаю, что с ROR все по-другому, и вот почему:
Харис Краина,
Я согласен с вашими пунктами и, например, в Java я всегда буду применять ваш подход. Но я думаю, что с ROR все по-другому, и вот почему: если вы хотите сохранить ActiveRecord в БД и у вас есть метод, который его сохраняет, вам придется собрать хэш, прежде чем я передаю его методу сохранения. Для примера пользователя мы устанавливаем имя, фамилию, имя пользователя и т. Д., А затем передаем хэш для метода сохранения, который будет что-то делать и сохранять. И вот проблема, как каждый разработчик знает, что вставлять в хеш? Это активная запись, поэтому вам нужно будет увидеть схему db, а не собрать хэш, и будьте очень осторожны, чтобы не пропустить какие-либо символы.
Харис Краина,
2

Если вы хотите изменить подпись метода, вы можете сделать что-то вроде этого:

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

Или:

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

В этом случае интерполированный argsили opts.valuesбудет массивом, но вы можете, joinесли через запятую. Ура

Симон Багреев
источник
2

Кажется, что то, что пытается решить этот вопрос, можно сделать с помощью только что выпущенного гема https://github.com/ericbeland/exception_details . Он перечислит локальные переменные и vlaues (и переменные экземпляра) из спасенных исключений. Стоит взглянуть ...

Ebeland
источник
1
Это хороший гем, для пользователей Rails я также рекомендовал бы better_errorsgem.
Харис Краина
1

Если вам нужны аргументы в виде хэша, и вы не хотите загрязнять тело метода сложным извлечением параметров, используйте это:

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

Просто добавьте этот класс в свой проект:

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
гринбэк
источник
1

Если функция находится внутри какого-то класса, вы можете сделать что-то вроде этого:

class Car
  def drive(speed)
  end
end

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

p method.parameters #=> [[:req, :speed]] 
Нихил Ваг
источник