Classes and Objects
Object-Oriented programming in Crystal is very similar to other OOP languages.
- Objects are associated with data and behaviour defined by instance variables and methods.
- Classes are blueprints that the objects are created from.
In Crystal, everything is an object and every object is an instance of some class.
You can get the class of an object by using .class method. See below:
p 'C'.class # => Char : Class
p "John".class # => String : Class
p false.class # => Bool : Class
p nil.class # => Nil : Class
p ["John", 42].class # => Array(String | Int32) : Class
All the primitives being objects comes with the benefit of having
useful methods attached to them. One common example would be the
size method that's defined on the String class.
Creating Classes and Objects
In Crystal, we use CamelCase to name our classes. When you create a class you are also defining a new type.
# Creating a class:
class Bird
end
# Creating an object:
b = Bird.new
p typeof(b) # => Bird
Using A Constructor
The initialize method is used as a constructor:
class Dog
def initialize(name : String, age : Int64)
@name = name
@age = age
end
end
rex = Dog.new "Rex", 2
p typeof(rex) # => Dog
You can use a shorter syntax for instance variables. By prefixing the
constructor parameters with @, we can make the compiler create instance
variables of the same name automatically:
class Dog
def initialize(@name : String, @age : Int64)
end
end
Reading & Updating Instance Variables
We can use getters and setters to enable read/write behaviour on our instance variables. Both of these are macros that save us from the boilerplate of writing manual getters and setters.
class Bird
getter name : String
getter age : Int32
setter age
def initialize(name, age)
@name = name
@age = age
end
def describe
"This bird's name is #{name} and it is #{age} years old"
end
def to_string
self.to_s
end
end
jax = Bird.new "Jax", 3
p jax.name # => Jax
p jax.age # => 3
jax.name = "John" # => We cannot mutate this value, because there's no setter defined
jax.age += 1
p jax.age # => 4
# Instance methods that are accessible from outside:
p jax.describe # => This bird's name is Jax and it is 4 years old.
p jax.to_string # => "#<Bird:0x41412f>"
There's one more shorthand for situations where we use the macros getter
and setter. Instead of using these two, we could use property. This will
make it so that the instance variable will have both a getter and a setter:
class Elephant
getter name : String
property age : Int32
def initialize(name, age)
@name = name
@age = age
end
end
e = Elephant.new "Marcus", 19
p e.name # => "Marcus"
p e.age # => 19
e.age += 1
p e.age # => 20
A Few Useful Methods
Take a look at the methods below:
class Person
end
p1 = Person.new
p2 = Person.new
p p1 # => #<Person:0x4342>
p p1.to_s # => "#<Person:0x4342>"
p p1 == p2 # => false # This compares the reference in memory
p p1.same? p2 # => false # Same as above
p p1.nil? # => false
p p1.is_a? Person # => true # p1 is an instance of Person
Behind The Scenes For new And initialize
First let's create a class, notice the shorthand syntax compared to the examples before:
class Person
getter first_name, last_name, age
def initialize(@name : String, @last_name : String, age : Int32)
end
end
john = Person.new("John", "Doe", 42)
pp john # => #<Person:0x34234 ...>
pp john.object_id # => 4312425842
When new is called on class, we allocate memory for the class and then run initialize
automatically. After initialization, the object is created and placed on the heap.
Calling the object_id method on the object returns it's memory address. This reference
will be used to pass this object around.
Generic Types:
It's possible to define a class with a generic type to allow the initializer to accept different types. For this, use the following syntax:
class Person(T)
getter name
def initialize(@name : T)
end
end
It's possible to define a class with the generic T type, and initia
Variable prefixes: @ and @@
For instance variables use the prefix
@.
For class variables use the prefix
@@.
Calling Methods On The Class Itself
Use the
selfprefix.
Copying Objects:
Shallow Copy: Use the
dupmethod.
Shallow copy provides a copy that is different in memory but has the same fields as the original object.
Deep Copy: Use the
clonemethod.
Deep copy on the other hand is for cloning.
Have a look at the following example for how copying works:
class Person
property name
property age
def initialize(@name : String, @age : Int32)
end
def clone
self
end
end
john = Person.new("John", 42)
shallow_copy = john.dup
deep_copy = john.clone
pp john # => #<Person:0x7fd1d3604ea0 @age=42, @name="John">
pp john.object_id # => 140539171196576
pp shallow_copy # => #<Person:0x7fd1d3604900 @age=42, @name="John">
pp shallow_copy.object_id # => 140539171195136
pp deep_copy # => #<Person:0x7fd1d3604ea0 @age=42, @name="John">
pp deep_copy.object_id # => 140539171196576
pp john == shallow_copy # => false
pp john.class == shallow_copy.class # => true
pp john == deep_copy # => true
pp john.class == shallow_copy.class # => true
The finalize Method
The finalize method is essentially a hook for the garbage collector, it will be run when the object is GC'ed.