05. Класове и обекти. Enumerable

05. Класове и обекти. Enumerable

05. Класове и обекти. Enumerable

23 октомври 2013

Днес

Ruby и ООП

Класове

прост пример

Дефинират се с class. Методите, дефинирани в тялото на класа, стават методи на инстанциите му. Инстанцират се с ИмеНаКласа.new.

class Bacon
  def chunky?
    'yes, of course!'
  end
end

bacon = Bacon.new
bacon.chunky?      # "yes, of course!"

Класове

полета

Полетата (още: instance variables) имат представка @.

class Vector
  def initialize(x, y)
    @x = x
    @y = y
  end

  def length
    (@x * @x + @y * @y) ** 0.5
  end
end

vector = Vector.new 2.0, 3.0
vector.length()     # 3.605551275463989
vector.length       # 3.605551275463989

Класове

полета (2)

По подразбиране имат стойност nil.

class Person
  def soul
    @nothingness
  end
end

person = Person.new
person.soul      # nil

Класове

викане на методи

В метод може да извикате друг със self.име_на_метод или просто име_на_метод:

class Person
  def initialize(name) @name = name                end
  def say_hi()         puts "My name is #{@name}!" end
  def sound_smart()    puts "1101000 1101001"      end

  def talk
    self.say_hi
    sound_smart
  end
end

mel = Person.new 'Mel'
mel.talk

Такова подравняване на методи е гадно, но пък се събира в слайд.

Класове

self

В методите на класа, self е референция към обекта, на който е извикан методът. Като this в Java или C++.

class Person
  def me
    self
  end
end

person = Person.new
person           # #<Person:0x41bf9514>
person.me        # #<Person:0x41bf9514>
person.me.me     # #<Person:0x41bf9514>

Атрибути

Полетата не са публично достъпни. Може да ги достигнете само чрез метод.

class Person
  def initialize(age)
    @age = age
  end

  def age
    @age
  end

  def set_age(age)
    @age = age
  end
end

person = Person.new(33)
person.age          # 33
person.set_age 20
person.age          # 20

Атрибути

setter-и

Разбира се, set_age е гадно име на метод. Може и по-добре:

class Person
  def age
    @age
  end

  def age=(value)
    @age = value
  end
end

person = Person.new
person.age = 33  # Същото като person.age=(33)

person.age       # 33

Атрибути

attr_accessor

Последното е досадно за писане. Затова:

class Person
  attr_accessor :age
end

person = Person.new
person.age = 33

person.age # 33

Атрибути

другите макроси

Ако ви трябва само getter или setter, може така:

class Person
  attr_reader :name
  attr_writer :grade
  attr_accessor :age, :height
end

Атрибути

какво е attr_accessor?

attr_accessor е метод, който генерира два метода — #foo и #foo=. Достъпен е в дефинициите на класове. Неформален термин за такива методи е "class macro".

Има ги в изобилие.

Атрибути

Meyer's Uniform Access Principle

Обърнете внимание, че следните два реда правят едно и също:

person.age()
person.age

Няма разлика между достъпване на атрибут и извикване на метод, който го изчислява. Това се нарича Uniform Access Principle и като цяло е хубаво нещо.

Конвенции

напомняне

В Ruby важат следните конвенции.

"Отваряне" на класове

Във всеки момент може да "отворите" клас и да му добавите методи. Това вече дори сте го правили.

class Person
  def name
    'River'
  end
end

class Person
  def say_hi
    "Hi, I am #{name}."
  end
end

Person.new.say_hi # "Hi, I am River."
Person.new.name   # "River"

Повторно дефиниране на метод

Ако дефинирате един метод два пъти, втората дефиниция измества първата.

class Someone
  def name
    'Tom Baker'
  end

  def name
    'Colin Baker'
  end
end

Someone.new.name # => 'Colin Baker'

Тялото на класа

където става странно

Тялото на класа е напълно изпълним код:

class Something
  a = 1
  b = 2
  a + b # 3
end

Тялото на класа (2)

Понякога дори е полезно:

class Object
  if RUBY_VERSION <= '1.8.6'
    def tap
      yield self
      self
    end
  end
end

Синоними на методи в Ruby

като говорим за код в тялото на методи...

Синтаксис за синоними на методи

Семантика на синоними на методи

Пример с alias

Въпреки името си, alias прави копие на метод.

class Array
  alias old_inject inject

  def inject(*args, &block)
    puts "I see you are using #inject. Let me help!"
    old_inject(*args, &block) * 0.01
  end
end

[1, 2, 3, 4, 5, 6].inject { |a, b| a + b } # 0.21

Синоними на методи

Пример за реална употреба:

def to_s
  to_html
end

По-добре да се запише така:

alias_method :to_s, :to_html

Или така:

alias to_s to_html

Разлики между alias и alias_method

Разлики между alias и alias_method

създаване на синоними по време на изпълнение

class Array
  [:size, :count, :length].each do |method_name|
    alias_method "old_#{method_name}", :size
  end

  def size
    0
  end
end

[1, 2, 3].size     # 0
[1, 2, 3].old_size # 3

Забележки относно синоними на методи

Въпроси до тук?

Сега е моментът да ги зададете :-)

Object#methods

Ако извикате #methods на нещо, ще получите масив от символи с имената на методите му.

Помните ли Array#-?

class Person
  def foo() end
  def bar() end
end

Person.new.methods - Object.new.methods # [:foo, :bar]

Предефиниране на оператори

Много интуитивно.

class Vector
  attr_accessor :x, :y

  def initialize(x, y)
    @x, @y = x, y
  end

  def +(other)
    Vector.new(x + other.x, y + other.y)
  end

  def inspect
    "Vector.new(#@x, #@y)"
  end
end

Vector.new(1, 5) + Vector.new(3, 10) # Vector.new(4, 15)

Предефинируеми оператори

Ето и всички оператори, които можете да предефинирате:

Забележки относно предефиниране на оператори

private

class Person
  def say_hi
    "Hello! I am #{name}"
  end

  private

  def name
    'the Doctor'
  end
end

person = Person.new
person.say_hi     # "Hello! I am the Doctor"
person.name       # error: NoMethodError

private (2)

Ако един метод е private, не може да го викате с явен получател. Дори със self.

class Person
  def say_hi
    "Hello! I am #{self.name}"
  end

  private

  def name
    'the Doctor'
  end
end

person = Person.new
person.say_hi     # error: NoMethodError

protected

Object#tap

Object#tap извиква блока със себе си и връща обекта, на който е извикан.

array = [].tap do |items|
  items              # []
  items.equal? array # false

  items << 'foo'
  'other thing'
end

array                # ["foo"]

Object#tap

за debug-ване

Имате следния код

(1..10).select { |x| x.odd? }.map { |x| x ** 2 }

Искате да видите какво остава след select-а:

(1..10).select { |x| x.odd? }.tap { |x| p x }.map { |x| x ** 2 }

Object#tap

друг пример

class Array
  def occurences_count
    Hash.new(0).tap do |result|
      each { |item| result[item] += 1 }
    end
  end
end

[nil, 1, 2, 1, :a, 'X', 1, nil].occurences_count # {nil=>2, 1=>3, 2=>1, :a=>1, "X"=>1}

Symbol#to_proc

Следните два реда са (почти) еквивалентни:

name = ->(object) { object.name }
name = :name.to_proc

Когато подавате блок на метод с &block, Ruby извиква #to_proc, ако block не е ламбда или proc.

Съответно, следните два реда са еквивалентни

%w(foo plugh larodi).map { |s| s.length } # [3, 5, 6]
%w(foo plugh larodi).map(&:length)        # [3, 5, 6]

Symbol#to_proc

с повече от един аргумент

Всъщност, малко по-сложно е:

block = ->(obj, *args) { obj.method_name *args }
block = :method_name.to_proc

Това значи, че може да направите така:

[{a: 1}, {b: 2}, {c: 3}].reduce { |a, b| a.merge b } # {:a=>1, :b=>2, :c=>3}
[{a: 1}, {b: 2}, {c: 3}].reduce(&:merge)             # {:a=>1, :b=>2, :c=>3}

Или дори:

[1, 2, 3, 4].reduce { |sum, b| sum + b } # 10
[1, 2, 3, 4].reduce(&:+)             # 10

Symbol#to_proc

Употреба

['Foo', :bar, 3].map(&:to_s).map(&:upcase)

Symbol#to_proc

Примерна имплементация

class Symbol
  def to_proc
    # ...?
  end
end

Object#send

3.send :+, 4 # 7

Symbol#to_proc

Примерна имплементация

class Symbol
  def to_proc
    ->(object, *args) { object.public_send self, *args }
  end
end

pry

$ gem install pry

Модули

Модулите в Ruby имат няколко предназначения:

Днес ще разгледаме последното.

Модули

като колекция от методи

Модулите в Ruby просто съдържат методи. Дефинират се подобно на класове:

module UselessStuff
  def almost_pi
    3.1415
  end

  def almost_e
    2.71
  end
end

Модули

миксиране

Модулите могат да се "миксират" с клас. Тогава той получава всички методи на модула като инстанционни методи.

module UselessStuff
  def almost_pi
    3.1415
  end
end

class Something
  include UselessStuff
end

Something.new.almost_pi # 3.1415

Модули

self

В метод на модула, self е инстанцията от класа, в който модулът е бил миксиран и на която е извикан даденият метод.

module Introducable
  def introduction
    "Hello, I am #{name}"
  end
end

class Person
  include Introducable
  def name() 'The Doctor' end
end

doctor = Person.new
doctor.introduction # "Hello, I am The Doctor"

Модули

приоритет на методите

Методите на класа имат приоритет пред методите на модула.

module Includeable
  def name() 'Module' end
end

class Something
  def name() 'Class' end
  include Includeable
end

Something.new.name # "Class"

Модули

приоритет на методите (2)

Ако два модула дефинират един и същи метод, ползва се методът от последно миксирания модул:

module Chunky
  def name() 'chunky' end
end

module Bacon
  def name() 'bacon' end
end

class Something
  include Chunky
  include Bacon
end

Something.new.name # "bacon"

Модули

приоритет на методите (3)

Просто за информация: методите на mixin-ите имат приоритет пред тези на родителя.

Всичко това е свързано с нещо, наречено ancestor chain, за което ще си говорим следващия път.

Enumerable

#collect, #find_all и #inject

Помните ли тези методи и техните синоними?

[1, 2, 3, 4, 5].select(&:odd?)     # [1, 3, 5]
%w(foo plugh barney).map(&:length) # [3, 5, 6]
[1, 2, 3, 4, 5].reduce(&:*)        # 120

Те са имплементирани в Enumerable, а не в Array.

Всяка колекция в Ruby ги има.

Други методи на Enumerable

all?        any?          chunk       collect          collect_concat
count       cycle         detect      drop             drop_while
each_cons   each_entry    each_slice  each_with_index  each_with_object
entries     find          find_all    find_index       first
flat_map    grep          group_by    include?         inject
lazy        map           max         max_by           member?
min         min_by        minmax      minmax_by        none?
one?        partition     reduce      reject           reverse_each
select      slice_before  sort        sort_by          take
take_while  to_a          zip 

После ще видите как генерирах тази таблица.

Hash < Enumerable

Хешовете също са Enumerable:

hash = {2 => 3, 4 => 5}

hash.to_a                                 # [[2, 3], [4, 5]]
hash.map { |p| p[0] + p[1] }              # [5, 9]
hash.map { |k, v| k + v }                 # [5, 9]
hash.reduce(0) { |s, p| s + p[0] * p[1] } # 26

Hash < Enumerable

бележка под линия

Някои от Enumerable методите в Hash са предефинирани.

hash = {2 => 3, 4 => 5, 6 => 7, 8 => 9}

hash.select { |k, v| v > 6 }      # {6=>7, 8=>9}
hash.to_a.select { |k, v| v > 6 } # [[6, 7], [8, 9]]

Enumerable#select връща списък, но Hash#select връща хеш.

Други неща за обхождане

#all? и #any?

#all?/#any? връщат истина, ако всички/един елемент(и) от колекцията отговарят на някакво условие.

[1, 2, 3, 4].all? { |x| x.even? } # false
[1, 2, 3, 4].any? { |x| x.even? } # true

[2, 4, 6, 8].all? { |x| x.even? } # true
[2, 4, 6, 8].any? { |x| x.odd? }  # false

# И разбира се:
[1, 2, 3, 4].any?(&:even?)        # true

#all? и #any? без блок

#all? и #any? могат да работят и без блок:

[1, 2, 3, nil].all?     # false
[1, 2, 3, :nil].all?    # true
[1, 2, 3, false].any?   # true

#one? и #none?

Аналогични на #all? и #any?. Също могат да работят без блок.

%w(foo bar larodi).one? { |word| word.length == 6 }  # true
%w(foo bar larodi).one? { |word| word.length == 3 }  # false

[1, 5, 3].none? { |number| number.even? }   # true
[1, 2, 3].none? { |number| number.even? }   # false

[1, 2, 3].one?     # false
[1, nil, nil].one? # true

#each_with_index

#each_with_index yield-ва всеки елемент с индекса му в масива:

%w[foo bar baz].each_with_index do |word, index|
  puts "#{index}. #{word}"
end

Горното извежда:

0. foo
1. bar
2. baz 

#group_by

Името казва всичко, което ви е нужно да знаете.

words  = %w(foo bar plugh larodi)
groups = words.group_by { |word| word.length }

groups # {3=>["foo", "bar"], 5=>["plugh"], 6=>["larodi"]}

#each_slice

#each_slice(n) yield-ва елементите на части по n:

%w(a b c d e f g h).each_slice(3) do |slice|
  p slice
end

Извежда

["a", "b", "c"]
["d", "e", "f"]
["g", "h"] 

#each_cons

#each_cons(n) yield "подмасиви" с n елемента:

[1, 2, 3, 4, 5].each_cons(3) do |cons|
  p cons
end

Извежда

[1, 2, 3]
[2, 3, 4]
[3, 4, 5] 

#include? и #member?

Вече знаете какво прави:

[1, 2, 3, 4].include? 3   # true
[1, 2, 3, 4].member? 5    # false

Двете са синоними.

#zip

[1, 2, 3].zip([4, 5, 6])    # [[1, 4], [2, 5], [3, 6]]
[1, 2].zip([3, 4], [5, 6])  # [[1, 3, 5], [2, 4, 6]]

#take, #drop, #take_while и #drop_while

[1, 2, 3, 4, 5].take(2)  # [1, 2]
[1, 2, 3, 4, 5].drop(2)  # [3, 4, 5]

[1, 3, 5, 6, 7, 9].take_while(&:odd?)  # [1, 3, 5]
[1, 3, 5, 6, 7, 9].drop_while(&:odd?) # [6, 7, 9]

Как генерирах таблицата с методите?

all?        any?          chunk       collect          collect_concat
count       cycle         detect      drop             drop_while
each_cons   each_entry    each_slice  each_with_index  each_with_object
entries     find          find_all    find_index       first
flat_map    grep          group_by    include?         inject
lazy        map           max         max_by           member?
min         min_by        minmax      minmax_by        none?
one?        partition     reduce      reject           reverse_each
select      slice_before  sort        sort_by          take
take_while  to_a          zip 

Как генерирах таблицата с методите?

кодът

Enumerable.instance_methods.
  sort.
  map { |name| name.to_s.ljust(16) }.
  each_slice(5) { |row| puts row.join '' }

Disclaimer: Леко редактирах whitespace-а, за да се събере в слайд.

include Enumerable

или как да го ползваме за наши класове

include Enumerable

пример

class FibonacciNumbers
  include Enumerable

  def initialize(limit)
    @limit = limit
  end

  def each
    current, previous = 1, 0

    while current < @limit
      yield current
      current, previous = current + previous, current
    end
  end
end

FibonacciNumbers.new(100).to_a # [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

include Enumerable

пример

class StepRange
  include Enumerable

  def initialize(first, last, step)
    @first, @last, @step = first, last, step
  end

  def each
    @first.step(@last, @step) { |n| yield n }
  end
end

StepRange.new(1, 10, 2).select { |n| n > 5 } # [7, 9]

Въпроси