Создание функции в ruby - PullRequest
       42

Создание функции в ruby

3 голосов
/ 20 апреля 2010

У меня возникла проблема с созданием функции для моего приложения Rails.

Я хочу, чтобы это работало так:

str = "string here"
if str.within_range?(3..30)
puts "It's withing the range!"
end

Для этого я добавил это в свой помощник приложения:

def within_range?(range)
  if self.is_a?(String)
    (range).include?(self.size)
  elsif self.is_a?(Integer)
    (range).include?(self)
  end
end

Я получаю эту ошибку:

undefined method `within_range?' for "":String

Кто-нибудь знает, в чем проблема?

Ответы [ 5 ]

5 голосов
/ 20 апреля 2010

То, что вы пытаетесь, это не просто создание новой функции. Вы пытаетесь добавить новый метод к String.

Для этого вы можете поместить свою функцию within_range? в новый модуль и затем включить этот модуль в нужные вам классы.

Обычно это делается путем вызова метода include, но, поскольку вы не можете вызвать String.include или изменить исходный код класса String или Integer, вы должны обмануть и использовать метод send .

String.send(:include, YourModule)
Integer.send(:include, YourModule)

Таким образом классы String и Integer получают все методы, определенные в YourModule.

Для получения более подробной информации, попробуйте эту статью: Почему миксины Ruby дают Rails преимущество по сравнению с фреймворками Java

4 голосов
/ 20 апреля 2010

Здравствуйте, у меня возникла проблема с созданием функции для моего приложения на Rails.

О-о, вот тут-то и возникло наше первое недоразумение: Ruby не является функциональным языком программирования. Это объектно-ориентированный язык программирования. В Ruby не существует такой функции как функция. В Ruby есть только методы , которые живут в классах .

str = 'string here'
puts "It's within the range!" if str.within_range?(3..30)

Здесь мы видим, что вы, очевидно, намереваетесь использовать свой метод для строковых объектов. Чтобы ваш метод был доступен для строковых объектов, его необходимо определить в классе String (или любом из его предков).

Для этого я добавил это в мой помощник приложения:

def within_range?(range)
  if is_a?(String)
    range.include?(size)
  elsif is_a?(Integer)
    range.include?(self) 
  end
end

Здесь вы добавляете метод within_range? к любому объекту верхнего уровня, а не к классу String. Добавление метода к любому объекту верхнего уровня также делает его «магически» доступным как закрытый метод для класса Object, который является суперклассом (почти) всех классов. Следовательно, within_range? теперь доступен для всех объектов, но он является закрытым, поэтому вы можете вызывать его только с неявным получателем.

Вот почему я не понимаю, почему вы получаете эту ошибку:

Но я получаю эту ошибку:

undefined method `within_range?' for "":String

Здесь две вещи не так: во-первых, объект, о котором Ruby сообщает об ошибке, должен быть "string here":String, а не "":String. Это означает, что вы на самом деле вызывали метод within_range? для пустой строки и не для строки 'string here', как показывает пример кода. Это означает, что где-то еще в базе кода, до примера кода, который вы разместили здесь, within_range? уже вызывается в пустой строке.

Вторая проблема заключается в том, что вы получаете сообщение об ошибке неправильно . Ruby сообщает вам, что Ruby не удалось найти метод within_range? для строк, т. Е. Отсутствует метод within_range? в классе String или в любом из его суперклассов, включая Object. Но там должно быть! Вы (хотя и случайно) определили приватный метод для Object. Вот сообщение об ошибке, которое вы должны получить :

NoMethodError: private method `within_range?' called for "string here":String

Это означает, что код, который определяет метод, фактически никогда не выполнялся. Я не достаточно знаком с Ruby on Rails, чтобы понять, почему это так.

Но теперь перейдем к содержанию самого кода. Как я уже говорил выше, вам нужно добавить этот код в класс String. И на самом деле, внутри вашего метода, вы имеете дело как со строками, так и с целыми числами, поэтому вам нужно добавить код в общий суперкласс String и Integer, и есть только один, и это Object

class Object
  def within_range?(range)
    if    is_a?(String)  then range.include?(size)
    elsif is_a?(Integer) then range.include?(self) end
  end
end

[Я позволил себе удалить пару лишних скобок, self с и новые строки.]

Есть несколько проблем с этим. Первое: is_a? в Ruby - это запах кода. Это почти всегда означает, что вы либо делаете что-то не так, либо, по крайней мере, что-то «не-Rubyish», например, пытаетесь повторно реализовать статическую типизацию. (Не поймите меня неправильно: мне нравится статическая типизация, но если мне это нужно, я использую более подходящий язык, чем Ruby.)

Как правило, если вы используете is_a? или его кузенов kind_of? и Module#===, вам следует сделать шаг назад и пересмотреть свой дизайн.

Другая проблема с вашим кодом состоит в том, что у вас есть условие, которое проверяет self. Это еще проще увидеть, когда мы реорганизуем код, чтобы использовать выражение case вместо выражения if:

class Object
  def within_range?(range)
    case self
    when String  then range.include?(size)
    when Integer then range.include?(self) end
  end
end

Здесь легко увидеть, что объект смотрит на себя. Это никогда не нужно делать! Он должен всегда уже знать все, что нужно знать о себе. Это как если бы вам пришлось смотреть в зеркало, чтобы узнать, что на вас надето: вам не нужно этого делать, ведь вы надеваете одежду на себя! (И если вам нужно это сделать, это, вероятно, результат какого-то плохого решения прошлой ночью, как в программировании ...)

Третий запах - это то, что вы делаете разные вещи, в зависимости от типа объекта. Именно для этого и нужен полиморфизм: если вы скажете foo.bar, Ruby назовет другую версию bar, в зависимости от типа foo. Вам не нужно заново реализовывать это самостоятельно, оно уже есть.

Итак, давайте вернемся назад: что на самом деле делает метод? Ну, если он вызывается для строки, то он делает одну вещь, а если он вызывается для целого числа, он делает другую вещь. Итак, мы ленивы и позволяем Ruby выяснить, какой из них запустить:

class String
  def within_range?(range)
    range.include?(size)
  end
end

class Integer
  def within_range?(range)
    range.include?(self)
  end
end

Это уже намного проще и понятнее. И есть еще одно преимущество: теперь, если вы вызовете within_range? для объекта, о котором он не знает, он на самом деле скажет вам , что он не знает, что делать, подняв NoMethodError:

[].within_range?(3..30)
# => NoMethodError: undefined method `within_range?' for []:Array

В старой версии отсутствовало предложение else, поэтому она просто вернула бы nil, что в логическом контексте означает false, что означает, что ошибка, вероятно, осталась бы незамеченной.

Мы можем сделать еще больше: на самом деле здесь нет ничего, что ограничивало бы метод от работы только с целыми числами, он мог бы так же легко работать с числами с плавающей запятой, рациональными или даже комплексными числами. Таким образом, вместо определения второго метода на Integer, мы могли бы определить его на Numeric.

Но если вы подумаете об этом: на самом деле даже нет ничего, что требует, чтобы это было число вообще , на самом деле это может быть любой объект , поэтому мы могли бы даже определить его на Object.

Это подводит меня к двум последним проблемам, которые у меня возникают с этим методом, которые связаны друг с другом: я думаю, что название версии String вводит в заблуждение, и мне не нравится, что метод делает совершенно разные вещи несмотря на то, что его имя одно и то же.

Для целого числа (или любого объекта, если мы определяем метод на Object), метод проверяет, находится ли целое число (или объект) само в пределах спектр. Но для строки метод не проверяет, находится ли сама строка в пределах диапазона, а проверяет, находится ли длина строки в пределах диапазон. Это совершенно неочевидно и удивительно для меня:

  2.within_range?(1..3)     # => true
'B'.within_range?('A'..'C') # => false

Я, и я подозреваю, что почти все, кто читает этот фрагмент кода, ожидают, что и из них будут true. Тот факт, что B не находится между A и C, полностью сбивает с толку.

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

Намного лучше было бы имя length_within_range?, и вы бы назвали его так:

'B'.length_within_range?(1..2) # => true
'B'.within_range?('A'..'C')    # => true

[Предполагая, что within_range? определено в Object.]

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

Тем не менее, сейчас действительно нет большой разницы между

str.length_within_range?(3..30)
str.length.within_range?(3..30)

Это означает, что мы можем полностью избавиться от строковой версии, оставив нас только с Object версией.

Версия Object, в свою очередь, на самом деле ничего не делает, кроме переворачивания аргумента и получателя, так что мы могли бы написать

(3..30).include?(str.length)

и избавиться от всего этого в целом.

4 голосов
/ 20 апреля 2010

Функция как-то не добавляет себя в класс объекта. Я предполагаю, что вы хотите добавить его к Object, поскольку у вас есть is_a звонок. Вы должны снова открыть его:

class Object
  def within_range?(range)
    if is_a?(String)
      (range).include?(size)
    elsif is_a?(Integer)
      (range).include?(self)
    end
  end
end
0 голосов
/ 20 апреля 2010

Я согласен, что использование модуля - лучший ответ. В качестве альтернативы, а не OO, напишите свой метод так:

def within_range?(obj, range)
  case obj
  when String  then range.include?(obj.size)
  when Integer then range.include?(obj)
  end
end

и назовите это как:

str = "string here"
puts "'#{str}' is within the range!" if within_range?(str, 3..30)
0 голосов
/ 20 апреля 2010

Как упоминалось, он не может найти метод с именем 'inside_range' для класса String. Вам необходимо добавить метод в класс String.

class String
 def within_range?(range)
  if self.is_a?(String)
    (range).include?(self.size)
  elsif self.is_a?(Integer)
    (range).include?(self)
  end
 end
end
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...