Automate Restorable Operations with Custom Managed Records
Erik van Bilsen - August 3, 2020
Erik van Bilsen - August 3, 2020
[SHOWTOGROUPS=4,20,22]
There are those common try..finally blocks that you write over and over again. For example entering and leaving a critical section to protect resources from multiple threads, or calling BeginUpdate and EndUpdate to efficiently bulk-update a FireMonkey control. These kinds of “restorable” operations can be automated with the new Custom Managed Records feature introduced in Delphi 10.4.
About Custom Managed Records
If you are not yet familiar with Custom Managed Records (called CMRs from here on out), then take a look at my previous post about using CMRs to wrap C(++) APIs.
As a short recap, consider the following CMR:
Then Delphi will automatically call the Initialize operator when you declare such a record, and Finalize when the record goes out of scope. The Assign operator is called to copy one record to another one. You don’t have to use all 3 operators, but you need at least one to create a CMR.
Now, when you write the following code:
Then Delphi will convert this to the following code behind the scenes (note that this code does not compile though):
Delphi will automatically insert try..finally statements to make sure that the Finalize operator is always called. This is the key to automate “restorable” operations.
For more information about Custom Managed Records, take a look at the Для просмотра ссылки Войдиили Зарегистрируйся, my previous post or other resources on the internet.
The remainder of this post shows 3 ways you can use CMRs to automate restorable operations.
1. Automatic BeginUpdate and EndUpdate
Lets start with a simple example: All FireMonkey controls have BeginUpdate and EndUpdate methods that you should call to efficiently bulk-update a control. When methods like these come in pairs, you instinctively use a try..finally block to ensure that EndUpdate is always called:
Whenever you see patterns like this, there is a potential for automating this with a CMR:
Some notes:
You just create the auto-update CMR, which will immediately call BeginUpdate. Then when the CMR goes out of scope (at the end of the method), the EndUpdate method will automatically be called because of the hidden try..finally block and Finalize operator.
This CMR saves 4 lines of code every time you use it, which I think is pretty great (unless you get paid by the line of course).
CMRs work great with inline variables, as in this example. When you declare the AutoUpdate variable as a “normal” local variable, then the Initialize operator will be called as soon as you enter the method. When using an inline variable instead, the Initialize operator will be called at location where you declare the inline variable, which gives you more control.
[/SHOWTOGROUPS]
There are those common try..finally blocks that you write over and over again. For example entering and leaving a critical section to protect resources from multiple threads, or calling BeginUpdate and EndUpdate to efficiently bulk-update a FireMonkey control. These kinds of “restorable” operations can be automated with the new Custom Managed Records feature introduced in Delphi 10.4.
About Custom Managed Records
If you are not yet familiar with Custom Managed Records (called CMRs from here on out), then take a look at my previous post about using CMRs to wrap C(++) APIs.
As a short recap, consider the following CMR:
1 2 3 4 5 6 7 8 | type TFoo = record public class operator Initialize(out ADest: TFoo); class operator Finalize(var ADest: TFoo); class operator Assign(var ADest: TFoo; const [ref] ASrc: TFoo); end; |
Now, when you write the following code:
1 2 3 4 | begin var Foo1: TFoo; var Foo2 := Foo1; end; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | begin var Foo1, Foo2: TFoo; TFoo.Initialize(Foo1); try TFoo.Initialize(Foo2); try TFoo.Assign(Foo2, Foo1); finally TFoo.Finalize(Foo2); end; finally TFoo.Finalize(Foo1); end; end; |
For more information about Custom Managed Records, take a look at the Для просмотра ссылки Войди
The remainder of this post shows 3 ways you can use CMRs to automate restorable operations.
1. Automatic BeginUpdate and EndUpdate
Lets start with a simple example: All FireMonkey controls have BeginUpdate and EndUpdate methods that you should call to efficiently bulk-update a control. When methods like these come in pairs, you instinctively use a try..finally block to ensure that EndUpdate is always called:
1 2 3 4 5 6 7 | ListView.BeginUpdate; try ListView.Items.Add.Text := 'Item 1'; ListView.Items.Add.Text := 'Item 2'; finally ListView.EndUpdate; end; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | type TAutoUpdate = record private FControl: TControl; // Reference public constructor Create(const AControl: TControl); public class operator Initialize(out ADest: TAutoUpdate); class operator Finalize(var ADest: TAutoUpdate); class operator Assign(var ADest: TAutoUpdate; const [ref] ASrc: TAutoUpdate); end; constructor TAutoUpdate.Create(const AControl: TControl); begin Assert(Assigned(AControl)); FControl := AControl; FControl.BeginUpdate; end; class operator TAutoUpdate.Initialize(out ADest: TAutoUpdate); begin ADest.FControl := nil; end; class operator TAutoUpdate.Finalize(var ADest: TAutoUpdate); begin if (ADest.FControl <> nil) then ADest.FControl.EndUpdate; end; class operator TAutoUpdate.Assign(var ADest: TAutoUpdate; const [ref] ASrc: TAutoUpdate); begin raise EInvalidOperation.Create( 'TAutoUpdate records cannot be copied') end; |
- The Initialize operator just clears the reference to the control. This is a very common pattern that you see a lot with CMRs. Note that you don’t have to add this operator, but we discuss below why you should.
- You pass the control you want to auto-update to the constructor. The constructor immediately calls BeginUpdate on it.
- The Finalize operator is called in the finally section of the hidden try..finally block, so this is where you call EndUpdate. You have to check if the control is assigned first though, since it can be nil if the constructor hasn’t been called.
- And finally, these kinds of CMRs should not be copyable (which is explained below). So we raise an exception in the Assign operator if you try to copy it anyway.
1 2 3 | var AutoUpdate := TAutoUpdate.Create(ListView); ListView.Items.Add.Text := 'Item 1'; ListView.Items.Add.Text := 'Item 2'; |
This CMR saves 4 lines of code every time you use it, which I think is pretty great (unless you get paid by the line of course).
CMRs work great with inline variables, as in this example. When you declare the AutoUpdate variable as a “normal” local variable, then the Initialize operator will be called as soon as you enter the method. When using an inline variable instead, the Initialize operator will be called at location where you declare the inline variable, which gives you more control.
These kind of CMRs are very common in C++ (although they are not called CMR in C++). In C++, constructors and destructors are called automatically when an object/struct is declared on the stack. And C++ has copy constructors as well, which are similar to Assign operators in Delphi.
[/SHOWTOGROUPS]