views:

279

answers:

3

Okay, this might be confusing. What I'm trying to do is use an enumerator to only return certain items in a generic list based on class type.

Given the following hierarchy:

type
    TShapeClass = class of TShape;

    TShape = class(TObject)
    private
        FId: Integer;
    public
        function ToString: string; override;
        property Id: Integer read FId write FId;
    end;

    TCircle = class(TShape)
    private
        FDiameter: Integer;
    public
        property Diameter: Integer read FDiameter write FDiameter;
    end;

    TSquare = class(TShape)
    private
        FSideLength: Integer;
    public
        property SideLength: Integer read FSideLength write FSideLength;
    end;

    TShapeList = class(TObjectList<TShape>)
    end;

How can I extend TShapeList such that I can do something similar to the following:

procedure Foo;
var
    ShapeList: TShapeList;
    Shape: TShape;
    Circle: TCircle;
    Square: TSquare;

begin
    // Create ShapeList and fill with TCircles and TSquares
    for Circle in ShapeList<TCircle> do begin
        // do something with each TCircle in ShapeList
    end;
    for Square in ShapeList<TSquare> do begin
        // do something with each TSquare in ShapeList
    end;
    for Shape in ShapeList<TShape> do begin
        // do something with every object in TShapeList
    end;
end;

I've tried extending TShapeList using an adapted version of Primoz Gabrijelcic's bit on Parameterized Enumerators using a factory record as follows:

type
    TShapeList = class(TObjectList<TShape>)
    public
        type
            TShapeFilterEnumerator<T: TShape> = record
            private
                FShapeList: TShapeList;
                FClass: TShapeClass;
                FIndex: Integer;
                function GetCurrent: T;
            public
                constructor Create(ShapeList: TShapeList);
                function MoveNext: Boolean;
                property Current: T read GetCurrent;
            end;

            TShapeFilterFactory<T: TShape> = record
            private
                FShapeList: TShapeList;
            public
                constructor Create(ShapeList: TShapeList);
                function GetEnumerator: TShapeFilterEnumerator<T>;
            end;

        function FilteredEnumerator<T: TShape>: TShapeFilterFactory<T>;
    end;

Then I modified Foo to be:

procedure Foo;
var
    ShapeList: TShapeList;
    Shape: TShape;
    Circle: TCircle;
    Square: TSquare;

begin
    // Create ShapeList and fill with TCircles and TSquares
    for Circle in ShapeList.FilteredEnumerator<TCircle> do begin
        // do something with each TCircle in ShapeList
    end;
    for Square in ShapeList.FilteredEnumerator<TSquare> do begin
        // do something with each TSquare in ShapeList
    end;
    for Shape in ShapeList.FilteredEnumerator<TShape> do begin
        // do something with every object in TShapeList
    end;
end;

However, Delphi 2010 is throwing an error when I try to compile Foo about Incompatible types: TCircle and TShape. If I comment out the TCircle loop, then I get a similar error about TSquare. If I comment the TSquare loop out as well, the code compiles and works. Well, it works in the sense that it enumerates every object since they all descend from TShape. The strange thing is that the line number that the compiler indicates is 2 lines beyond the end of my file. In my demo project, it indicated line 177, but there's only 175 lines.

Is there any way to make this work? I'd like to be able to assign to Circle directly without going through any typecasts or checking in my for loop itself.

A: 

You can't do that directly in the enumerator. I'm afraid you're stuck with "is". The only way is indeed to create some helper enumerator. In your case try commenting out lines until you find the only that's making the compiler unhappy.

Sorry that's quite all I can suggest.

alex
Sorry, I was trying to use the helper enumerator (`FilteredEnumerator<T: TShape>`), I just didn't clarify that in my question. It's been updated. Is there something horribly wrong I'm doing in my helper?
afrazier
A: 

Hi,

You wrote the answer in your question :)

You just have to call the right function :

for Circle in ShapeList.FilteredEnumerator<TCircle> do
  //blah blah

for Square in ShapeList.FilteredEnumerator<TSquare> do
  //blah blah

One small remark about your code : you can dump your TShapeFilterFactory record, and simply add a method :

TShapeFilterEnumerator<T>.GetEnumerator : TShapeFilterEnumerator<T>;
begin
  Result := Self;
end;
LeGEC
Actually, I did try using `for Circle in ShapeList.FilteredEnumerator<TCircle>`. That's where the compiler error came from. I'll update the question to be more clear. That's what I get for rushing my question. :-)
afrazier
Thanks for the tip about eliminating the factory record too.
afrazier
+2  A: 

You don't show it here, but the problem probably lies in the implementation of GetCurrent.

While the compiler accepts something like

result := FShapeList[FIndex];

in the generic class, this will fail when the result class doesn't equal TShape, which is the case for TCircle and TSquare. That's why it works in the third loop.

Change the code to

result := T(FShapeList[FIndex]);

and you are fine.

The non-existing line number for the error is due to resolving the internal generic done by the compiler which doesn't map well to line numbers.

Uwe Raabe
:headdesk: That was exactly it. Thank you!
afrazier