Search code examples
delphidelphi-11-alexandria

Can I cast an Object as an Interface?


I would like to cast a generic object as an interface, but I get an "Operator not applicable to this operand type" (this code is part of a custom component where I attach different Entity classes to be opened by them).

procedure OpenNewEntity(EntityClass: TClass);
begin
  if not Supports(EntityClass, IEntity) then
    raise Exception.Create(EntityClass.ClassName + ' is not an Entity');

  (EntityClass.Create as IEntity).Open;
end;

I'm trying to use TClass instead of the real class that implements the interface because I don't want to add all the dependencies of that class to this unit.

Instead of TClass I have also tried to declare a class reference to classes implementing the interface, but it says "Class type required".

  TEntityClass = class of IEntity;

Can I use a new instance of an interface from a class that implements it without adding dependencies to those classes ?.


Solution

  • I guess that won't work this way in most cases, because that would always call TObject.Create and not any constructor declared in the EntityClass.

    A better approach would be to use Generics, which allows the compiler to catch any class that does not support IEntity:

    type
      IEntity = interface
        ['{1B885059-58E5-4475-883F-6CD04FA01D40}']
        procedure Open;
      end;
    
    type
      TEntityFactory = record
      public
        class function OpenNewEntity<T: constructor, IEntity>: IEntity; static;
      end;
    
    class function TEntityFactory.OpenNewEntity<T>: IEntity;
    begin
      Result := T.Create;
      Result.Open;
    end;
    

    The constraints put onto the Generic type T guarantees that the class has a parameterless constructor and supports IEntity.

    Update: As it turns out, you only have the class type in a variable, and thus cannot use the Generic approach. The tricky part is now to create the Entity instance in the appropriate way. Based on the code found in TJSONUnMarshal.ObjectInstance(), an implementation of your procedure could look like this:

    procedure OpenNewEntity(EntityClass: TClass);
    var
      ctx: TRttiContext;
      entity: IEntity;
      instance: TObject;
      rType: TRttiType;
      mType: TRTTIMethod;
      metaClass: TClass;
    begin
      ctx := TRttiContext.Create;
      rType := ctx.GetType(EntityClass);
      if rType <> nil then
        begin
        for mType in rType.GetMethods do
        begin
          if mType.HasExtendedInfo and mType.IsConstructor and (Length(mType.GetParameters) = 0) then
          begin
            // invoke
            metaClass := rType.AsInstance.MetaclassType;
            instance := mType.Invoke(metaClass, []).AsObject;
            if not Supports(instance, IEntity, entity) then
              raise Exception.Create(EntityClass.ClassName + ' is not an Entity');
            entity.Open;
            Exit;
          end;
        end;
        raise Exception.Create(EntityClass.ClassName + ' has no parameterless constructor');
      end;
      raise Exception.Create(EntityClass.ClassName + ' has no runtime type information');
    end;