Code against an interface with TStrings and TStringList
Asked Answered
W

2

7

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.

Wismar answered 1/2, 2012 at 12:37 Comment(10)
If you want my opinion: don't do that! Just as never using interfaces is no solution, using interfaces for everything isn't one either. Even Nick claims spefically TStrings/TStringList to be perfectly used as class instances in his latest blog post.Chally
TStrings is 'almost' an interface, it is an abstract class which could have different implementations. Whenever possible, I only pass around TStrings as type for parameters, instead of TStringList.Tycoon
I agree with @UweRaabe. Interfaces are powerful, and powerful tools are frequently misused by newbies. Don't use an interface reference instead of an object reference just because that is possible. I recommend to adhere to original purpose of interfaces - open extensible design.Ator
Developers are faddish creatures, aren't they.Veinstone
@mjn: Can you help me understand why you say, "Whenever possible, I only pass around TStrings as type for parameters, instead of TStringList." I've wondered about the difference between TString and TStringList for a long time.Tenpins
TStrings could be items in a listbox, which are not literally TStringList type, because the content is stored and managed by memory controlled by Windows Common Controls DLL, but TStrings provides the operations common to what you think of as a TStringList. In that way, but without being reference counted, it is like an interface, but is more accurately called "an abstract base class". You can't create a new object of type TStrings directly but you can subclass it and provide your own storage/backing logic. If you go the IStrings route, you just made a mess of it.Veinstone
Actually TStrings.Create works (it does not generate a compile-time error), but instance operations will fail at run timeTycoon
It generates a warning, though, doesn't it, @Mjn? It's an abstract class, or at least it used to be. Delphi only warns when instantiating abstract classes, whereas it's an error in most other languages.Zoroastrian
I didn't do IStringList or IStrings, but I did a "demo" for IIniFileServices: code.google.com/p/delphi-spring-framework/source/browse/trunk/…Larkins
@Rob definitely, I only stumbled over the statement "You can't create a new object of type TStrings directly"Tycoon
T
6

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.

Tiffanitiffanie answered 1/2, 2012 at 12:43 Comment(4)
Why not just create a TIStrings.Strings: TStrings default property to be accessed without having to wrap all methods?Ovariectomy
@arnaud that would make the code a lot more clunky because you would have to write out Strings a lot or store to a local. A default property only avoids that for array properties. But if all you wanted was an RAII emulator that would be a good way to go I guessTiffanitiffanie
@Arnoaud +1 for neatness and the fact that MyList.Free goes away.Wismar
@BrianFrost Arnaud's suggestion doesn't compile. You can only have a default property for an array property. You would have to write MyList.Strings.Add everywhere.Tiffanitiffanie
Z
4

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.

Zoroastrian answered 1/2, 2012 at 14:35 Comment(2)
ISafeGuard is crying out for generics and type-safety. It's also pretty trivial to write. If you aren't already using JCL it's probably not worth taking a dependency on JCL just to get at this.Tiffanitiffanie
@Rob: +1 for an intriguing solution. Academically fascinating but propbably not explainable to my co-workers!Wismar

© 2022 - 2024 — McMap. All rights reserved.