2

Very un-snappy title I know.

I have a series of text lines that I need to perform certain operations on in a certain order. I have come up with a means of doing this by defining the following record structure:

TProcessOrderRecord = record
  RecordTypes:       TByteSet;
  InitialiseProcedure: TPreScanProc;
  ProcessProcedure:    TProcessRecord;
  FinaliseProcedure:   TEndScanProc;
end;

AProcessOrderArray = array of TProcessOrderRecord;

Initialise tends to call a constructor which will fill a field in the host object.

Process will be a procedure on the object which will be called for each text line that matches one of the record types in RecordTypes.

Finalise will tend to call the destructor and possibly do any checks when it knows that the full set of records has been processed.

The means of processing this array is quite straightforward:

procedure TImport.ScanTransferFile;
var
  i: integer;
  lArrayToProcess: AProcessOrderArray;
begin
  lArrayToProcess := SetUpProcessingOrder(NLPGApp.ImportType);
  for i := low(lArrayToProcess) to high(lArrayToProcess) do
  begin
    ProcessRecordType(lArrayToProcess[i].RecordTypes,     lArrayToProcess[i].InitialiseProcedure, lArrayToProcess[i].ProcessProcedure, lArrayToProcess[i].FinaliseProcedure);
  end;
end;

procedure TImport.ProcessRecordType(const RecordTypesToFind: TByteSet; PreScanProcedure: TPreScanProc; OnFindRecord: TProcessRecord; OnCompleteScan: TEndScanProc);
var
  lLineOfText: string;
  lIntegerRecordID: byte;
begin
  if Assigned(PreScanProcedure) then PreScanProcedure;
  try
    if assigned(OnFindRecord) then
    begin
      Reader.GoToStartOfFile;
      while not Reader.EndOfFile do
      begin
        lLineOfText := Reader.ReadLine;
        lIntegerRecordID := StrToIntDef(GetRecordID(lLineOfText), 0);
        if lIntegerRecordID in RecordTypesToFind then
        begin
          try
            OnFindRecord(lLineOfText);
          except
            on E: MyAppException do
            begin
            // either raise to exit or log and carry on
            end;
          end;
        end;
      end;
    end;
  finally
    // OnCompleteScan usually contains calls to destructors, so ensure it's called
    if Assigned(OnCompleteScan) then OnCompleteScan;
  end;
end;

My problem is that I want to define a record as such:

RecordTypes = [10]
InitialiseProcedure = ProcToCreateFMyObj
ProcessProcedure = FMyObj.do
FinaliseProcedure = ProcToFreeFMyObj

This compiles fine, however when ProcessProcedure is called, as FMyObj was nil when the ProcessProcedure is set, the instance of TMyObj is nil even though FMyObj is now set. Is there any clean way to get the record to point to the instance of FMyObj at the time of calling rather than at the time of first assignment?

At present I have resorted to having 'caller' methods on the host object which can then call the FMyObj instance when needed, but this is creating quite a bloated object with lots of single-line methods.

Edit to clarify/complicate the problem

Sometimes one instance of FObj can handle more than one types of record (usually if they have a master-detail relationship). In this case, InitialiseProcedure of the first record type will create FObj, FinaliseProcedure of the second record will free FObj and each record's ProcessProcedure can reference different procedures of FObj (do1 and do2).

Matt Allwood
  • 1,448
  • 12
  • 25
  • Where is FMyObj declared? – jachguate Mar 06 '13 at 18:13
  • FMyObj is a member of class TImport. TImport exists at the time of setting (so FMyObj is a valid pointer at the time, just pointing to nil). – Matt Allwood Mar 07 '13 at 09:33
  • if `FMyObj` is in the `TImport` class, and the `ProcessRecordType` is a method of the same class, I fail to see why do you need to use method pointers, since you can just directly call FMyObj.Do anytime. What are you trying to accomplish? – jachguate Mar 07 '13 at 09:40
  • My AProcessOrderArray may contain several sets of methods each using their own (or maybe sharing) the object. I *could* do this with a massive CASE statement, but as I have potential for multiple configurations of AProcessOrderArrays, I preferred the idea of setting this up separately and having very simple calling procedures. I would really have liked declaring them as const arrays, but that was causing even more grief! – Matt Allwood Mar 07 '13 at 09:46
  • Do you have to use a record, or can you change it to use a class or interface? – jachguate Mar 07 '13 at 09:57
  • @jachguate Not forced to use any particular solution, just scouting to see whether there's something more elegant – Matt Allwood Mar 07 '13 at 10:49

2 Answers2

3

At present I have resorted to having 'caller' methods on the host object which can then call the FMyObj instance when needed, but this is creating quite a bloated object with lots of single-line methods.

That is the right solution. Since the instance is not available at the point of initialisation you have no alternative.

When you use of object you are defining something called a method pointer. When you assign to a variable of method pointer type, the instance is captured at the point of assignment. There is no mechanism for the instance associated with a method pointer to be dynamically resolved. The only way to achieve that is to use runtime delegation, which is what you are currently doing. As is so often the case, another layer of indirection is used to solve a problem!

Your record that contains a number of methods looks awfully like an interface. I suspect that the most elegant solution will involve an interface. Perhaps at the point of calling you can call a function that returns an interface. And that function will using the value of FMyObj at the time of calling to locate the appropriate interface.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • 1
    @serg why do you so? Looks on the face of it like tit for tat. – David Heffernan Mar 07 '13 at 07:25
  • @serg What makes you say that? What aspect of the answer makes it invalid? Do you have any substance behind your assertion? The final paragraph explains why it's not possible to do what the asker wants. Do I need to explain it better? – David Heffernan Mar 07 '13 at 07:44
  • I already given the answer. It is possible to do what the asker wants. The point that the answer is not a recommended solution does not matter. I mentioned it in my answer. – kludg Mar 07 '13 at 07:48
  • @serg No it's not possible. You misunderstand what is desired. The asker wants the method pointer to bind automatically to the object reference stored in FMyObject at the point of calling at runtime. That's what is meant by "Is there any clean way to get the record to point to the instance of FMyObj at the time of calling rather than at the time of first assignment?" I find your petty tit-for-tat behaviour disappointing. – David Heffernan Mar 07 '13 at 07:58
  • No, the asker wants to set different methods of an object as the record field before the object is created. He needs additional record initialization after the object is created. Binding at compile time is a nonsense and it was not asked. – kludg Mar 07 '13 at 08:06
  • @serg Binding at compile time? What are you talking about? Nobody said that. – David Heffernan Mar 07 '13 at 08:14
  • Whatever was said I have given the correct answer to the question how to bind object instance to a method-type variable or field. And that is what was asked. Any other interpretation of the question makes the question a nonsense. – kludg Mar 07 '13 at 08:25
  • @Serg Read the quote in my comment above. There's only one way to interpret "at the time of calling". Your answer would require FMyObj to be available at the point of calling. In which case you'd just call the method directly on that object reference. So it seems clear to me that you've mis-interpreted the question. – David Heffernan Mar 07 '13 at 08:29
  • David you are just stubborn and don't want to admit that you are wrong. Period. No wish to continue cause it makes no sense. – kludg Mar 07 '13 at 08:35
  • @Serg I regularly admit I am wrong. I correct or delete answers that are wrong. I am very susceptible to reasoned argument. I'd like to understand how you interpret "at the time of calling". You clearly interpret it differently from me. I don't understand how you are interpreting it. My interpretation is that it means the time of calling one of the `of object` method pointers in one of the `TProcessOrderRecord` instances. How do you interpret it? – David Heffernan Mar 07 '13 at 09:01
  • 1
    I upvoted both your answers: + 1 for David for mentioning Interface as this is the most sane solution and +1 for serg for answering the question. Like Serg already mentioned, he would never do this, but it is a valid answer... – whosrdaddy Mar 07 '13 at 10:21
  • While I like the idea of an interfaces, I think that problems with this not being 'virgin' code means that this will be awkward to implement. The `FMyObj's` tend to have differing construction requirements, so factory methods tend to be in `TImport` whereas the Do procedures tend to be in FMyObj (and indeed, some FMyObj's have different more than 1 type of `Do` procedure depending on the record type). Issues with splitting procedures over two objects and different procedure names would mean a significant amount of refactoring to come up with a solution. – Matt Allwood Mar 07 '13 at 10:58
  • Continued as out of space... Suppose `InitialiseProc` could be replaced by `InitialiseFunc : IProcessInterface` returning an interface of Process and Finalise methods which are then called in `ProcessRecordType` but would need to re-think how to handle different Process methods for the same `FMyObj` – Matt Allwood Mar 07 '13 at 11:00
  • If implementing objects have multiple versions of the *interface*, that makes it tricky to use interfaces. Did I interpret your question accurately? – David Heffernan Mar 07 '13 at 12:13
  • @David, I've extended the question to clarify. It's not impossible to handle with an interface, just that the Do procedure would have to handle forwarding the call to Do1 and Do2. What is a little more awkward is freeing the object after the 2nd set of records, as there is a need to persist the interface between calls. Admittedly, `FInt` is pretty much the same as `FObj`, but it spoils a bit of the neatness of reference counting – Matt Allwood Mar 07 '13 at 13:01
  • Marked this answer as accepted, as I think the best solution is buried in the comments. It'll be a bit more work than I want to do right now (never enough time!) but I'll make a note of it so it could be implemented in future – Matt Allwood Mar 07 '13 at 13:02
1

Yes it is possible to make additional runtime initialization of your record:

var
  A: TProcessOrderRecord;

begin
  ..
  TMethod(A.ProcessProcedure).Data:= FMyObj;
  ..
end;

though I would prefer a different solution, like the one you already use.

kludg
  • 27,213
  • 5
  • 67
  • 118
  • 1
    -1 This is never a good idea. If you have `FMyObj` at hand then you should just assign the entire method pointer. – David Heffernan Mar 06 '13 at 18:06
  • I agree that this should work (and I found a web article doing something similar before asking), however as you say I think another solution is preferred as it's not a well-known aspect of the Delphi language and while it would be alright if I was the only one ever to look at the code and it was fairly static this isn't going to be the case. – Matt Allwood Mar 07 '13 at 09:42
  • 2
    @MattAllwood Why would you do that? If you could do that, you could do this: `A.ProcessProcedure := FMyObj.ProcessProcedure`. – David Heffernan Mar 07 '13 at 10:08
  • @MattAllwood - I have once an idea of "splitting" Data and Code parts of a method to get a more compact solution but finally preferred a "bloated" solution as more clear and better maintainable - see http://stackoverflow.com/questions/9721396/passing-a-methods-code-as-an-argument-in-a-typesafe-way. – kludg Mar 07 '13 at 10:25
  • 1
    @David - agree on reflection that would be the same - either way kinda invalidates trying to set up the entire structure at the outset – Matt Allwood Mar 07 '13 at 10:47