Classes and Objects

Object-Oriented programming in Crystal is very similar to other OOP languages.

  1. Objects are associated with data and behaviour defined by instance variables and methods.
  2. 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 self prefix.

Copying Objects:

Shallow Copy: Use the dup method.

Shallow copy provides a copy that is different in memory but has the same fields as the original object.

Deep Copy: Use the clone method.

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.