Code against an interface with TStrings and TStringList

3.4k views Asked by At

I read with interest Nick Hodges blog on Why You Should Be Using Interfaces and since I'm already in love with interfaces at a higher level in my coding I decided to look at how I could extend this to quite low levels and to investigate what support for this existed in the VCL classes.

A common construct that I need is to do something simple with a TStringList, for example this code to load a small text file list into a comma text string:

var
  MyList : TStrings;
  sCommaText : string;
begin
  MyList := TStringList.Create;
  try
    MyList.LoadFromFile( 'c:\temp\somefile.txt' );
    sCommaText := MyList.CommaText;

    // ... do something with sCommaText.....

  finally
    MyList.Free;
  end;
end;

It would seem a nice simplification if I could write with using MyList as an interface - it would get rid of the try-finally and improve readability:

var
  MyList : IStrings;
         //^^^^^^^
  sCommaText : string;
begin
  MyList := TStringList.Create;
  MyList.LoadFromFile( 'c:\temp\somefile.txt' );
  sCommaText := MyList.CommaText;

  // ... do something with sCommaText.....

end;

I can't see an IStrings defined though - certainly not in Classes.pas, although there are references to it in connection with OLE programming online. Does it exist? Is this a valid simplification? I'm using Delphi XE2.

2

There are 2 answers

2
Rob Kennedy On

Since TStrings is an abstract class, an interface version of it wouldn't provide much. Any implementer of that interface would surely be a TStrings descendant anyway, because nobody would want to re-implement all the things TStrings does. I see two reasons for wanting a TStrings interface:

  1. Automatic resource cleanup. You don't need a TStrings-specific interface for that. Instead, use the ISafeGuard interface from the JCL. Here's an example:

    var
      G: ISafeGuard;
      MyList: TStrings;
      sCommaText: string;
    begin
      MyList := TStrings(Guard(TStringList.Create, G));
    
      MyList.LoadFromFile('c:\temp\somefile.txt');
      sCommaText := MyList.CommaText;
    
      // ... do something with sCommaText.....
    end;
    

    To protect multiple objects that should have the same lifetime, use IMultiSafeGuard.

  2. Interoperation with external modules. This is what IStrings is for. Delphi implements it with the TStringsAdapter class, which is returned when you call GetOleStrings on an existing TStrings descendant. Use that when you have a string list and you need to grant access to another module that expects IStrings or IEnumString interfaces. Those interfaces are clunky to use otherwise — neither provides all the things TStrings does — so don't use them unless you have to.

    If the external module you're working with is something that you can guarantee will always be compiled with the same Delphi version that your module is compiled with, then you should use run-time packages and pass TStrings descendants directly. The shared package allows both modules to use the same definition of the class, and memory management is greatly simplified.

4
David Heffernan On

There is no interface in the RTL/VCL that does what you want (expose the same interface as TStrings). If you wanted to use such a thing you would need to invent it yourself.

You would implement it with a wrapper like this:

type
  IStrings = interface
    function Add(const S: string): Integer;
  end;

  TIStrings = class(TInterfacedObject, IStrings)
  private
    FStrings: TStrings;
  public
    constructor Create(Strings: TStrings);
    destructor Destroy; override;
    function Add(const S: string): Integer;
  end;

constructor TIStrings.Create(Strings: TStrings);
begin
  inherited Create;
  FStrings := Strings;
end;

destructor TIStrings.Destroy;
begin
  FStrings.Free; // don't use FreeAndNil because Nick might see this code ;-)
  inherited;
end;

function TIStrings.Add(const S: string): Integer;
begin
  Result := FStrings.Add(S);
end;

Naturally you would wrap up the rest of the TStrings interface in a real class. Do it with a wrapper class like this so that you can wrap any type of TStrings just by having access to an instance of it.

Use it like this:

var
  MyList : IStrings;
....
MyList := TIStrings.Create(TStringList.Create);

You may prefer to add a helper function to actually do the dirty work of calling TIStrings.Create.

Note also that lifetime could be an issue. You may want a variant of this wrapper that does not take over management of the lifetime of the underlying TStrings instance. That could be arranged with a TIStrings constructor parameter.


Myself, I think this to be an interesting thought experiment but not really a sensible approach to take. The TStrings class is an abstract class which has pretty much all the benefits that interfaces offer. I see no real downsides to using it as is.