Ruby, class and inheritance

Viewed 110

I need help understanding inheritance.

class MyArray < Array
end

a = MyArray[1, 2, 3] #=> [1, 2, 3]
b = MyArray[4, 5]    #=> [4, 5]
c = a + b            #=> [1, 2, 3, 4, 5]

a.class #=> MyArray
b.class #=> MyArray
c.class #=> Array

I don't understand why the result of the addition is not an instance of MyArray class.

3 Answers

I don't understand why my array "a" is not "MyArray" class after add.

Why should it (be a MyArray)? Concatenation operation on arrays is defined to return a new Array, so that's what happens here. https://ruby-doc.org/core-2.5.3/Array.html#method-i-2B

If you want, you can override that operation in your class to return an instance of MyArray. Don't forget about all other similar methods.

This is also why it's a bad idea to subclass standard collections. Better to use composition over inheritance here.

Just to add a bit to Sergio's answer in terms of his comment on using composition over inheritance and the exchange in the comments.

Instead of saying MyArray is an array you can say MyArrayLike has and array. Then you can "forward" methods that make sense to the underlying array but still add your own functionality that makes sense for your class without sub-classing array.

Ruby even has several ways to make this very easy including the Forwardable module.

class MyArrayLike 
   attr_reader :arr 
   def initialize( initial_arr )
      @arr = initial_arr 
   end

   def +(other)
     result = self.class.new(arr + other.arr)
     # maybe you want to do more than just concat the underlying array, if so you can do it here
     result
   end

   def first
      # for example maybe you want first to just return the first item in the underlying array.
      arr.first 
   end    
end


a = MyArrayLike.new([1,2,3])
b = MyArrayLike.new([4,5])

puts "a.class = #{a.class}"
# => a.class = MyArrayLike
puts a
# => #<MyArrayLike:0x00000000dc4b00>
a += b
puts "a.class = #{a.class}"
# => a.class = MyArrayLike
puts a 
# => #<MyArrayLike:0x00000000dc4470>

puts a.first 
# => 1
puts a.arr 
# => 1
#    2
#    3
#    4
#    5 

Adding MyArray to MyArray to get Array may be counterintuitive, but a method can be defined to return any class. And in the case of Array#+, which you are calling, it just happens to be defined to return an Array. That's it.

If you want it to return a MyArray, one way to do it is to define MyArray#+ as follows:

class MyArray < Array
  def +other
    MyArray.new(super)
  end
end

(MyArray.new([1, 2, 3]) + MyArray.new([4, 5])).class # => MyArray

By the way, notice that your MyArray#initialize definition is meaningless, and hence redundant.

Related