Ruby: Определено ли определение метода внутри другого метода? - PullRequest
13 голосов
/ 04 ноября 2010

Я читал статью о метапрограммировании, и она показала, что вы можете определить метод в другом методе.Это то, что я знал некоторое время, но это заставило меня задать себе вопрос: имеет ли это какое-либо практическое применение?Существуют ли в реальной жизни способы определения метода внутри метода?

Пример:

def outer_method
  def inner_method
     # ...
  end
  # ...
 end

Ответы [ 5 ]

11 голосов
/ 04 ноября 2010

Мой любимый пример метапрограммирования - динамическое создание метода, который вы затем будете использовать в цикле. Например, у меня есть механизм запросов, который я написал на Ruby, и одна из его операций - фильтрация. Существует множество различных форм фильтров (подстрока, равно, <=,> =, пересечения и т. Д.). Наивный подход таков:

def process_filter(working_set,filter_type,filter_value)
  working_set.select do |item|
    case filter_spec
      when "substring"
        item.include?(filter_value)
      when "equals"
        item == filter_value
      when "<="
        item <= filter_value
      ...
    end
  end
end

Но если ваши рабочие наборы могут стать большими, вы будете делать это большое выражение case 1000 или 1000000s раз для каждой операции, даже если на каждой итерации будет выполняться одно и то же ветвление. В моем случае логика гораздо сложнее, чем просто инструкция case, поэтому накладные расходы еще хуже. Вместо этого вы можете сделать это так:

def process_filter(working_set,filter_type,filter_value)
  case filter_spec
    when "substring"
      def do_filter(item,filter_value)
        item.include?(filter_value)
      end
    when "equals"
      def do_filter(item,filter_value)
        item == filter_value
      end
    when "<="
      def do_filter(item,filter_value)
        item <= filter_value
      end
    ...
  end
  working_set.select {|item| do_filter(item,filter_value)}
end

Теперь одноразовое разветвление выполняется один раз, и полученная одноцелевая функция используется во внутреннем цикле.

Фактически, мой реальный пример имеет три уровня этого, поскольку существуют различия в интерпретации как рабочего набора, так и значения фильтра, а не только в форме реального теста. Поэтому я создаю функцию подготовки элемента и функцию подготовки значения фильтра, а затем создаю функцию do_filter, которая использует их.

(И я на самом деле использую лямбды, а не определения.)

5 голосов
/ 13 июля 2012

Я думаю, что есть еще одно преимущество использования внутренних методов, которое заключается в ясности.Подумайте об этом: класс со списком методов - это плоский, неструктурированный список методов.Если вы заботитесь о разделении интересов и сохранении содержимого на одном уровне абстракции, а фрагмент кода используется только в одном месте, внутренние методы приходят на помощь, хотя и намекают на то, что они используются только во вложенном методе.

Предположим, у вас есть этот метод в классе:

class Scoring
  # other code
  def score(dice)
    same, rest = split_dice(dice)

    set_score = if same.empty?
      0
    else 
      die = same.keys.first
      case die
      when 1
        1000
      else
        100 * die
      end
    end
    set_score + rest.map { |die, count| count * single_die_score(die) }.sum
  end

  # other code
end

Теперь, это своего рода простое преобразование структуры данных и более высокоуровневый код, добавление множества кубиков, образующих набор, и тех, которые не принадлежатзадавать.Но не очень понятно, что происходит.Давайте сделаем это более наглядным.Далее следует простой рефакторинг:

class Scoring
  # other methods...
  def score(dice)
    same, rest = split_dice(dice)

    set_score = same.empty? ? 0 : get_set_score(same)
    set_score + get_rest_score(rest)
  end

  def get_set_score(dice)
    die = dice.keys.first
    case die
    when 1
      1000
    else
      100 * die
    end
  end

  def get_rest_score(dice)
    dice.map { |die, count| count * single_die_score(die) }.sum
  end

  # other code...
end

Идея get_set_score () и get_rest_score () состоит в том, чтобы документировать с помощью описательного (хотя и не очень хорошего в этом придуманном примере) действия этих частей.Но если у вас есть много таких методов, код в Score () не так легко следовать, и если вы реорганизуете один из методов, вам может понадобиться проверить, какие другие методы их используют (даже если они закрытые - другиеметоды одного и того же класса могут их использовать).

Вместо этого я начинаю отдавать предпочтение таку:

class Scoring
  # other code
  def score(dice)
    def get_set_score(dice)
      die = dice.keys.first
      case die
      when 1
        1000
      else
        100 * die
      end
    end

    def get_rest_score(dice)
      dice.map { |die, count| count * single_die_score(die) }.sum
    end

    same, rest = split_dice(dice)

    set_score = same.empty? ? 0 : get_set_score(same)
    set_score + get_rest_score(rest)
  end

  # other code
end

Здесь должно быть более очевидно, что get_rest_score () и get_set_score ()заключены в методы, позволяющие поддерживать логику Score () на том же уровне абстракции, не вмешиваться в хэши и т. д.

Обратите внимание, что технически вы можете вызвать Scoring # get_set_score и Scoring # get_rest_score,но в этом случае это будет плохим стилем IMO, потому что семантически они являются просто приватными методами для одиночного метода score ()

Итак, имея эту структуру, вы всегда сможете прочитать всю реализацию Score () безищем любой другой метод, определенный вне Scoring # score.Несмотря на то, что я не вижу такого кода Ruby часто, я думаю, что я собираюсь преобразовать больше в этот структурированный стиль с помощью внутренних методов.

NOTE : еще один вариант, который не выглядит«Чистый, но избегающий проблемы конфликтов имен» - это просто использовать лямбды, которые были в Ruby с самого начала.Используя пример, он превратится в

get_rest_score  = -> (dice) do
  dice.map { |die, count| count * single_die_score(die) }.sum
end
...
set_score + get_rest_score.call(rest)

Это не так красиво - кто-то, глядя на код, может задаться вопросом, зачем все эти лямбды, тогда как использование внутренних методов довольно самодокументировано.Я бы все-таки больше склонялся к лямбдам, поскольку у них нет проблемы утечки потенциально конфликтующих имен в текущую область.

5 голосов
/ 04 ноября 2010

Да, есть.На самом деле, держу пари, что вы используете хотя бы один метод, который определяет другой метод каждый день: attr_accessor.Если вы используете Rails, то при постоянном использовании их будет больше, например, belongs_to и has_many.Это также обычно полезно для конструкций в стиле АОП.

3 голосов
/ 05 января 2015

Не используется def. Практического применения для этого нет, и компилятор, вероятно, должен выдать ошибку.

Есть причины динамически определять метод в течениевыполняя другой метод.Рассмотрим attr_reader, который реализован в C, но может быть эквивалентно реализован в Ruby как:

class Module
  def attr_reader(name)
    define_method(name) do
      instance_variable_get("@#{name}")
    end
  end
end

Здесь мы используем #define_method для определения метода.#define_method - фактический метод;def нет.Это дает нам два важных свойства.Во-первых, он принимает аргумент, который позволяет нам передать ему переменную name для имени метода.Во-вторых, он занимает блок, который закрывает нашу переменную name, что позволяет нам использовать его внутри определения метода.

Так что же произойдет, если вместо этого мы будем использовать def?1020 * Это не работает вообще.Во-первых, после ключевого слова def следует буквальное имя, а не выражение.Это означает, что мы определяем метод с именем, буквально, #name, что совсем не то, что мы хотели.Во-вторых, тело метода ссылается на локальную переменную с именем name, но Ruby не распознает ее как ту же переменную, что и аргумент #attr_reader.Конструкция def не использует блок, поэтому она больше не закрывается над переменной name.

Конструкция def не позволяет вам «передавать» любую информацию впараметризовать определение метода, который вы определяете.Это делает его бесполезным в динамическом контексте.Нет смысла определять метод, используя def из метода.Вы всегда можете переместить одну и ту же внутреннюю конструкцию def из внешней def и в итоге использовать один и тот же метод.


Кроме того, динамическое определение методов имеет свою стоимость.Ruby кэширует ячейки памяти методов, что повышает производительность.Когда вы добавляете или удаляете метод из класса, Ruby должен выбросить этот кеш.(До Ruby 2.1 этот кэш был global . Начиная с 2.1, кеш - для каждого класса.)

Если вы определяете метод внутри другого метода, каждый раз, когда вызывается внешний метод, это делает недействительным кеш.Это хорошо для макросов верхнего уровня, таких как attr_reader и Rails 'belongs_to, потому что они все вызываются при запуске программы, а затем (надеюсь) никогда больше.Определение методов во время текущего выполнения вашей программы немного замедлит вас.

0 голосов
/ 17 ноября 2011

Я думал о рекурсивной ситуации, но не думаю, что в этом есть смысл.

...