In the previous section, and in Organizing Code in Classes and Modules, you saw a simple Mineral class. Here’s the class code by itself without any additional logic:
| class Mineral |
| getter name : String |
| getter hardness : Float64 |
| getter crystal_struct : String |
| |
| def initialize(@name, @hardness, @crystal_struct) # constructor |
| end |
| end |
This class has three read-only instance variables: name, hardness, and crystal_struct. Giving them a type is imposed by the Crystal compiler. But you can also do this in the initialize method:
| class Mineral |
| getter name, hardness, crystal_struct |
| |
| def initialize(@name : String, |
| @hardness : Float64, |
| @crystal_struct : String) |
| end |
| end |
Default values can be assigned like this:
| def initialize(@name : String = "unknown", ...) |
| end |
Some people use symbols like :hardness for the property name, but it isn’t required. A property without a type must have a default value. Or you could give it a value in initialize (try it!). You don’t need to define variables at the start of the class.
The new method creates a Mineral object:
| min1 = Mineral.new("gold", 1.0, "cubic") |
| min1 # => #<Mineral:0x271cf00 @crystal_struct="cubic", |
| # => @hardness=1.0, @name="gold"> |
| min1.object_id # => 41012992 == 0x271cf00 |
| typeof(min1) # => Mineral # compile-time type |
| min1.class # => Mineral # run-time type |
| Mineral.class # => Class # all classes have type Class |
new is a class method that’s created automatically for every class. It allocates memory, calls initialize, and then returns the newly created object. An object is created on the heap and it has an object_id: its memory address. When it gets a new name or when it’s passed to a method, only the reference is passed. This means the object is changed when it’s changed in the method.
When you’re not sure which types your initialize method will accept, you can also use generic types like T, as in this class Mineralg:
| class Mineralg(T) |
| getter name |
| |
| def initialize(@name : T) |
| end |
| end |
| |
| min = Mineralg.new("gold") |
| min2 = Mineralg.new(42) |
| min3 = Mineralg(String).new(42) |
| |
| # => Error: no overload matches 'Mineralg(String).new' with type Int32 |
When naming instance variables, prefix them with @. For class variables, use @@, like the @@planet our mineral species comes from. All objects built using this class will share this variable, and its value will be the same to all of them. (However, subclasses, which you’ll see in the next section, all get their own copy with the value shared across the subclass.)
To name properties that can change, such as quantity in the code that follows, prefix them with property. For write-only properties that can’t be read, use the prefix setter, like id in the following code. Trying to show them is an error:
| class Mineral |
| @@planet = "Earth" |
| |
| getter name, hardness, crystal_struct |
| setter id |
| property quantity : Float32 |
| |
| def initialize(@id : Int32, @name : String, @hardness : Float64, |
| @crystal_struct : String) |
| @quantity = 0f32 |
| end |
| |
| def self.planet |
| @@planet |
| end |
| end |
| |
| min1 = Mineral.new(101, "gold", 1.0, "cubic") |
| min1.quantity = 453.0f32 # => 453.0 |
| min1.id # => Error: undefined method 'id' for Mineral |
| Mineral.planet # => "Earth" |
| |
| min2 = min1.dup |
| min1 == min2 # => false |
You must make sure that properties are always initialized, either in the initialize method or when calling new. The names of methods called on the class itself are prefixed with self., like the planet method.
Use the dup method to create a “shallow” copy of the object: the copy min2 is a different object, but if the original contains fields that are objects themselves, these are not copied. If you need a “deep” copy, you have to define a clone method.
You can also optionally write a finalize method for a class, which is automatically invoked when an object is garbage collected:
| def finalize |
| puts "Bye bye from this #{self}!" |
| end |
But this creates a burden for the garbage collection process. You should use it only if you want to free resources taken by external libraries that the Crystal garbage collector won’t free for you. Add this code snippet to see finalization at work, but be warned: you’ll exhaust your machine’s memory by digging so much gold. So save anything you need before running it.
| loop do |
| Mineral.new(101, "gold", 1.0, "cubic") |
| end |
As in Ruby or C#, you can reopen a class, which means making additional definitions of a class: they’re all combined into a single class. This even works for built-in classes. How cool is it to define your own new methods on existing classes, such as String or Array? (Yes, this is sometimes derisively called “monkey patching,” and it’s not always a good idea.)
➤ a. Employee: Create a class Employee with a getter name and a property age. Make an Employee object and try to change its name.
➤ b. Increment: Create a class Increment with a property amount and two versions of a method increment: one that adds 1 to amount, and another that adds a value, inc_amount.
3.137.41.205