X
5434 Rate this article:
5.0

In-Scope Object for Data I/O: Using Garbage Collection to Clean up LUNs & File IDs

Anonym

When writing a program in IDL, I find it very tedious to remember that I must always clean up any file LUNs or file IDs that were created in the program. I must do this no matter what exit my program takes. Otherwise, I will eventually end up with this frustrating error message:

% OPENR: All available logical units are currently in use.

Then I have to debug my program to see where it opens files but doesn't close them.

In addition to closing files immediately before any RETURN statement, I must remember that any routine or subroutine I write needs to consider closing files that were opened in the routine if an error occurs. Therefore, my routines would need to contain a catch block like this one:

CATCH, err
IF err NE 0 THEN BEGIN
  CATCH, /CANCEL
  IF N_ELEMENTS(unit) GT 0 THEN BEGIN
    FREE_LUN, unit
  ENDIF
  MESSAGE, /REISSUE_LAST
ENDIF

Not only is this is something I often forget, but the use of a catch block that calls REISSUE_LAST makes debugging of the routine difficult. Upon encountering an error, IDL will execute the Catch block and then halt at the REISSUE_LAST line rather than halting at the line where the error actually occurred. 

Creating an In-Scope Object

An alternative to the catch block above is to create a simple object that opens the file when initialized, and then closes the file when it falls out of scope. If an instance of the object is created within a routine or subroutine, then as long as it isn't passed out of the routine where it lives, IDL's Automatic Garbage Collection will destroy the object upon exit of the routine. When the object is destroyed, the object's ::Cleanup method will be called, which is where the file close can be done. This has the benefit that the file close logic only ever needs to be written in one place, and it will always be executed.

Here is an example of an in-scope file manager object that accepts a single filename upon Init, and the file unit is automatically freed when the object is destroyed. 

FUNCTION InScopeFileManager::Init, filename
  COMPILE_OPT IDL2
  
  OPENR, unit, filename, /GET_LUN
  ; Store the unit as a member variable.
  self.unit = unit
  
  IF (~self.IDL_Object::Init()) THEN RETURN, 0
  
  RETURN, 1
  
END

;------------------------------------------------

PRO InScopeFileManager::GetProperty, UNIT=unit
  COMPILE_OPT IDL2
  
  IF (ARG_PRESENT(unit)) THEN unit = self.unit
  
END

;------------------------------------------------

PRO InScopeFileManager::Cleanup
  COMPILE_OPT IDL2
  
  IF (self.unit GT 0) THEN FREE_LUN, self.unit
  
END

;------------------------------------------------

PRO InScopeFileManager__Define
  COMPILE_OPT IDL2
  
  !NULL = {InScopeFileManager, $
           INHERITS IDL_Object, $
           unit: 0}
  
END

Making Use of the In-Scope File Manager Object

Now that I've implemented an in-scope object to manage my file, I can call it in lieu of direct file open calls. 

The following example routine will use the object to open a file. It will then step through each line of the file, analyzing each line to determine whether the line contains some piece of information I want. If the information I want is found, then the routine is done and it returns without needing to finish reading the file.

Notice that when the routine is complete, the object will fall out of scope, and IDL will automatically clean it up for me. There is nothing in this routine I need to do to ensure the file is closed when I'm done, including when I call the early return.

PRO ReadMyFile, filename, output
  COMPILE_OPT IDL2
  
  fileMng = InScopeFileManager(filename)
  nLines = FILE_LINES(filename)
  FOR i=0, nLines-1 DO BEGIN
    line = ''
    READF, fileMng.UNIT, line
    ; Call a function that analyzes a line from the file 
    ; and determines whether or not that line has what I 
    ; am looking for.
    AnalyzeLine, line, output, HAS_WHAT_I_WANT=hasWhatIwant
    ; If this line has what I want, then return and don't 
    ; read the rest of the file.
    IF (hasWhatIWant) THEN RETURN
  ENDFOR
  
END

In the event that I want to manually close the file within the routine, for instance if I'm looping through several files and want to close the first file before opening the next, all I have to do is call

OBJ_DESTROY, fileMng

and the Cleanup method will do the work to free the LUN.

In the example above, I can verify that the file was properly closed after running the routine by calling

HELP, /FILES

The file should not appear as being open in IDL.

Other Uses of In-Scope Objects

In addition to managing file units, this principle can be used for other types of file handles, such as keeping track of file and dataset identifiers when working with HDF data. 

Furthermore, in-scope objects can be used outside of reading data. For instance, if a routine displays a widget dialog, such as a status message or a progress bar, and you want to ensure that the widget is always closed upon exit of the routine, then you can wrap the widget into an object just like the one above, where the Cleanup method will contain a line like this:

WIDGET_CONTROL, self, widgetId, /DESTROY

When the routine exits, the object will fall out of scope, it will be garbage collected, and the widget will be destroyed.

In conclusion, I must say that even though it's a bit of extra work up front to create an in-scope object rather than just use a catch block or manually make file close calls, I have found that using this method has saved me time and frustration in the long run.