Issues with TCollection.Add and polymorphic objects creation lazarus / 62-General ---------------------------------------------------------------------- speleomania · 2017-02-02 22:16 Need some advice with the following design concept: Assuming this nested structure: TCountry = class; TCities = class(TCollection) TCity = class(TCollectionItem); TBuildings = class(TCollection); TBuilding = class(TCollectionItem); TRooms = class(TCollection); TRoom = class(TCollectionItem); and so on The top level is a var of TCountry and TCountry contains a collection of cities, each city has a collection of buildings and so on Now - I'm developing a framework and id like my users to be able to use it with their own versions of any of my classes. So for example I need to be able to do TBedRoom = class(TRoom) and the framework should be able to work fine with a collection of bedrooms instead of rooms Issue No 1: What is the best way to implement the top level TCountry constructor so that all particular custom classes are passed as parameters upon creation? I don't want users to have to worry about the structure, For example is this a good solution: TBedRoom = class(TRoom) TBedRoomClass = class of TBedRoom MyCountry := TCountry.Create(TBedRoomClass) This class reference should be passed down the chain until it gets to the TRooms Collection and used there to generate the specific collection item types Issue No 2: Is it possible to implement a TRoom constructor like this constructor TRoom.Create(ARooms: TRooms); The compiler doesn't seem to like this TCollection has a method function Add which builds the child objects by calling their Create constructor. Looking at the source code I'm puzzled by the fact that neither the constructor nor the function Add are virtual. Assume that the TRoom has a constructor with a different no of parameters than the parent TCollectionItem class for example constructor TRoom.Create(ACollection: TCollection; AColor: TColor); or ideally a user class: constructor TBedRoom.Create(ARooms:TRooms; AColor: TColor); How do I make sure that the polymorphic constructor is called by the TCollection.Add function so that I end up with objects of the proper class and with the proper color as well Hopefully all this makes sense if not let me know and ill try to do a better job explaining it ---------------------------------------------------------------------- wp · 2017-02-02 22:31 All usages of TCollection that I have seen do not allow creation of polymorphic collection items. This is because the items are created by the Add method of the collection which takes the class to be used from the parameter specified by the constructor of the collection, i.e. all collection items are instances of the same class. Why don't you use a TList, TObjectlist or similar? They store only pointers or objects. Here you explicitely create the items by yourself as instances of any class that you need and then you "Add" these now existing items to the list. ---------------------------------------------------------------------- Akira1364 · 2017-02-05 02:25 This is what generics are for! Here's a quick example I whipped up, hopefully it's of use to you. Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i unit PolymorphicGenericsExample; {$mode objfpc}{$H+} interface uses  Classes, SysUtils, Forms, Dialogs, FGL; type   TGeographicalLocation = class  private    FName: String;  public    constructor Create(constref AName: String);    property Name: String read FName write FName;  end;   TCountry = class(TGeographicalLocation)  end;   TProvince = class(TGeographicalLocation)  end;   TCity = class(TGeographicalLocation)  end;   TBuilding = class(TGeographicalLocation)  end;   TLocationList = specialize TFPGObjectList;   TForm1 = class(TForm)    procedure FormCreate(Sender: TObject);    procedure FormShow(Sender: TObject);    procedure FormDestroy(Sender: TObject);  end; var  Form1: TForm1;  ALocationArray: array[0..3] of TGeographicalLocation;  ALocationList: TLocationList; implementation {$R *.lfm} constructor TGeographicalLocation.Create(constref AName: String);begin  inherited Create;  FName := AName;end; procedure TForm1.FormCreate(Sender: TObject);var  I: Integer;begin  {There's no real reason to declare a 'class of TGeographicalLocation' type, unless you specifically need it.  Any descendant class will already be implicitly recognized as such, as shown below.}   ALocationArray[0] := TCountry.Create('Canada');  ALocationArray[1] := TProvince.Create('Ontario');  ALocationArray[2] := TCity.Create('Toronto');  ALocationArray[3] := TBuilding.Create('My House');  ALocationList := TLocationList.Create(True);   {The 'True' sets the FreeObjects property of ALocationList to true, meaning it will automatically  free anything inside of it when it is itself freed.}   for I := 0 to 3 do    ALocationList.Add(ALocationArray[I]);end; procedure TForm1.FormShow(Sender: TObject);var  I: Integer;begin  with ALocationList do    for I := 0 to 3 do      ShowMessage(Items[I].Name);   {The items property of ALocationList will always be viewed as type TGeographicalLocation, so no manual typecasting is required.  This also shows how even though the four items in ALocationList are technically members of different classes, they will all retain and  display the correct inherited name property that was initialized in their constructor.}end; procedure TForm1.FormDestroy(Sender: TObject);begin  ALocationList.Free;end; end. Edit: I should point out as well that you can get even more specific with the list specialization, as needed. For example, you could do something like this: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i type   {Now TLocationList is being declared as a full-on generic type, which can then be specialized further.}   generic TLocationList = class(specialize TFPGObjectList)  public  {You could add base-level methods that are relevant to all TGeographicalLocation descendants here.}  end;   TCountryList = class(specialize TLocationList)  public    {You could add methods that are relevant only to TCountry here. Of course, TCountryList will also inherit any methods declared in TLocationList.}  end; ---------------------------------------------------------------------- speleomania · 2017-02-06 18:03 Thanks for the responses! Akira: I'll need some time to figure out exactly what you're trying to tell me with your code, in the meantime 2 quick comments/questions: 1) The end user shouldn't need to do ALocationArray[2] := TCity.Create('Toronto'); This is managed internally by my unit, I don't want the end user to worry about creation and destruction of TCity objects and need for this to happen behind the scene 2) In addition - I need the end user to be able to supply their own classes - for example TTownHouse. Suppose that the TTownHouse constructor looks something like this constructor TTownHouse.Create(AName: string; AColor: TColor) I'm wondering how /if possible/ the user can specify the class type and any additional parameters that are needed for the custom constructor so that my unit can: 1 call the appropriate constructor and 2 supply any additional arguments for it guess I'm asking too much here ... ---------------------------------------------------------------------- Akira1364 · 2017-02-07 18:39 Quote from: speleomania on February 06, 2017, 07:03:24 pm1) The end user shouldn't need to do ALocationArray[2] := TCity.Create('Toronto'); Yes, instantiating an array simply to then add its contents to a list would be a terrible and inefficient idea in production. I was just trying to demonstrate how it isn't strictly necessary to use a "class of TSomeClass" type specification in most cases, when dealing with the creation of descendant classes. After re-reading what it is it sounds like you're trying to do, I came up with another little template that I think demonstrates how you could possibly go about it. I didn't have time to comment it like I did before, but have a look to see if you can get a grasp on what it's doing yourself, and let me know if you have any questions. Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i unit AnotherGenericsExample; {$mode objfpc}{$H+} interface uses  Classes, SysUtils, Forms, Dialogs, FGL; type  TLocation = class  end;   generic TLocationList = class(specialize TFPGObjectList)  public    procedure AddNewInstance;  end;   generic TLocationMap = class(specialize TFPGMapObject)  end;   TCity = class(TLocation)  end;   TBuilding = class(TLocation)  end;   TRoom = class(TLocation)  end;   generic TCityList = class(specialize TLocationList)  end;   generic TBuildingList = class(specialize TLocationList)  end;   generic TRoomList = class(specialize TLocationList)  end;   generic TCityToBuildingRelationshipMap = class(specialize TLocationMap);   generic TBuildingToRoomRelationshipMap = class(specialize TLocationMap);   generic TCountry = class  private type    FCityToBuildingMapType = specialize TCityToBuildingRelationshipMap;    FBuildingToRoomMapType = specialize TBuildingToRoomRelationshipMap;    FRoomListType = specialize TRoomList;    FBuildingListType = class(specialize TBuildingList)    public      Rooms: FRoomListType;    end;    FCityListType = class(specialize TCityList)    public      Buildings: FBuildingListType;    end;  public    Cities: FCityListType;    CityToBuildingLinks: FCityToBuildingMapType;    BuildingToRoomLinks: FBuildingToRoomMapType;    constructor Create;  destructor Destroy; override;  end;   TMyCustomCity = class(TCity)  end;   TMyCustomBuilding = class(TBuilding)  end;   TMyCustomRoom = class(TRoom)  end;   TMyCustomCountry = specialize TCountry;   TForm1 = class(TForm)    procedure FormCreate(Sender: TObject);    procedure FormShow(Sender: TObject);    procedure FormDestroy(Sender: TObject);  end; var  Form1: TForm1;  ACountry: TMyCustomCountry; implementation {$R *.lfm} procedure TLocationList.AddNewInstance;begin  inherited Add(T.Create);end; constructor TCountry.Create;begin  inherited Create;  Cities := FCityListType.Create(True);  Cities.Buildings := FBuildingListType.Create(True);  Cities.Buildings.Rooms := FRoomListType.Create(True);  CityToBuildingLinks := FCityToBuildingMapType.Create(False);  BuildingToRoomLinks := FBuildingToRoomMapType.Create(False);end; destructor TCountry.Destroy;begin  CityToBuildingLinks.Free;  BuildingToRoomLinks.Free;  Cities.Buildings.Rooms.Free;  Cities.Buildings.Free;  Cities.Free;  inherited Destroy;end; procedure TForm1.FormCreate(Sender: TObject);begin  ACountry := TMyCustomCountry.Create;  with ACountry do  begin    Cities.AddNewInstance;    Cities.Buildings.AddNewInstance;    Cities.Buildings.Rooms.AddNewInstance;  end;end; procedure TForm1.FormShow(Sender: TObject);begin  with ACountry do  begin    ShowMessage(Cities.First.ClassName);    ShowMessage(Cities.Buildings.First.ClassName);    ShowMessage(Cities.Buildings.Rooms.First.ClassName);  end;end; procedure TForm1.FormDestroy(Sender: TObject);begin  ACountry.Free;end; end. ---------------------------------------------------------------------- Remy Lebeau · 2017-02-08 21:11 Quote from: speleomania on February 02, 2017, 11:16:58 pmNow - I'm developing a framework and id like my users to be able to use it with their own versions of any of my classes. So for example I need to be able to do TBedRoom = class(TRoom) and the framework should be able to work fine with a collection of bedrooms instead of rooms A TCollection of TRoom objects can hold any object whose type is derived from TRoom. Quote from: speleomania on February 02, 2017, 11:16:58 pmIssue No 1: What is the best way to implement the top level TCountry constructor so that all particular custom classes are passed as parameters upon creation? I wouldn't implement this at the TCountry layer at all. Quote from: speleomania on February 02, 2017, 11:16:58 pmI don't want users to have to worry about the structure Why not?  What are you expecting them to do that is not related to structure? Quote from: speleomania on February 02, 2017, 11:16:58 pmFor example is this a good solution: TBedRoom = class(TRoom) TBedRoomClass = class of TBedRoom MyCountry := TCountry.Create(TBedRoomClass) Not really.  For one thing, that means every room in every building must be a bedroom.  What about bathrooms, or living rooms, or kitchen rooms? Plus, I don't think it is TCountry's responsibility to control the room types anyway.  That should be the responsibility of TRooms instead, based on the type of building it belongs to.  A residential house could have bedrooms, but an office building should have offices and conference rooms instead. You can have TRooms declare a constructor that does not expose a specific ItemType parameter, specifying TRoom when calling the base constructor, eg: Code: [Select]type   TRooms = class(TCollection)   private     FBuilding: TBuilding;   public     constructor Create(ABuilding: TBuilding); reintroduce;     property Building: TBuilding read FBuilding;   end; constructor TRooms.Create(ABuilding: TBuilding); begin   inherited Create(TRoom);   FBuilding := ABuilding; end; Then you can do this in TBuilding: Code: [Select]constructor TBuilding.Create(ACollection: TBuildings); begin   inherited Create(ACollection);   FRooms := TRooms.Create(Self); end; destructor TBuilding.Destroy; begin   FRooms.Free;   inherited; end; Follow this same pattern up the hierarchy until you reach TCountry. Or, if you want to be more restrictive, you can do something like this: Code: [Select]type   TRooms = class(TCollection)   private     FBuilding: TBuilding;   public     constructor Create(ABuilding: TBuilding, ARoomType: TRoomClass); reintroduce;     property Building: TBuilding read FBuilding;   end;   ...   TResidentialBuilding = class(TBuilding)   public     constructor Create(ACity: TCity); reintroduce;   end;   TResidentialRooms = class;   TResidentialRoom = class(TRoom)   public     constructor Create(ARooms: TResidentialRooms); reintroduce;   end;   TResidentialRooms = class(TRooms)   public     constructor Create(ABuilding: TResidentialBuilding); reintroduce;   end;   TBedRoom = class(TResidentialRoom)     ...   end;   ...   TOfficeBuilding = class(TBuilding)   public     constructor Create(ACity: TCity); reintroduce;   end;   TOfficeRooms = class;   TOfficeRoom = class(TRoom)   public     constructor Create(ARooms: TOfficeRooms); reintroduce;   end;   TOfficeRooms = class(TRooms)   public     constructor Create(ABuilding: TOfficeBuilding); reintroduce;   end;   TConferenceRoom = class(TOfficeRoom)     ...   end; ... constructor TRooms.Create(ABuilding: TBuilding, ARoomType: TRoomClass); begin   inherited Create(ARoomType);   FBuilding := ABuilding; end; constructor TResidentialBuilding.Create(ACity: TCity); begin   inherited Create(ACity);   FRooms := TResidentialRooms.Create(Self); end; constructor TResidentialRooms.Create(ABuilding: TResidentialBuilding); begin   inherited Create(ABuilding, TResidentialRoom); end; constructor TResidentialRoom.Create(ARooms: TResidentialRooms); begin   inherited Create(ARooms); end; constructor TOfficeBuilding.Create(ACity: TCity); begin   inherited Create(ACity);   FRooms := TOfficeRooms.Create(Self); end; constructor TOfficeRooms.Create(ABuilding: TOfficeBuilding); begin   inherited Create(ABuilding, TOfficeRoom); end; constructor TOfficeRoom.Create(ARooms: TOfficeRooms); begin   inherited Create(ARooms); end; Yes, it is more work.  But it gives you more control over what types are acceptable at each layer of the hierarchy. Quote from: speleomania on February 02, 2017, 11:16:58 pmIssue No 2: Is it possible to implement a TRoom constructor like this constructor TRoom.Create(ARooms: TRooms); Technically yes, especially if would declare this custom constructor as 'reintroduce'.  However, just know that this constructor won't be callable by TCollection.Add() since it does not override the existing virtual constructor that Add() calls.  But you can hide/reintroduce Add() to prevent plain TRoom objects from being added, if needed. Quote from: speleomania on February 02, 2017, 11:16:58 pmTCollection has a method function Add which builds the child objects by calling their Create constructor. Looking at the source code I'm puzzled by the fact that neither the constructor nor the function Add are virtual. TCollection's constructor and Add() method are not virtual, no.  Nor do they need to be.  TCollectionItem's constructor is virtual, thus allowing TCollection.Add() to create an instance of whatever class type is specified in the TCollection constructor. Quote from: speleomania on February 02, 2017, 11:16:58 pmAssume that the TRoom has a constructor with a different no of parameters than the parent TCollectionItem class for example constructor TRoom.Create(ACollection: TCollection; AColor: TColor); or ideally a user class: constructor TBedRoom.Create(ARooms:TRooms; AColor: TColor); How do I make sure that the polymorphic constructor is called by the TCollection.Add function so that I end up with objects of the proper class and with the proper color as well You can't.  TCollection.Add() knows nothing about your custom constructor at all, and so cannot call it.  It only knows about the virtual TCollectionItem constructor. However, you can call your custom constructor directly instead of calling Add(). Quote from: wp on February 02, 2017, 11:31:59 pmAll usages of TCollection that I have seen do not allow creation of polymorphic collection items. Indy does, for example.  The TIdMessage.MessageParts property is a standard TCollection (actually a TOwnedCollection) that can hold any number of TIdMessagePart descendants (TIdText, TIdAttachment, etc) at one time. Quote from: wp on February 02, 2017, 11:31:59 pmThis is because the items are created by the Add method of the collection which takes the class to be used from the parameter specified by the constructor of the collection, i.e. all collection items are instances of the same class. That is true for TCollection.Add() (and TCollection.Insert()), but those are not the only ways to add items into the collection.  You can just create an item class directly, passing the owning TCollection to its constructor.  As long as the new item is derived from the type that is specified in the TCollection constructor, this works just fine, eg: Code: [Select]type   TCity_NewYork = class(TCity)     ...   end;   TCity_LosAngeles = class(TCity)     ...   end;   TBuilding_EmpireState = class(TBuilding)     ...   end;   TBuilding_CapitolRecords = class(TBuilding)     ...   end; ... USA := TCountry.Create; ... NewYork := TCity_NewYork.Create(USA.Cities); TBuilding_EmpireState.Create(NewYork.Buildings); ... LosAngeles := TCity_LosAngeles.Create(USA.Cities); TBuilding_CapitolRecords.Create(LosAngeles.Buildings); ... Code: [Select]MyRoom := TBedRoom.Create(SomeBuilding.Rooms); And really, that is all TCollection.Add() (and TCollection.Insert()) is doing internally anyway (at least in Delphi, I have not checked FreePascal): Code: [Select]function TCollection.Add: TCollectionItem; begin   Result := FItemClass.Create(Self); // <-- HERE   Added(Result); end; function TCollection.Insert(Index: Integer): TCollectionItem; begin   Result := Add;   Result.Index := Index; end;