You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Use String#to_s and Integer#to_i method to avoid calling methods on nil objects.
defformat_string(string)string.to_s.upcaseend
Array#compact method removes all nil from arrays
[1,nil,3,nil].compact# [1, 3]
ITEM 3: AVOID RUBY’S CRYPTIC PERLISMS
Unlike with local variables, instance variables, or even constants, you’re allowed to use all sorts of characters as variable names, including numbers. Global variables vegin with a $.
The first perlisms in that code is the =~ operator from String class. When the regular expression matches, several global variables will be set. Global variable $1 has the content of the first capture group.
Variables created by the =~ operator are called special global variables because they’re scoped locally to the current thread and method. Essentially, they’re local values with global names. Outside of the extract_error method from the previous example, the $1 “global” variable is nil, even after using the =~ operator.
String#match is more idiomatic and doesn't use any of the special global variables. It returns a MatchData object with all relevant information.
$: is an array of strings representing the directories where Ruby will search for the libraries loaded with the require method. The alias $LOAD_PATH is prefered. All cryptic global variable have descriptive alias, but some require the library require('English').
whilereadlineprintif ~ /^ERROR:/end
Here the Kernel#readline method reads a line from standard input and returns it. It also stores that line of input in the $_ variable. If Kernel#print is called without arguments, it will print the content of $_ to standard output. Regexp#~ operator tries to match the content of $_ against the regular expression to its right. If there's a match, it returns the position of the match, otherwise it returns nil.
ITEM 4: BE AWARE THAT CONSTANTS ARE MUTABLE
A constant is any identifier which begins with an uppercase letter. The names of classes and modules are actually constants in Ruby.
Constants are more like global variables than unchanging values.
Always freeze constants to prevent them from being mutated. But consider this:
defhost_addresses(host,networks=Defaults::NETWORKS)networks.map{ |net| net << ".#{host}"}end
While the NETWORKS array itself is frozen, its elements are still mutable. You might not be able to add or remove elements from the array, but you can surely make changes to the existing elements.
If a constant references a collection object such as an array or hash, freeze the collection and its elements:
To prevent assigning new values to existing constants, freeze the class or module they’re defined in. You may even want to structure your code so that all constants are defined in their own module, isolating the affects of the freeze method:
moduleDefaultsTIMEOUT=5endDefaults.freeze
So, freeze the constant, freeze its module and in case of a collection freeze its elements.
ITEM 6: KNOW HOW RUBY BUILDS INHERITANCE HIERARCHIES
ITEM 7: BE AWARE OF THE DIFFERENT BEHAVIORS OF SUPER
ITEM 8: INVOKE SUPER WHEN INITIALIZING SUB-CLASSES
ITEM 9: BE ALERT FOR RUBY’S MOST VEXING PARSE
ITEM 10: PREFER STRUCT TO HASH FOR STRUCTURED DATA
ITEM 11: CREATE NAMESPACES BY NESTING CODE IN MODULES
ITEM 12: UNDERSTAND THE DIFFERENT FLAVORS OF EQUALITY
equal?# compares by object id, it is not to be overrided.
== # Behaves as `equal?`, should be overriden, expected to represent same value eg. 1 == 1.0. Check ITEM 13eql?# Behaves as `equal?`, should be overriden. It is the most loosly defined of the three.
=== # case equality operator, used on case expresions
About hashes
eql? is used by hashes to determine the hash's key, in case of collision, another comparison is made using the hash method defined on the instance.
ITEM 13: IMPLEMENT COMPARISON VIA "<=>" AND THE COMPARABLE MODULE
<=> operator, informally referred to as the "spaceship" operator.
When writing a comparison operator it’s common practice to name the argument “other” since it will be the other object you’re comparing the receiver with.
When it doesn’t make sense to compare the receiver with the argument then the comparison operator should return nil.
If the receiver is less than the argument, return -1.
If the receiver is greater than the argument, return 1.
If the receiver is equal to the argument, return 0.
Example
Class to compare software versions, eg. 2.1.1, 2.10.3
Consider that the Comparable module includes a version of "==". You can set its behaviour by changing the conditions <=> returns 0 or overriding it.
To make instances usable as Hash keys:
ITEM 14: SHARE PRIVATE STATE THROUGH PROTECTED METHODS
protected methods were designed for sharing private information between related classes.
The caller and receiver don’t necessarily have to be instances of the same class, but they both have to share the superclass where the method is defined.
ITEM 15: PREFER CLASS INSTANCE VARIABLES TO CLASS VARIABLES
@ instance variables
@@ class variables (know as static on other OO languages)
Class variables in a superclass are shared between it and all of its subclasses.
Singleton class manual implementation (instead of Ruby module)
classSingletonprivate_class_method(:new,:dup,:clone)# can't be called from the outsidedefself.instance@single ||= new# new will be called only the first time and is stored on a class' instance variableendend
Classes are objects, so they have instance variables too.
“class methods” are actually instance methods for the class object.
Class variables and class instance variables have all of the same problems that global variables do. If your application has multiple threads of control then altering any of these variables without using a mutex isn’t safe. Thankfully, the Singleton module which is part of the Ruby standard library correctly implements the singleton pattern in a thread-safe way. Using it is simple:
ITEM: 16: DUPLICATE COLLECTIONS PASSED AS ARGUMENTS BEFORE MUTATING THEM
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 of Array#delete_if.
classTunerdefinitialize(presets)@presets=presetscleanendprivatedefclean# Valid frequencies end in odd digits.@presets.delete_if{|f| f[-1].to_i.even?}# Will modify the object passed as param outside the classendend
classTunerdefinitialize(presets)@presets=clean(presets)endprivatedefclean(presets)presets.reject{|f| f[-1].to_i.even?}# Will make a copy omitting odd numbersendend
Copying objects
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.
classTunerdefinitialize(presets)@presets=presets.dupclean# 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:
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:
^ 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.
ifhash.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
ITEM 21: PREFER DELEGATION TO INHERITING FROM COLLECTION CLASSES
A class that inherence from Array, some of its methods will return a Array instead of the inhereted class.
classLikeArray < Array;endx=LikeArray.new([1,2,3])# [1,2,3]y=x.reverse# [3,2,1]y.class#=> Array # Is an Array instead of a LikeArrayLikeArray.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 keyclassRaisingHashextend(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
definitialize@hash=Hash.newdo |hash,key|
raise(KeyError,"invalid key `#{key}'!")# raising an error will be the default valueendend
To make a invert method that will not return a hash but an instance of RaisingHash
definvertother=self.class.newother.replace!(@hash.invert)otherendprotecteddefreplace!(hash)# passes the block given to #new to ensure rising error on nonexisting keyhash.default_proc=@hash.default_proc@hash=hashend
To make the class behave as any collection class:
For cloning:
definitialize_copy(other)@hash=@hash.dupend
For freezing and tainting:
deffreeze# same for taint and untaint@hash.freezesuperend
Exceptions can be thought of as two different language features rolled into one, error descriptions and control flow.
ITEM 22: PREFER CUSTOM EXCEPTIONS TO RAISING STRINGS
raise("coffee machine low on water")
Is equivalent to
raise(RuntimeError,"coffee machine low on water")
The first argument is the class name to create and raise an exception object from the class. The second is the string to be used as the error message.
A RuntimeError is nothing more than a generic “Oops, something went wrong” error.
Since exceptions are handled based on their type, creating a new class is the standard way to differentiate it.
Rules to create exception classess:
New exception classes must inherit from one of the standard exception classes.
The Exception class and several of its sub-classes are considered low-level errors which should generally crash the program. The majority of the standard exception classes inherit instead from StandardError and you should follow suit.
It’s common practice to give exception class names the “Error” suffix.
Inheriting from StandardError comes from the default behavior of the rescue clause. You can omit the class name when handling exceptions with rescue. In this case it will intercept any exception whose class (or superclass) is StandardError. (The same is true when you use rescue as a statement modifier.)
To create the most basic exception:
classCoffeeTooWeakError < StandardError;end
And to raise that exception:
raise(CoffeeTooWeakError)# orraise(CoffeeTooWeakError,"coffee to water ratio too low")# with a more descriptive message
The raise method sends the exception message to the first argument. This method is suppose to return an object which can then be raised.
Both exception classes and exception objects have their own exception method courtesy of the Exception class. The class method version is simply an alias for new.
The instance method version of exception is a bit weird though, depending on how many arguments are given to raise:
1 argument, an exception object: returns self
2 arguments, an exception object and a message: A copy of the exception will be made with the msg as its own overriding internal message, and raising this copy.
If you are going to create several exception classes for a project, consider creating a base class that inherits from StandardError, and inherit from it.
ITEM 23: RESCUE THE MOST SPECIFIC EXCEPTION POSSIBLE
Ruby evaluates the rescue clauses in order, from top to bottom, first match wins.
This style is too brittle and error-prone.
Whitelisting
A better approach would be to rescue only the ones you know how to handle and let the rest propagate up the stack.
begintask.performrescueNetworkConnectionError=>e# Retry logic...rescueInvalidRecordError=>e# Send record to support staff...end
To be able to do some 'cleaning' before exiting the current scope, you can use ensure which will be executed for both normal and exceptional situations.
begintask.performrescueNetworkConnectionError=>e# Retry logic...rescueInvalidRecordError=>e# Send record to support staff...rescue=>eservice.record(e)raiseensure
...
end
Exceptions raised while a rescue clause is executing replace the original exception, exit the scope of the rescue clause, and start exception processing with the new exception. So when handling exceptions try to not create another exception, e.g. trying to connect to a log server and having a connection exception.
To avoid this, you could create a method that receives the exception
defsend_to_support_staff(e)
...
rescueraise(e)end
ITEM 24: MANAGE RESOURCES WITH BLOCKS AND ENSURE
ITEM 25: EXIT ENSURE CLAUSES BY FLOWING OFF THE END
ITEM 26: BOUND RETRY ATTEMPTS, VARY THEIR FREQUENCY, AND KEEP AN AUDIT TRAIL
ITEM 27: PREFER THROW TO RAISE FOR JUMPING OUT OF SCOPE