Глава друга
Визначення класів, створення об'єктів та швидкий огляд їхньої роботи…
Досі ми використовували “стандартні” об'єкти Ruby, такі як цифри і рядки. Тепер розглянемо створення власних нових типів об'єктів. Як і в більшості об’єктно орієнтованих мов програмування, в Ruby об'єкт визначається класом. Клас подібний до креслення, з якого все будується. Це приклад дуже простого класу:
class MyClass
end
А тут я створюю об'єкт нашого простого класу:
object = MyClass.new
Я небагато можу зробити з нашим об'єктом хоча б тому, що нічого не запрограмував в класі MyClass, з якого він був створений.
Насправді, якщо ви створите порожній клас як от
MyClass
, об'єкти створені від нього — не будуть зовсім безкорисними. Усі класи в Ruby автоматично успадковують функціональність від класуObject
. Тож, наш об'єктob
може використовувати методи класуObject
, наприклад такі, як.class
(виводить назву класу, якому належить наш клас):
puts ob.class #=> виведе: “MyClass”
Щоб зробити MyClass
трохи кориснішим, потрібно додати в нього один або декілька методів. У цьому прикладі, що був коротко згаданий у попередній главі, я додав метод, який називається saysomething
:
class MyClass
def saysomething
puts("Привіт")
end
end
Тепер, якщо я створю об'єкт класу MyClass
, я зможу викликати метод saysomething
цього об'єкта і вивести слово "Привіт"
:
ob = MyClass.new
ob.saysomething
Екземпляри і змінні екземплярів класів
Створимо кілька корисних об'єктів. Дім не повинен бути без собаки, тож давайте створимо клас Dog
:
class Dog
def set_name(aName)
@myname = aName
end
end
Зверніть увагу, що визначення класу починається з ключового слова class
(в нижньому регістрі). Після нього — назва класу, що повинна починатися з великої літери. Мій клас Dog
містить один метод set_name
. Він приймає вхідний аргумент aName
. В тілі методу set_name
значення aName
присвоюється змінній екземпляра @myname
.
Змінні, що починаються з символу
@
є змінними екземпляра (instance variables). Це означає, що вони належать індивідуальним об'єктам класу або “екземплярам” класу. Оголошувати змінні заздалегідь не обов’язково.
Я можу створити екземпляр класу Dog
, тобто “об'єкт собака”, викликавши метод new
. Тут я створюю два об'єкти класу Dog
тобто “дві собаки” (пам'ятайте, що імена класів починаються з великої літери, а імена об'єктів складаються з літер нижнього регістру):
mydog = Dog.new
yourdog = Dog.new
Зараз ці дві собаки не мають імен. Тож наступне, що я повинен зробити, щоб задати їм імена — це викликати метод set_name
:
mydog.set_name('Фідо')
yourdog.set_name('Бензо')
Задавши імена собакам, мені потрібно мати спосіб дізнати їх пізніше. У кожної собаки повинно бути власне ім'я, тому давайте створимо метод get_name
:
def get_name
return @myname
end
Слово return
тут необов'язковим. В Ruby методи завжди повертають значення останнього обчисленого виразу. Але для більшої ясності (і для уникнення несподіваних результатів від методів складніших ніж цей) нам слід виробити звичку явно вказувати return
, коли ми хочемо повернути якесь значення. Давайте задамо поведінку нашій собаці, наприклад, навчимо її відгукуватися. Нижче визначений завершений клас Dog
:
class Dog
def set_name(aName)
@myname = aName
end
def get_name
return @myname
end
def talk
return 'woof!'
end
end
Тепер ми можемо створити собаку, задати їй ім'я, дізнатися її ім'я і попросити відгукнутися:
mydog = Dog.new
mydog.set_name('Фідо')
puts mydog.get_name
puts mydog.talk
Для різноманітності та практики — можете створити файл dogs_and_cats.rb
і задати в ньому поведінку не тільки собаці, а й коту, створивши відповідно новий клас Cat
. Клас Cat
відрізняється від класу Dog
лише тим, що метод talk
має повертати 'miaow'
замість 'woof'
.
Ця програма містить помилку. Об’єкт
someotherdog
ніколи не матиме значення, яке присвоюється у його змінну@name
. На щастя, Ruby не підведе нас, якщо ми спробуємо вивести ім’я цього собаки. Замість цього, вона виведеnil
. Ми скоро поглянемо на простий спосіб, який допоможе уникнути подібних помилок у майбутньому.
Конструктор - new і initialize
Зараз розглянемо ще один приклад класу. Відкрийте файл treasure.rb
. Ми створимо пригодницьку гру з двома класами: Thing
(Річ) і Treasure
(Скарб). Клас Thing
дуже схожий на клас Dog
з нашої попередньої програми (крім того, що він не вміє гавкати).
Однак, у класу Treasure
є кілька цікавих особливостей. По-перше, в нього немає таких методів як get_name
і set_name
. Замість цього, у ньому визначений метод initiaize
, який приймає два аргументи, а їх значення присвоюються змінним екземпляра @name
і @description
:
def initialize(aName, aDescription)
@name = aName
@description = aDescription
end
Якщо клас містить метод initialize
, то він буде автоматично викликаний при створенні нового об'єкта, методом new
. Використовувати метод initialize
для задання значень змінним екземпляра — хороша ідея. Вона має дві явні переваги у порівнянні з використанням методів накшталт set_name
для встановлення значення кожній зі змінних екземпляру. По–перше, складний клас може містити велику кількість змінних екземпляру і ви можете встановити всім значення лише одним методом initialize
, а не багатьма set
–методами. По-друге, якщо змінним автоматично задається значення в момент створення об'єкта — у вас не буде змінної екземпляру, якій ви забули встановити значення.
Зауважте: Не рекомендується реалізовувати свою власну версію методу
new
.
І нарешті, я створив метод to_s
щоб повернути об'єкт класу Treasure
у вигляді рядка. Назва to_s
вибрана не просто так, така сама назва використовується і в стандартній ієрархії класів Ruby. Насправді, to_s
метод визначається для самого класу Object, який є кінцевим предком всіх інших класів в Ruby. Перевизначаючи метод to_s
, я додав нову поведінку, яка більше підходить класу Treasure
, ніж метод за замовчуванням.
Інспектування об'єктів
До речі, зверніть увагу також, що я “заглянув в середину” Treasure
об'єкта t1
, використовуючи метод inspect
:
t1.inspect
Метод inspect
визначений для всіх об'єктів Ruby. Він повертає рядок, що містить зручний для читання опис об'єкта. В даному випадку, він показує щось на зразок цього:
#<Treasure:0x28962f8 @description="ельфійська зброя, викована золотом", @name="Меч">
Перше в цьому рядку це клас, якому належить об'єкт класу Treasure
. За ним йде число, яке у вас може відрізнятися від написаного вище - це внутрішній ідентифікатор в Ruby для даного конкретного об'єкта. Останніми показані змінні нашого екземпляра та їх значення.
p.rb
:
class Treasure
def initialize(aName, aDescription)
@name = aName
@description = aDescription
end
def to_s # перезаписує метод to_s за замовчуванням
"Скарб #{@name} — це #{@description}\n"
end
end
a = "привіт"
b = 123
c = Treasure.new("кільце", "блискуча золота річ")
p(a)
p(b)
p(c)
Ruby надає метод p
для перевірки і відображення об'єктів.
p(anobject)
# Показуємо різні об'єкти у вигляді рядка використовуючи метод `to_s`
class Treasure
def initialize(aName, aDescription)
@name = aName
@description = aDescription
end
# Тут ми не перевизначатимемо метод `to_s`
# тому об'єкти цього класу використовуватимуть стандартний метод `to_s`
end
t = Treasure.new("Меч", "чудова ельфійська зброя")
print("Class.to_s: ")
puts(Class.to_s)
print("Object.to_s: ")
puts(Object.to_s)
print("String.to_s: ")
puts(String.to_s)
print("100.to_s: ")
puts(100.to_s)
print("Treasure.to_s: ")
puts(Treasure.to_s)
print("t.to_s: ")
puts(t.to_s)
print("t.inspect: ")
puts(t.inspect)
Щоб побачити, як метод to_s
може використовуватися різними об'єктами і перевірити як екземпляр класу Treasure
буде перетворений в рядок, якщо не перевизначати метод to_s
, випробуйте програму to_s.rb
.
Ви побачите, що, під час виклику методу to_s
на таких класах, як Class
, Object
, String
і Treasure
, метод просто поверне їхні імена. А якщо ми його викличемо на об'єкті, як наприклад t
екземпляр класу Treasure
то він поверне той же ідентифікатор, що повертає метод inspect
.
class Thing
def set_name(aName)
@name = aName
end
def get_name
return @name
end
end
class Treasure
def initialize(aName, aDescription)
@name = aName
@description = aDescription
end
def to_s # перезаписує метод to_s за замовчуванням
"Скарб #{@name} — це #{@description}\n"
end
end
thing1 = Thing.new
thing1.set_name("Прекрасна річ")
puts thing1.get_name
t1 = Treasure.new("Меч", "ельфійська зброя, викована золотом")
t2 = Treasure.new("Кільце", "магічне кільце великої влади")
puts t1.to_s
puts t2.to_s
# Метод inspect дозволяє вам заглянути всередину об’єкта
puts "Інспектуємо 1й скарб: #{t1.inspect}"
Дивлячись на цей код, можна побачити, що місцями він повторюється. Зрештою, чому маючи клас Thing
, який містить змінну @name
і клас Treasure
, який також містить цю саму змінну, кожна з них ініціалізуються окремо один від одного? Це здається логічним, якщо розглядати клас Treasure (Скарб) як тип Thing (Річ). Якби я хотів розвинути цю програму до справжньої пригодницької гри, інші об'єкти, такі як “кімнати” і “зброя” були б іншим типом класу Think
. Думаю, пора почати роботу над правильною побудовою ієрархії класів. Спробуємо це зробити у наступній главі.