When inserting objects into collections or passing as parameters to methods, most are passed as reference, Fixnum
are passed as value.
- Consider adding a
!
to methods that will change the parameters. - Consider using
Array#reject
instea ofArray#delete_if
.
class Tuner
def initialize (presets)
@presets = presets
clean
end
private
def clean
# Valid frequencies end in odd digits.
@presets.delete_if {|f| f[-1].to_i.even?} # Will modify the object passed as param outside the class
end
end
class Tuner
def initialize (presets)
@presets = clean(presets)
end
private
def clean (presets)
presets.reject {|f| f[-1].to_i.even?} # Will make a copy omitting odd numbers
end
end
Object#clone
and Object#dup
produce a shallow copy of obj—the instance variables of obj are copied, but not the objects they reference. clone
copies the frozen and tainted state of obj.
clone
, unlike dup
, will make the copy frozen if the original is frozen.
If obj
has singleton methods, clone
will also duplicate any singleton class, unlike dup
.
On Ruby 2.4+ clone(freeze: true)
can recieve the freeze param as false to make an unfrozen copy.
In general, clone and dup may have different semantics in descendant classes. While clone is used to duplicate an object, including its internal state, dup typically uses the class of the descendant object to create the new instance.
class Tuner
def initialize (presets)
@presets = presets.dup
clean # Modifies the duplicate.
end
...
end
When copying collections, clone
and dup
don't make copies of the referenced objects in the colleciton.
When writing your own class you can override the initialize_copy
method to control the depth of the duplication process.
Marshall
class can serialize and deserialize the collection and its elements:
irb> a = ["Monkey", "Brains"]
irb> b = Marshal.load(Marshal.dump(a))
irb> b.each(&:upcase!); b.first # "MONKEY"
irb> a.last # "Brains"
Marshall
limitations:
- Memory heavy (would need to have in memory the original, the copy and the serialized version)
- Not all objects can be serialized and will rise TypeError exceptions
- Objs with clousures or singleton methods can't
- Some Ruby core classes (eg. IO, File)
The Array
method converts its arguments into an array
Array(1..5) #=> [1, 2, 3, 4, 5]
Array(['Nadroj', 'Retep']) #=> ["Nadroj", "Retep"]
Array(nil) #=> []
h = {pepperoni: 20, jalapenos: 2}
Array(h) #=> [[:pepperoni, 20], [:jalapenos, 2]]
Set
is a class from the standard library, it has to be required.
Is a container that skips duplicates.
require('set')
class Role
def initialize(name, permissions)
@nae, @persissoins = name, Set.new(permissions)
end
def can?(permission)
@permissoins.include?(permission)
end
Set
characteristics:
- Needs to be required.
- Uses obj's
eql?
to check for identity. Therfore the stored objs should be able to be hash keys. - Unordered container.
- Can't index individual elements.
In case of wanting an order, consider SortedSet
which is also part of the standard library.
def sum(enum)
enum.reduce(0) do |accumulator, element|
accumulator + element
end
end
The block doesn’t make any assignments.
After each iteration, reduce
throws away the previous accumulator and keeps the return value of the block as the new accumulator. If you mutate the accumulator and return nil from the block then the accumulator for the next iteration will be nil.
The argument given to reduce is the starting value for the accumulator, if omitted, the first element will be used and start the iteration cycle on the second element.
A symbol can be passed which will be called as a method on the accumulator and passing the current element as the argument:
def sum(enum)
enum.reduce(0, :+)
end
To transform an Array
to a Hash
array.reduce({}) do |hash, element|
hash.update(element => true)
end
Another optimization using reduce could be when filtering and extracting information from a collection.
users.reduce([]) do |names, user|
names << user.name if user.age >= 21
names
end
With Hash.new(param)
you can set param
as the default value to be returned on non existing keys, instead of nil
h = Hash.new(0)
a = h[:missing_key] # 0
h.keys # []
h[:missing_key] += 1 # 43
h.keys # [:missing_key]
But the default value is mutable.
h = Has.new([])
h[:missing_key] << "Hi"
h # []
h.keys # []
h[:missing_key2] # ["Hi"]
And beware of unintended beahaviour.
h = Has.new([])
h[:day] = h[:day] << "Sunday"
h[:month] = h[:month] << "January"
h[:month] # ["Sunday", "January"]
h.default # ["Sunday". "January"]
^ This can be avoided by passing Hash::new a block, that will be invoked when needs a default value.
h = Hash.new { [] }
h[:day] = h[:day] << "Sunday"
h[:month] = h[:month] << "January"
h[:day] # ["Sunday"]
The given block can receive 2 arguments, the hash itself and the accessed key.
h = Hash.new {|hash, key| hash[key] = [] }
h[:day] << "Sunday"
h[:holidays] # []
h.keys # [:weekdays, :holidays]
^ With this solution, every time a missing key is accessed, the block create a new entry in the hash and it’ll create a new array.
The correct way to check for a key/value existance is using has_key?
method.
if hash.has_key?(:day)
...
end
Hash#fetch
can be used to establish a default value in case of a non existing key, which would be the second parameter. Or if the second parameter is ommited, it will raise an exception on nonexistent key.
h = {}
h[:day] = h.fetch(:day, []) << "Sunday" # ["Sunday"]
h.fetch(:missing_key)
KeyError: key not found: :missing_key
A class that inherence from Array, some of its methods will return a Array instead of the inhereted class.
class LikeArray < Array; end
x = LikeArray.new([1,2,3]) # [1,2,3]
y = x.reverse # [3,2,1]
y.class #=> Array # Is an Array instead of a LikeArray
LikeArray.new([1,2,3]) == [1,2,3] #=> true # maybe not the expected behaviour
A solution can be found on Set
's implementation, it has internally a Hash but never exposes it.
Delegation allows you to declare methods which should be forwarded along to a specific instance variable.
require('forwardable')
# A Hash clone that will rise exception when accessing nonexistent key
class RaisingHash
extend(Forwardable)
include(Enumerable)
def_delegators(:@hash, :[], :[]=, :delete, :each,
:keys, :values, :length,
:empty?, :has_key?)
end
It can also change the access name of the method.
def_delegator(:@hash, :delete, :erase!) # Forward self.erase! to @hash.delete
To raise an error on nonexisting key access, you can pass a block to #new
def initialize
@hash = Hash.new do |hash, key|
raise(KeyError, "invalid key `#{key}'!") # raising an error will be the default value
end
end
To make a invert
method that will not return a hash but an instance of RaisingHash
def invert
other = self.class.new
other.replace!(@hash.invert)
other
end
protected
def replace! (hash)
# passes the block given to #new to ensure rising error on nonexisting key
hash.default_proc = @hash.default_proc
@hash = hash
end
To make the class behave as any collection class: For cloning:
def initialize_copy (other)
@hash = @hash.dup
end
For freezing and tainting:
def freeze # same for taint and untaint
@hash.freeze
super
end