Search code examples
rubyresourcesdestructorraiifinalizer

RAII in Ruby (Or, How to Manage Resources in Ruby)


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?


Solution

  • 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