Suppose we have 2 classes located in different units:

  • TMaster is the owner of a dog who should know about her
  • TDog - a dog that needs to know about its owner

Unit_Master:

 uses Unit_Dog; type TMaster = class public Dog: TDog; end; 

Unit_Dog:

 uses Unit_Master; type TDog = class public Master: TMaster; end; 

Using such direct links between classes would make life much easier. Unfortunately, Delphi cannot compile such code due to the cyclical dependency between units.

How to correctly fix circular dependency and compile such code, what are the ways?

  • didn't you think of having a common ancestor like TEntity for these classes? And for references to the owner / subordinate to use it. Where necessary, it is necessary to explicitly cast the type. - kami
  • @kami Now I use this option, but it is inconvenient. I would like to know about alternatives (in addition to castes, helpers and merging into one file) - Kromster
  • cyclic dependency in the interface section is in any case forbidden. You can do it via interfaces and use IMaster / IDog fields, but it is easier to combine them into one module. - kami
  • @kami sketched 4 options, are there any others? - Kromster

5 answers 5

You can use interfaces and store references to them in classes, and not to objects:

 unit EntityInterfaces type IMaster = interface; IDog = interface; IMaster = interface ['{8417E5A8-02FE-4A83-BD0C-F69E79492796}'] function GetDog: IDog; procedure SetDog(const ADog: IDog); property Dog: IDog read GetDog write SetDog; end; IDog = interface ['{9B501AD7-BD77-46AC-BC08-545433EC5FFE}'] function GetMaster: IMaster; procedure SetMaster(const AMaster: IMaster); property Master: IMaster read GetMaster write SetMaster; end; 

Classes themselves in this case are declared like this:

  TMaster = class(TInterfacedObject, IMaster) private [Weak] FDog: IDog; function GetDog: IDog; procedure SetDog(const ADog: IDog); end; TDog = class(TInterfacedObject, IDog) private [Weak] FMaster: IMaster; function GetMaster: IMaster; procedure SetMaster(const AMaster: IMaster); end; 

And work is already underway with the interfaces:

 procedure TForm11.btn1Click(Sender: TObject); var Master: IMaster; Dog: IDog; begin Master := TMaster.Create; Dog := TDog.Create; Master.Dog := Dog; Dog.Master := Master; end; // на выходе из метода Master и Dog будут автоматически удалены, // поскольку их "циклические ссылки" друг на друга имеют атрибут Weak // без использования слабых ссылок возникла бы утечка памяти. 

The disadvantages of the method:

the need to twice describe the methods - in the interface and directly in the implementation.

Auto-deletion of an object when bringing the reference count to zero will be unusual for those who have not worked with interfaces.


To eliminate the last drawback, you can make your own IUnknown implementation and inherit your classes not from TInterfacedObject, but from TNoReferenceObject:

 type TNoReferenceObject = class(TObject, IInterface) { IInterface } function QueryInterface(const IID: TGUID; out Obj): HResult; virtual; stdcall; function _AddRef: Integer; stdcall; function _Release: Integer; stdcall; end; { TNoReferenceObject } function TNoReferenceObject.QueryInterface(const IID: TGUID; out Obj): HResult; begin if GetInterface(IID, Obj) then Result := 0 else Result := E_NOINTERFACE; end; function TNoReferenceObject._AddRef: Integer; begin Result := -1; end; function TNoReferenceObject._Release: Integer; begin Result := -1; end; 

Reference counting for all TNoReferenceObject heirs will not work and all created instances will need to be deleted manually, as well as ordinary objects:

 procedure TForm11.btn1Click(Sender: TObject); var Master: IMaster; Dog: IDog; begin Master := TMaster.Create; Dog := TDog.Create; try Master.Dog := Dog; Dog.Master := Master; finally TObject(Master).Free; TObject(Dog).Free; end; end; 
  • Quite a difficult option. By the way, is it not easier then in the 3rd unit to declare 2 abstract classes and inherit from them in 1 and 2 units? At least you don’t have to worry about the reference counter. - Kromster
  • @Kromster yes, it is difficult. Actually, he wrote the answer only because of his commentary on the question. Abstract classes ... in this case it will be almost an exact copy of answers №2 and 3 - kami
  • @kami Auto-deleting objects is not a disadvantage, but an advantage. But to use a hack with TNoReferenceObject generally not worth it, because This is some kind of wild crutch. - zed
  • @zed - if a person did not work with interfaces, then for him this would be a huge disadvantage. He will most likely announce Master: TMaster and will spit for a long time, why after Master.Free comes out of AV. Well, about the "wild crutch" - based on this approach VCL and FMX, starting with TComponent in both frameworks. So there is little wild here. Yes, and kostulnosti either. - kami
  • 2
    It’s worth adding from which version [Weak] starts to work, because the compiler in the lower versions does not warn about anything, but just ignores it silently and you think that everything is fine) in XP, for example, it is not taken into account. - Isaev

Option number 1

Combine both classes into 1 file and use forward declaration :

Unit:

 type TDog = class; // т.н. forward declaration TMaster = class public Dog: TDog; end; TDog = class public Master: TMaster; end; 

The disadvantages are obvious - we get one huge file with a bunch of code.

  • Using one file has another side effect that is often forgotten - classes become friendly, i.e. can directly access each other’s private fields and methods. Sometimes it is also a pole. - German Borisov
  • one
    @HermanBorisov Thanks for the addition! Although IMHO from the friendliness just need to try to leave, in favor of clearer interfaces and "black boxes". - Kromster
  • one
    @Kromster Since your classes link to each other, everything is already bad for you and if you combine them into one unit, it will not get much worse. But it is better to break the head over architecture and try to break this dependence. - zed
  • @zed write a response showing how to “tear” the dog from the owner with a minimum of labor costs? - Kromster
  • 2
    @HermanBorisov if this is an undesirable side effect, then you can get rid of it in new versions of Delphi using strict private or strict protected - Alekcvp

Option number 2

Create a common ancestor with common methods:

Unit_Entity:

 type TEntity = class end; 

Unit_Dog:

 uses Unit_Entity; type TMaster = class(TEntity) public Dog: TEntity; end; 

Unit_Dog:

 uses Unit_Entity; type TDog = class(TEntity) public Master: TEntity; end; 

The disadvantages are that all the code specific for each class still remains in each class and is not available without additional solutions.

    Option number 3

    Use a common ancestor and cast types when referring to objects:

    Unit_Master:

     type TMaster = class public Dog: TObject; end; implementation uses Unit_Dog; .. TDog(Dog).Bark; 

    Unit_Dog:

     type TDog = class public Master: TObject; end; implementation uses Unit_Master; .. TMaster(Master).Yell; 

    The downsides are that with every call we will have to lead the object to a specific class. With a large number of hits, it clutters the code.

    • In this way there is such an opportunity? TMaster.Dog.Master.Dog.Master ... It will not create problems? - ArtGrek13
    • one
      @ ArtGrek13 This possibility is in all ways. There is no problem in this, unless you yourself write clearly meaningless code like newMaster.Dog.Master.Dog.Master.Dog.Master ... - Kromster

    Option number 4

    Use class assistants.

    (The private method officially stopped working with Delphi Seattle (but there are ways to get around: https://stackoverflow.com/questions/37351215 ) and you can probably switch to protected )

    Unit_Helper:

     uses Unit_Master, Unit_Dog; type TMasterHelper = class helper for TMaster private function GetDog: TDog; procedure SetDog(aObject: TDog); public property Dog: TDog read GetDog write SetDog; end; TDogHelper = class helper for TDog private function GetMaster: TMaster; procedure SetMaster(aObject: TMaster); public property Master: TMaster read GetMaster write SetMaster; end; function TMasterHelper.GetDog: TDog; begin Result := fDog; end; 

    Unit_Master:

     type TMaster = class protected fDog: TObject; end; uses Unit_Helper; .. Dog.Bark; 

    Unit_Dog:

     type TDog = class protected fMaster: TObject; end; uses Unit_Helper; .. Master.Yell; 

    The downsides are that a class can have a maximum of 1 assistant, therefore, this trick can only be used once.

    • Unit_Dog: in the first case - a typo - Isaev
    • @isaev, sure, thanks! - Kromster
    • and how in this case will the getter and setter look like? - Isaev
    • one
      @Isaev added. Including with clarifications on Seattle - Kromster