Update: Information in this article relates to older versions of ILNumerics. For version 5 and later updated information is found here: https://ilnumerics.net/ClassRules.html.
A lot of people are confused about how to use ILArray as class member variables. The documentation is really sparse on this topic. So let’s get into it!
Take the following naive approach:
class Test {
ILArray<double> m_a;
public Test() {
using (ILScope.Enter()) {
m_a = ILMath.rand(100, 100);
}
}
public void Do() {
System.Diagnostics.Debug.WriteLine("m_a:" + m_a.ToString());
}
}
If we run this:
Test t = new Test();
t.Do();
… we get … an exception Why that?
ILNumerics Arrays as Class Attributes
We start with the rules and explain the reasons later.
- If an ILNumerics array is used as class member, it must be a local ILNumerics array: ILArray<T>
- Initialization of those types must utilize a special function: ILMath.localMember<T>
- Assignments to the local variable must utilize the .a property (.Assign() function in VB)
- Classes with local array members should implement the IDisposable interface.
- UPDATE: it is recommended to mark all ILArray local members as
readonly
By applying the rules 1..3, the corrected example displays:
class Test {
ILArray<double> m_a = ILMath.localMember<double>();
public Test() {
using (ILScope.Enter()) {
m_a.a = ILMath.rand(100,100);
}
}
public void Do() {
System.Diagnostics.Debug.WriteLine("m_a:" + m_a.ToString());
}
}
This time, we get, as expected:
m_a:<Double> [100,100]
0,50272 0,21398 0,66289 0,75169 0,64011 0,68948 0,67187 0,32454 0,75637 0,07517 0,70919 0,71990 0,90485 0,79115 0,06920 0,21873 0,10221 ...
0,73964 0,61959 0,60884 0,59152 0,27218 0,31629 0,97323 0,61203 0,31014 0,72146 0,55119 0,43210 0,13197 0,41965 0,48213 0,39704 0,68682 ...
0,41224 0,47684 0,33983 0,16917 0,11035 0,19571 0,28410 0,70209 0,36965 0,84124 0,13361 0,39570 0,56504 0,94230 0,70813 0,24816 0,86502 ...
0,85803 0,13391 0,87444 0,77514 0,78207 0,42969 0,16267 0,19860 0,32069 0,41191 0,19634 0,14786 0,13823 0,55875 0,87828 0,98742 0,04404 ...
0,70365 0,52921 0,22790 0,34812 0,44606 0,96938 0,05116 0,84701 0,89024 0,73485 0,67458 0,26132 0,73829 0,10154 0,26001 0,60780 0,01866 ...
...
If you came to this post while looking for a short solution to an actual problem, you may stop reading here. The scheme will work out fine, if the rules above are blindly followed. However, for the interested user, we’ll dive into the dirty details next.
Some unimportant Details
Now, let’s inspect the reasons behind. They are somehow complex and most users can silently ignore them. But here they are:
The first rule is easy. Why should one use anything else than a local array? So lets step to rule two:
- Initialization of those types must utilize a special function: ILMath.localMember<T>
A fundamental mechanism of the ILNumerics memory management is related to the associated livetime of certain array types. All functions return temporary arrays (ILRetArray<T>) which do only live for exactly one use. After the first use, they get disposed off automatically. In order to make use of such arrays multiple times, one needs to assign them to a local variable. This is the place, where they get converted and the underlying storage is taken for the local, persistent array variable.
At the same time, we need to make sure, the array is released after the current ILNumerics scope (using (ILScope.Enter())) { … }) was left. Thereforem the conversion to a local array is used. During the conversion, since we know, there is going to be a new array out there, we track the new array for later disposal in the current scope.
If the scope is left, it does exactly what it promises: it disposes off all arrays created since its creation. Now, local array members require a different behavior. They commonly live for the livetime of the class – not of the current ILNumerics scope. In order to prevent the local array to get cleaned up after the scope in the constructor body was left, we need something else.
The ILMath.localMember() function is the only exception to the rule. It is the only function, which does not return a temporary array, but a local array. In fact, the function is more than simple. All it does, is to create a new ILArray<T> and return that. Since bothe types of both sides of the assignment match, no conversion is necessary and the new array is not registered in the current scope, hence it is not disposed off – just what we need!
What, if we have to assign the return value from any function to the local array? Here, the next rule jumps in:
- Assignments to the local variable must utilize the .a property (.Assign() function in VB)
Assigning to a local array directly would activate the disposal mechanism described above. Hence, in order to prevent this for a longer living class attribute, one needs to assign to the variable via the .a property. In Visual Basic, the .Assign() function does the same. This will prevent the array from getting registered into the scope.
Example ILNumerics Array Utilization Class
Now, that we archieved to prevent our local array attribute from getting disposed off magically, we – for the sake of completeness – should make sure, it gets disposed somewhere. The recommended way of disposing off things in .NET is … the IDisposal interface. In fact, for most scenarios, IDisposal is not necessary. The array would freed, once the application is shut down. But we recommend implementing IDisposable, since it makes a lot of things more consistent and error safe. However, we provide the IDisposable interface for convenience reasons only – we do not rely on it like we would for the disposal of unmanaged ressources. Therefore, a simplified version is sufficient here and we can omit the finalizer method for the class.
Here comes the full test class example, having all rules implemented:
class Test : IDisposable {
// declare local array attribute as ILArray<T>,
// initialize with ILMath.localMember<T>()!
readonly ILArray<double> m_a = ILMath.localMember<double>();
public Test() {
using (ILScope.Enter()) {
// assign via .a property only!
m_a.a = ILMath.rand(100,100);
}
}
public void Do() {
// assign via .a property only!
m_a.a = m_a + 2;
System.Diagnostics.Debug.WriteLine("m_a:" + m_a.ToString());
}
#region IDisposable Members
// implement IDisposable for the class for transparent
// clean up by the user of the class. This is for con-
// venience only. No harm is done by ommitting the
// call to Dispose().
public void Dispose() {
// simplified disposal pattern: we allow
// calling dispose multiple times or not at all.
if (!ILMath.isnull(m_a)) {
m_a.Dispose();
}
}
#endregion
}
For the user of your class, this brings one big advantage: she can – without knowing the details – clean up its storage easily.
using (Test t = new Test()) {
t.Do();
}
@UPDATE: by declaring your ILArray members as readonly
one gains the convenience that the compiler will prevent you from accidentally assigning to the member somewhere in the code. The other rules must still be fullfilled. But by only using readonly ILArray<T>
the rest is almost automatically.
ILArray, Properties and Lazy Initialization
@UPDATE2: Another common usage pattern for local class attributes is to delay the initialization to the first use. Let’s say, an attribute requires costly computations but is not needed always. One would usually create a property and compute the attribute value only in the get accessor:
class Class {
// attribute, initialization is done in the property get accessor
Tuple<int> m_a;
public Tuple<int> A {
get {
if (m_a == null) {
m_a = Tuple.Create(1); // your costly initialization here
}
return m_a;
}
set { m_a = value }
}
}
How does this scheme go along with ILNumerics’ ILArray? Pretty well:
class Class1 : ILMath, IDisposable {
readonly ILArray<double> m_a = localMember<double>();
public ILRetArray<double> A {
get {
if (isempty(m_a)) {
m_a.a = rand(1000, 2000); // your costly initialization here
}
return m_a; // this will only return a lazy copy to the caller!
}
set {
m_a.a = value;
}
}
public void Dispose() {
// ... common dispose implementation
}
}
Instead of checking for null in the get accessor, we simply check for an empty array. Alternatively you may initialize the attribute with some marking value in the constructor. NaN, MinValue, 0 might be good candidates.