class Test
@data : IO
def initialize
file = File.tempfile
mem = IO::Memory.new
decision = false
pp file.size
pp mem.size
@data = decision ? file : mem
end
def process
pp @data
pp @data.class
pp @data.size
end
end
Test.new.process
Throws exception:
In src/test.cr:19:14
19 | pp @data.size
^---
Error: undefined method 'size' for IO::ARGF (compile-time type is IO+)
Both types are derived from IO and both have a method ‘size’.
What’s wrong here and what about IO::ARGF?
It works in the initialize method because the compiler knows their specific types File and IO::Memory. However when you try to do the same thing on the @data ivar, it’s not as simple since its typed as IO, which itself does not have a #size method. Compiler tells you this via the compile-time type is IO+ part of the error message. Basically are running into what’s outlined in Virtual and abstract types - Crystal. I.e. @data : File | IO::Memory is reduced to @data : IO+, which now fails since not all IO types implement #size.
Just a minor comment, this behaviour is not really caused by virtual and abstract types.
The type restriction of @data is actually IO, thus the parent type of File and IO::Memory. The rest still applies, that IO does not have a size method.
The trouble with virtualization is even if you would declare the ivar type with a union of the specific types (@data : File | IO::Memory), it wouldn’t work because that would then virtualize into IO+.
As a workaround you can explicitly add type guards for @data to make the compiler happy:
if (data = @data) && data.is_a?(File) || data.is_a?(IO::Memory)
data.size
end
This is quite verbose, unfortunately.
A little more concise alternative is checking for size method:
if (data = @data).responds_to?(:size)
data.size
end