RAII in Ruby (Or, How to Manage Resources in Ruby)
Asked Answered
O

4

10

I know it's by design that you can't control what happens when an object is destroyed. I am also aware of defining some class method as a finalizer.

However is the ruby idiom for C++'s RAII (Resources are initialized in constructor, closed in destructor)? How do people manage resources used inside objects even when errors or exceptions happen?

Using ensure works:

f = File.open("testfile")
begin
  # .. process
rescue
  # .. handle error
ensure
  f.close unless f.nil?
end

but users of the class have to remember to do the whole begin-rescue-ensure chacha everytime the open method needs to be called.

So for example, I'll have the following class:

class SomeResource
 def initialize(connection_string)
   @resource_handle = ...some mojo here...
 end

 def do_something()
   begin
    @resource_handle.do_that()
    ...
   rescue
    ...
   ensure
 end

 def close
  @resource_handle.close
 end

end

The resource_handle won't be closed if the exception is cause by some other class and the script exits.

Or is the problem more of I'm still doing this too C++-like?

Old answered 18/10, 2008 at 5:40 Comment(0)
J
17

So that users don't "have to remember to do the whole begin-rescue-ensure chacha" combine rescue/ensure with yield.

class SomeResource
  ...
  def SomeResource.use(*resource_args)
    # create resource
    resource = SomeResource.new(*resource_args) # pass args direct to constructor
    # export it
    yield resource
  rescue
    # known error processing
    ...
  ensure
    # close up when done even if unhandled exception thrown from block
    resource.close
  end
  ...
end

Client code can use it as follows:

SomeResource.use(connection_string) do | resource |
  resource.do_something
  ... # whatever else
end
# after this point resource has been .close()d

In fact this is how File.open operates - making the first answer confusing at best (well it was to my work colleagues).

File.open("testfile") do |f|
  # .. process - may include throwing exceptions
end
# f is guaranteed closed after this point even if exceptions are 
# thrown during processing
Jigaboo answered 3/7, 2009 at 4:39 Comment(0)
R
8

How about yielding a resource to a block? Example:

File.open("testfile") do |f|
  begin
    # .. process
  rescue
    # .. handle error
  end
end
Repercussion answered 18/10, 2008 at 6:9 Comment(2)
In fact, File#open called with a block has an 'ensures' block, which properly closes the file, whatever happens. So yes, you can have rescue/ensure blocks, but they are optional.Revive
Exactly. With this idiom, you only have to implement the ensure block once per type of resource, rather than in all code that uses the resource.Repercussion
L
3

Or is the problem more of I'm still doing this too C++-like?

Yes it is since in C++ resource deallocation happens implicitly for everything on the stack. Stack unwound = resource destroyed = destructors called and from there things can be released. Since Ruby has no destructors there is no "do that when everything else is done with" place since grabage collection can be delayed several cycles from where you are. You do have finalizers but they are called "in limbo" (not everything is available to them) and they get called on GC.

Therefore if you are holding a handle to some resource that better be released you need to release it explicitly. Indeed the correct idiom to handle this kind of situation is

def with_shmoo
  handle = allocate_shmoo
  yield(handle)
ensure
  handle.close
end
Leftover answered 29/11, 2010 at 23:30 Comment(0)
F
-1

See http://www.rubycentral.com/pickaxe/tut_exceptions.html

In Ruby, you would use an ensure statement:

f = File.open("testfile")
begin
  # .. process
rescue
  # .. handle error
ensure
  f.close unless f.nil?
end

This will be familiar to users of Python, Java, or C# in that it works like try / catch / finally.

Frangos answered 18/10, 2008 at 5:49 Comment(1)
While this works, you have to depend on someone that is using your class to do the right thing. They have to remember to do the whole begin-rescue-ensure chacha everytime the open method needs to be called. I'm editing the question to make this clearer, but thanks for the answer :)Old

© 2022 - 2024 — McMap. All rights reserved.