Здравствуйте, у меня возникла проблема с созданием функции для моего приложения на 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)
и избавиться от всего этого в целом.