神刀安全网

A Deep Dive into Ruby Scopes

byDaniel P. Clark | No Comments

The Ruby language was designed with a pure object-oriented approach. In Ruby, everything is an object.

Object-oriented design provides encapsulation for properties and actions. Encapsulation’s purpose is to protect methods and data from outside interference and misuse. With encapsulation, everything has certain scopes from which they may be utilized. Several categories of scope in Ruby are global, instance, and local scopes. These are the primary scopes within Ruby, but there are some outliers to the rules, such as class variables and the use of lexical scope with refinements.

Understanding Ruby scopes will go a long way in helping you fully leverage the language. I’ve compiled an in-depth overview to demonstrate how they can assist you with having a more beautiful code base.

Encapsulation

Let’s start out with an example of encapsulation with local variables. (Local variables can be created when you use the equals sign for assignment, such as a = 1 .) First, I’ll show some code written within a begin-end block which has no encapsulation and then a simple method definition which does.

begin   a = 4 end puts a if defined?(a) # 4  def local_var_example   b = 4 end local_var_example puts b if defined?(b) # => nil

Here we can clearly see that when we assigned the value 4 to the local variable b that the variable did not exist beyond the scope of the function. This way, you can write as much code as you want within the method and not worry about variables leaking out. Local variables are very scoped; you cannot write a local variable before a standard method definition and retrieve it.

a = "example" def a?   puts a end  a? #NameError: undefined local variable or method `a' for main:Object

If you want to draw in the environment and access local scope from outside a method definition, you may use closure definitions such as define_method , proc , or lambda .

word = "moo"  define_method :rage: do   puts word end x # moo  y = proc {puts word} y.call # moo  z = lambda {puts word} z.call # moo

Caveat

Local variables take precedence over methods of the same name. To explicitly ask for the method result when there’s a local variable of the same name, you can use the send method.

a = 4 def a   5 end  puts a # 4 send :a # => 5

Instances

With Ruby being an object-oriented language, we get to create multiple object instances of their class definitions. Every Ruby object is its own singleton instance. And I mean that purely by the definition of the word singleton : “a single person or thing of the kind under consideration.” If you had two identical human clones, they would still each be their own individual existence. It’s the same way in Object-Oriented Programming.

When you want to define a kind of object that you plan on having more than one instance of, you write a classification of the object with class.

class Pet   def mood     "hungry"   end end  cat = Pet.new dog = Pet.new mouse = Pet.new  cat.mood # => "hungry" dog.mood # => "hungry"

The method mood is an instance method. It’s defined only on all instances created from the class Pet. The one place it is not defined, however, is on the classification of Pet itself.

Pet.mood #NoMethodError: undefined method `mood' for Pet:Class

The Pet class is not an instance of itself; it is a classification of the kind of objects you can propagate from it. This is the intent of Ruby’s class design, where it provides the new method for you to instantiate individual instances of this kind of class. So the scope of methods defined in this way are all for the objects that will be created from it.

Now it is possible to write methods for the class Pet itself and not for the instances created from it. To do this, we define a method on self . Here are two ways you may do this:

class Pet   def self.definition     "Living thing belongs to an owner and is cared for.  Can be a plant, animal, or amoeba."   end    class << self     def free?       "Not likely.  How much money do you have?"     end   end end  Pet.definition # => "Living thing belongs to an owner and is cared for.  Can be a plant, animal, or amoeba."  Pet.free? # => "Not likely.  How much money do you have?"  dog = Pet.new dog.free? #NoMethodError: undefined method `free?' for #<Pet:0x00000002845330>

As you can see, the scope of methods defined on self within a class is only available on that singleton instance of the class. The created objects from this classification will not have those methods defined, as they were written specifically for the class Pet.

The same thing is true with modules. When you define a method with def mood in a module, it will only be available within the scope of classes that have it “included” (much like what the class method new does). And if you use the self identifier for defining a method on a module, it will only be available on that singleton instance of the module and not any class it is inherited in.

module Car   def self.description     "A vehicle of transportation"   end    def engine     "vroom"   end end  Car.description # => "A vehicle of transportation" Car.engine #NoMethodError: undefined method `engine' for Car:Module  class Boxcar   include Car end  betsy = Boxcar.new betsy.engine # => "vroom" betsy.description #NoMethodError: undefined method `description' for #<Boxcar:0x000000025e61c0>

The scope of the methods defined are dependent on whether you assign it to the singleton instance of that object or let it be defined on instances from that object.

Singleton Instance

Saying “singleton instance” feels a bit repetitive to me, but it is important to specify so as not to confuse it with the Singleton Design Pattern or the singleton_class object which exists on most Ruby objects (which is not the singleton instance of the object it is on but is an extra singleton instance of its own).

Ruby is designed where everything is an instance of the Object class and therefore is a singleton instance, meaning that it exists as its own individual self. Yes, this may seem confusing at first, but once you see every module, class, and object as their own singleton instance which may or may not create more singleton instances from their definitions, then things become clearer.

Global Scope

When you write code at the top level, you are writing in global scope. Local variables will not cross over any scope, instance variables will become available to local methods, and methods and classes are available everywhere.

local_variable = 1     # not available in any other scope  @instance_variable = 2 # available within methods in the same scope  $global_variable = 3   # available everywhere  CONSTANT = 4           # available everywhere  def a_method           # available everywhere end  class Klass            # available everywhere end  module Mod             # available everywhere end

All of these, except for global variables, can be encapsulated within singleton instances and maintain the same behavior as described above.

Now the way method definitions are managed in the global namespace is quite interesting. As you may recall that everything in Ruby is an object, they are all also instances of the Object class itself. So the way that global methods are handled is that they are defined as private instance methods on the Object class.

def hey_you   "it's me" end  Object.private_method_defined? :hey_you # => true  Array.send :hey_you # => "it's me"  12345.send :hey_you # => "it's me" nil.send :hey_you # => "it's me"

This is also how classes and modules are made available at lower levels of scope.

Namespacing

Namespacing is the practice of placing code within the scope of another class or module. This is a good practice for both clarifying purpose, usage, and to protect code from potential clashes with other people’s code. You may reuse class or module names within a namespace without overwriting the outer definitions.

module Help   def self.me     "this is a general help"   end end  module Dog   module Help     def self.me       "woof woof woof woof"     end   end end  Help.me # => "this is a general help"  Dog::Help.me # => "woof woof woof woof"

You’ve protected your code from overwriting the other Help object by namespacing one specifically for Dog. If you want access to constants, classes, or modules at the top level of scope, you may use a double-colon :: before the object to access it.

CONSTANT = "world"  module Greeter   CONSTANT = "hello"   def self.greet     puts CONSTANT + " " + ::CONSTANT   end end  Greeter.greet # hello world

Refinements

In Ruby, you can reopen every object to add or make changes to it. Making changes outside of the scope of the original definition is known as monkey patching .

class Warn   def warn     "original behavior"   end end  class Warn   alias_method :_warn, :warn   def warn     "not " + _warn   end end  Warn.new.warn # => "not original behavior"

The problem with monkey patching is that the changes happen globally. Any other place in your code base where others have used this object and method has now been changed. Very often this is how things will break; when depended-upon code is modified globally, everyone experiences the change.

Ruby’s solution for this is to use refinements . Refinements allow you to do the same thing as monkey patching but restrict the changes only to the very specific places you specify to use it. This way you won’t break anyone else’s code because your changes are lexically scoped.

module FixForMe   refine String do     alias_method :_to_s, :to_s     def to_s       "not " + _to_s     end   end end  class A   using FixForMe   def a     "to be".to_s   end end  class B   def b     "to be".to_s   end end  A.new.a # => "not to be"  B.new.b # => "to be"

Here we have changed the behavior for String#to_s only where we’ve written using FixForMe , and the to_s behavior did not change in class B . This is how lexical scope works.

Lexical scope only goes as far as the visual block of code before you. If you reopen a class and don’t write the using syntax in it, the refinements behavior will not be there even if you’ve previously used it in the same class. Refinements are well worth using to avoid the pains that monkey patching may bring.

Binding: The Exception to Scope

The Binding object is the only object that lets you pass and modify local variables out of scope. To create a binding, you simply type the method binding , and it creates a binding of the local environment. You may pass this binding into other scopes and access the local variables from where the binding was instantiated.

module A   def self.a(bnd)     printf "%s/n", bnd.local_variables      x = bnd.local_variable_get :rage:     y = bnd.local_variable_get :y     z = bnd.local_variable_get :z      printf "%s/n", [x, y, z]        bnd.local_variable_set(:z, x + y)   end end  module B   def self.b     x = 1     y = 7     z = 0      A.a(binding)      puts "x + y = #{z}"   end end  B.b # [:x, :y, :z] # [1, 7, 0] # x + y = 8

The local variable z has been changed from a different scope in A , and the result was within B .

Class Variables

Class variables are rarely used as the scope is broadened to all instances of the same class. If you were to modify the value of a class variable in one instance, it will be changed for all other instances.

class Building   def initialize     @@state ||= :built   end    def state(value = nil)     @@state = value if value     @@state   end end  library = Building.new office = Building.new  library.state # => :built office.state # => :built  office.state :demolished # => :demolished library.state # => :demolished

As you can see, using class variables may cause surprise values in your other classes if they aren’t managed properly. It would probably be wise to think of using these variables for either read-only values or by having a thread-safe system in place.

Conclusion

Ruby is a language that has been designed to make programmers happy, and understanding its scope gives you full leverage in using the language. With it, you may employ many strategies in design that help you toward having a more beautiful code base.

I recommend studying good object-oriented design. Each language/feature is a tool, and tools are most effective when understood and mastered. Encapsulation as a core design in Ruby will serve you well if you use it in the way it was designed to be used. Thankfully Ruby is a very flexible language, so we do have a lot of free reign in how we use it.

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » A Deep Dive into Ruby Scopes

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
分享按钮