Now that you know the foundations of how Crystal’s classes and structs work, it’s time to explore some things that make it easier to use them.
A convenient class should have a method that converts its objects to a string in order to describe itself. A Java developer will know this as the toString() method. A Rubyist will know to_s, which is inherited from Object. Crystal also knows to_s.
However, it’s best not to use this form, but rather to override the to_s(IO) method. From the previous section, you know that a String is created in heap memory: if you need to create lots of them, that will damage your program’s performance because they all have to be garbage collected. You should avoid creating too many temporary strings or objects in general. Instead, append your objects with << immediately to an IO object, without creating intermediate strings through interpolation, using to_s or concatenation, as in the first of the to_s methods that follow:
| class Mineral |
| getter name, hardness |
| |
| def initialize(@name : String, @hardness : Float64) |
| end |
| |
| # Good |
| def to_s(io) |
| io << name << ", " << hardness |
| end |
| |
| end |
| |
| min1 = Mineral.new("gold", 42.0) |
| io = IO::Memory.new |
| # To see what io contains, use to_s: |
| min1.to_s(io).to_s # => "gold, 42.0" |
IO is a module for input and output in Crystal, supporting many different media: in memory, on a file, on a socket, and so on.
Knowing that Exception is a class, you can infer that it has a lot of subclasses, such as IndexError, TypeCastError, IO::Error, and others. In addition, you can also define your own subclasses:
| class CoolException < Exception |
| end |
| |
| raise CoolException.new("Somebody pushed the red button") |
| # => Somebody pushed the red button (CoolException) |
Better rescue this! You can use multiple rescue branches, which each accept a certain type of Exception, putting a catchall rescue branch at the end:
| ex = begin |
| raise CoolException.new |
| rescue ex1 : IndexError |
| ex1.message |
| rescue ex2 : CoolException | KeyError |
| ex2.message |
| rescue ex3 : Exception |
| ex3.message |
| rescue # catch any kind of exception |
| "an unknown exception" |
| end # => "ex2" |
Here’s a more realistic example of exception handling when reading a file, trying to parse it in JSON format, and then writing it back to another file:
| require "json" |
| path = "path/to/file" |
| |
| begin |
| if File.exists?(path) |
| raw_file = File.read(path) |
| map = JSON.parse(raw_file) |
| File.write(path, "ok") |
| :ok |
| end |
| rescue JSON::ParseException # Parsing error |
| raise "Could not parse file" |
| rescue ex |
| raise "Other error: #{ex.message}" |
| end |
Using the concepts from Working with Yield, Procs, and Blocks about procs, here’s a nice way to define a series of callbacks. A callback is a method that has to be called when a certain event happens. In this case, each after_save call adds a new callback, and when the save event finally occurs, each one of the callbacks in turn is called:
| class MineralC |
| def initialize |
| @callbacks = [] of -> |
| end |
| |
| def after_save(&block) |
| @callbacks << block |
| end |
| |
| # save in database, then execute callbacks |
| def save |
| # save |
| |
| |
| rescue ex |
| p "Exception occurred: #{ex.message}" |
| else |
| @callbacks.each &.call |
| end |
| end |
| min = MineralC.new |
| min.after_save { puts "Save in DB successful" } |
| min.after_save { puts "Logging save" } |
| min.after_save { puts "Replicate save to failover node" } |
| min.save # => |
| # Save in DB successful |
| # Logging save |
| # Replicate save to failover node |
➤ a. Reopen a Class:
By now, you know why the following code gives an exception:
| x = rand < 0.0001 ? 1 : "hello" |
| x - 1 # => Error: undefined method '-' for String |
Define a new method for String by overloading the - operator. It should take a number and cut off as many characters from the string as the number indicates.
➤ b. Reopen a Method:
Predict what the p statements in the following code will show, first in Crystal and then in Ruby. Try it out and explain the difference:
| class A |
| def b |
| 41 |
| end |
| end |
| # this can also be written on 1 line as: class A; def b; 41; end; end; |
| |
| p A.new.b |
| |
| class A |
| def b |
| 42 |
| end |
| end |
| |
| p A.new.b |
A method can be redefined and effectively overwritten, but you can invoke the earlier version inside the redefinition with previous_def.
3.135.187.210