Сегодня я вам расскажу о том, чего на самом деле нет - о операторах в руби. Вы можете спросить: а как же +, -, * и т.п. Так вот, их нет. И ложки тоже нет кстати.
Есть только
матрица объекты. Методы - тоже объекты, а все эти операторы - на самом деле вызов методов.
1 + 1 #=> 2
#То же самое, что и
1.+(1) #=> 2
1 * 2 #=> 2
#То же самое, что и
1.*(2) #=> 2
#и так далее
Но не все так просто, как на первый взгляд
1 + 2 * 3 + 4
#это выражение нельзя представить в таком виде
1.+(2).*(3).+(4)
#правильно будет так
1.+(2.*(3)).+(4)
Поэтому в руби есть приоритет операторов, некоторые выполняются раньше, некоторые позже. Но я специально не буду вам о них рассказывать - математические операторы исполняются точно так, как мы привыкли в школе, - деление и умножение раньше чем сложение и т.п. Остальные приоритеты - совсем не очевидны, поэтому лучше их группировать при помощи скобок
expression_one && expression_two || expression_three && expression_for
#не стоит пологаться, что кто то помнит что сильнее - && или ||. Используйте скобки!
В общем знайте, когда вы видите что то похожее на оператор - на самом деле это вызов метода и синтаксический сахар, которым так славен руби. Да они по-особому обрабатываются интерпретатором, но они остаются самыми настоящими методами, со всеми вытекающими.
Но что нам это дает? Возможность
останавливать пули определить \ переопределить операторы и получить отличный код.
Допустим нам нужно написать класс урона, ну вы знаете - "Алистер нанес скелету 20 единиц святого урона"
Уже хорошо, теперь нужно добавить атрибуты - для простоты оставим единицы и природу.
class Damage
attr_reader :points, :nature
def initialize(points, nature)
@points, @nature = points, nature
end
end
Почему я использовал attr_reader а не attr_accessor - я долго мучался, как же мне стоит писать, но в итоге решил - если предполагается установка атрибута из вне - использую attr_accessor, если нет - attr_reader и работаю с инстанс переменными (если мне нужно атрибут установить).
Теперь мы можем написать:
alister_damage = Damage.new 20, 'holly'
morrigan_damage = Damage.new 40, 'fire'
А сколько нанесли Алистер и Мориган вместе? Нужно добавить сложение!
#Дальше буду писать как будто я в теле класса.
def +(other)
Damage.new points + other.points, [nature, other.nature].flatten
end
Сложение не должно изменять текущий объект, поэтому создаем новый. Points в новом объекте будут равны сумме points в исходных объектах, nature - тут уже на выбор разработчика. На мой взгляд, если, у урона природа одна, то и nature для такого объекта должен возвращать собственно строку природы. Если же несколько - то от массива не уйдешь. Обратите внимание, как я использовал flatten, это мне позволяет не думать, сколько там природ у слагаемого. Метод в таком виде - пример
утиной типизации - если в двух словах то нам не важно, какой объект передается методу, мы предполагаем что этот объект скорее всего принадлежит классу Damage "Если кто то крякает как утка и выглядит как утка - значит это утка" (c) - отсюда название такого подхода. Плюс тут в том, что кода меньше, ну а минус - иногда получаются такие веселые баги, что век не поймаешь. Поэтому я почти всегда стараюсь хоть как то валидировать то, что ко мне пришло в метод.
def +(other)
check other, Damage
Damage.new points + other.points, [nature, other.nature].flatten
end
private
def check(value, klass)
unless value.is_a? klass
raise ArgumentError "expect #{value} to be a #{klass.name}"
end
end
Это пример защитного программирования. Идея в том, что лучше сразу сообщить о ошибке, ведь
чем раньше вы обнаружите ошибку - тем дешевле ее исправить.
party_damage = alister_damage + morrigan_damage
party_damage.points #=> 60
party_damage.nature #=> ['holly', 'fire']
Хорошо, а что если на цели дебафф, увеличивающий урон вдвое? А если бафф, уменьшающий урон?
def *(number)
check number, Numeric
Damage.new points * number, nature
end
def /(number)
check number, Numeric
Damage.new points / number, nature
end
Теперь мы можем сделать так:
party_damage = party_damage * 2
# или в сокращенной форме
party_damage *= 2
Ну так кто круче, Алистер или Морриган? Кто нанес
больше урона? Нам нужны методы сравнения >, <, <=, == и т.д. Для начала нужно решить, будет ли влиять природа на сравнение урона? Вспоминая свои годы жизни в WoW, я думаю что нет. Будем сравнивать только points. Мы можем сами определить каждый из этих методов сами, а можем пойти легким путем. Comparable - это миксин из стандартной библиотеки, включив его мы получаем сразу все методы сравнения в классе. Но мы должно определить последний на сегодня оператор <=> (Лодочка). Этот метод должен возвращать -1 когда объект меньше, чем предоставленный, 0 когда объекты равны, 1, когда больше. (Мы же пойдем совсем легким путем и просто вызовем <=> у атрибута points, ведь это число).
include Comparable
def <=>(other)
check other, Damage
points <=> other.points
end
В итоге у нас получился полный класс:
class Damage
include Comparable
attr_reader :points, :nature
def initialize(points, nature)
@points, @nature = points, nature
end
def +(other)
check other, Damage
Damage.new points + other.points, [nature, other.nature].flatten
end
def *(number)
check number, Numeric
Damage.new points * number, nature
end
def /(number)
check number, Numeric
Damage.new points / number, nature
end
def <=>(other)
check other, Damage
points <=> other.points
end
private
def check(value, klass)
unless value.is_a? klass
raise ArgumentError "expect #{value} to be a #{klass.name}"
end
end
end
И вот как мы его используем:
alister_damage = Damage.new 20, 'holly'
morrigan_damage = Damage.new 50, 'fire'
alister.kindness #=> 2 Алистер жутко добрый
alister_damage *= alister.kindness
alister_damage.points #=> 40
morrigan.malice #=> 2 та еще злюка эта Мориган
morrigan_damage /= morrigan.malice
morrigan_damage.points #=> 25
morrigan_damage >= alister_damage #=> false
alister_damage > morrigan_damage #=> true
alister_damage == morrigan_damage #=> false
party_damage = morrigan_damage + alister_damage
party_damage.points #=> 65
party_damage.nature #=> ['holly', 'fire']
В общем всем теперь ясно, что Алистер круче, а Морриган желаю быть добрее.
Так же, определив методы равенства в нашем классе, мы получаем доступ к другим фишкам, которые на этих методах основываются. На ум приходят только массивы, но уверен что есть еще.
arr = [Damage.new(100, 'acid'), Damage.new(50, 'cold'), Damage.new(120, 'fire')]
arr.max.nature #=> 'fire'
arr.min.points #=> 50
arr.sort.map { |dmg| "#{dmg.nature} - #{dmg.points}" } #=> ['cold - 50', 'acid - 100', 'fire - 120']
И для тех кто дочитал до сюда - бонус! Два самых мутных оператора в руби [] и []=, ведь аргументы для этих методов передаются по особому:
def [](arg1, arg2, arg3) #геттер, все что во время вызова окажется внутри квадратных скобок - будет аргументами для этого метода.
end
#вызвать его нужно так obj[arg1, arg2, arg3]
def []=(arg1, arg2, arg3) #сеттер, поступает с аргументами так же, как и метод сверху, за тем исключением, что последний аргумент передается после знака =
end
#вызывать его нужно так obj[args1, arg2] = arg3
#и чтобы совсем вас запутать, скажу, что если после знака = больше аргументов, чем один, они сгрузятся в массив
self[1, 2] = 3, 4, 5
#в нашем случае arg3 будет равен [3, 4, 5]
Это все, что я хотел сказать, спасибо за внимание!