Reputation: 21
I have been wondering how user defined Matlab classes as handle subclasses works. It seems, and also from what I read, that in some cases they reference to each other that might be unexpected or misleading and causing bugs. I have encountered this problem mainly during preallocation of these user defined classes. It seems to me, that a working solution might be using the approach of Btest, see below, however this solution is more nasty (as Atest or Ctest) make the code more clean and also it is more time costly. Lets look at the code first. Lets say I have four classes, each one is a subclass of Matlab handle class. Now pay attention that the first three classes have a property CXp3 that contains a scalar handle or an array of handles to the other consecutive classes. Also, the classes contain constructors, where this third property CXp3 is defined again (so that is the initialization during constructing the class, whereas the default initialization in properties is during the first compiling of that class).
classdef MyClass1 < handle
properties
C1p1(1,1) {mustBeNumeric};
C1p2 = 1;
C1p3(1,:) = MyClass2;
end
methods
function obj = MyClass1()
% MyClass1 constructor function
obj.C1p2 = 4;
obj.C1p3(1,:) = MyClass2;
end
end
end
classdef MyClass2 < handle
properties
C2p1(1,1) {mustBeNumeric};
C2p2 = 2;
C2p3(1,:) = MyClass3;
end
methods
function obj = MyClass2()
% MyClass2 constructor function
obj.C2p2 = 5;
obj.C2p3(1,:) = MyClass3;
end
function ChangeC2p3(obj,Number)
obj.C2p3(1,Number) = MyClass3;
end
end
end
classdef MyClass3 < handle
properties
C3p1(1,1) {mustBeNumeric};
C3p2 = 3;
C3p3(1,:) = MyClass4;
end
methods
function obj = MyClass3()
% MyClass3 constructor function
obj.C3p2 = 6;
obj.C3p3(1,:) = MyClass4;
end
function ChangeC3p3(obj,Number)
obj.C3p3(1,Number) = MyClass4;
end
end
end
classdef MyClass4 < handle
properties
C4p1(1,1) {mustBeNumeric};
C4p2 = 4;
end
methods
function obj = MyClass4()
% MyClass4 constructor function
obj.C4p2 = 8;
end
end
end
Now lets test their behaviour, and especially, when we want to create an array of the classes including the first class. Lets create an array of the first MyClass1
Num = 10000;
tic
Atest(1,Num) = MyClass1;
toc
clear A
tic
Btest(1,Num) = MyClass1;
for m = 1:1:Num
Btest(m) = MyClass1;
end
toc
tic
Ctest = repmat(MyClass1,1,Num);
toc
the answer to this is
Elapsed time is 0.036575 seconds.
Elapsed time is 1.618905 seconds.
Elapsed time is 0.002550 seconds.
Here it can been seen, that Btest took the longest, whereas the Atest or Ctest, that are more likely to be recomended by Matlab documentation, are much faster. However, the fun part starts once we start working with different array elements and with their nested properties. Lets say I want to use function ChangeC2p3(obj,Number)
of MyClass2
to the first element of Xtest array. I would suppose that it will change only the C2p3
property of the first element of Xtest(1), however it will change multiple. To track the changes, lets have a code:
MyNum = 10;
L1 = length(Atest(1).C1p3.C2p3);
L2 = length(Atest(2).C1p3.C2p3);
L3 = length(Atest(3).C1p3.C2p3);
L4 = length(Atest(end).C1p3.C2p3);
fprintf('\nThe length of Atest(X).C1p3.C2p3 is for X=1,2,3,end: %d, %d, %d, %d',L1,L2,L3,L4)
% now run the function
Atest(1).C1p3.ChangeC2p3(MyNum)
L1 = length(Atest(1).C1p3.C2p3);
L2 = length(Atest(2).C1p3.C2p3);
L3 = length(Atest(3).C1p3.C2p3);
L4 = length(Atest(end).C1p3.C2p3);
fprintf('\nThe length of Atest(X).C1p3.C2p3 is for X=1,2,3,end: %d, %d, %d, %d',L1,L2,L3,L4)
fprintf('\n')
L1 = length(Btest(1).C1p3.C2p3);
L2 = length(Btest(2).C1p3.C2p3);
L3 = length(Btest(3).C1p3.C2p3);
L4 = length(Btest(end).C1p3.C2p3);
fprintf('\nThe length of Btest(X).C1p3.C2p3 is for X=1,2,3,end: %d, %d, %d, %d',L1,L2,L3,L4)
% now run the function
Btest(1).C1p3.ChangeC2p3(MyNum)
L1 = length(Btest(1).C1p3.C2p3);
L2 = length(Btest(2).C1p3.C2p3);
L3 = length(Btest(3).C1p3.C2p3);
L4 = length(Btest(end).C1p3.C2p3);
fprintf('\nThe length of Btest(X).C1p3.C2p3 is for X=1,2,3,end: %d, %d, %d, %d',L1,L2,L3,L4)
fprintf('\n')
L1 = length(Ctest(1).C1p3.C2p3);
L2 = length(Ctest(2).C1p3.C2p3);
L3 = length(Ctest(3).C1p3.C2p3);
L4 = length(Ctest(end).C1p3.C2p3);
fprintf('\nThe length of Ctest(X).C1p3.C2p3 is for X=1,2,3,end: %d, %d, %d, %d',L1,L2,L3,L4)
% now run the function
Ctest(1).C1p3.ChangeC2p3(MyNum)
L1 = length(Ctest(1).C1p3.C2p3);
L2 = length(Ctest(2).C1p3.C2p3);
L3 = length(Ctest(3).C1p3.C2p3);
L4 = length(Ctest(end).C1p3.C2p3);
fprintf('\nThe length of Ctest(X).C1p3.C2p3 is for X=1,2,3,end: %d, %d, %d, %d',L1,L2,L3,L4)
fprintf('\n')
Where the lengths have changed to multiple elements of these arrays.
The length of Atest(X).C1p3.C2p3 is for X=1,2,3,end: 1, 1, 1, 1
The length of Atest(X).C1p3.C2p3 is for X=1,2,3,end: 10, 10, 10, 1
The length of Btest(X).C1p3.C2p3 is for X=1,2,3,end: 1, 1, 1, 1
The length of Btest(X).C1p3.C2p3 is for X=1,2,3,end: 10, 1, 1, 1
The length of Ctest(X).C1p3.C2p3 is for X=1,2,3,end: 1, 1, 1, 1
The length of Ctest(X).C1p3.C2p3 is for X=1,2,3,end: 10, 10, 10, 10
It can be also proved that the elements reference to each other when we change the value of the first element in Xtest(1) of its nested handle C3p1=10
by this code
N1 = Atest(1).C1p3.C2p3.C3p1;
N2 = Atest(2).C1p3.C2p3.C3p1;
N3 = Atest(3).C1p3.C2p3.C3p1;
N4 = Atest(end).C1p3.C2p3.C3p1;
fprintf('\nAtest Before: %d, %d, %d, %d',N1,N2,N3,N4)
N1 = Btest(1).C1p3.C2p3.C3p1;
N2 = Btest(2).C1p3.C2p3.C3p1;
N3 = Btest(3).C1p3.C2p3.C3p1;
N4 = Btest(end).C1p3.C2p3(end).C3p1;
fprintf('\nBtest Before: %d, %d, %d, %d',N1,N2,N3,N4)
N1 = Ctest(1).C1p3.C2p3.C3p1;
N2 = Ctest(2).C1p3.C2p3.C3p1;
N3 = Ctest(3).C1p3.C2p3.C3p1;
N4 = Ctest(end).C1p3.C2p3.C3p1;
fprintf('\nCtest Before: %d, %d, %d, %d',N1,N2,N3,N4)
fprintf('\n')
% now change the value of property a see the effect
Atest(1).C1p3.C2p3(1).C3p1 = 10;
N1 = Atest(1).C1p3.C2p3.C3p1;
N2 = Atest(2).C1p3.C2p3.C3p1;
N3 = Atest(3).C1p3.C2p3.C3p1;
N4 = Atest(end).C1p3.C2p3.C3p1;
fprintf('\nAtest After: %d, %d, %d, %d',N1,N2,N3,N4)
Btest(1).C1p3.C2p3(1).C3p1 = 10;
N1 = Btest(1).C1p3.C2p3.C3p1;
N2 = Btest(2).C1p3.C2p3.C3p1;
N3 = Btest(3).C1p3.C2p3.C3p1;
N4 = Btest(end).C1p3.C2p3.C3p1;
fprintf('\nAtest After: %d, %d, %d, %d',N1,N2,N3,N4)
Ctest(1).C1p3.C2p3(1).C3p1 = 10;
N1 = Ctest(1).C1p3.C2p3.C3p1;
N2 = Ctest(2).C1p3.C2p3.C3p1;
N3 = Ctest(3).C1p3.C2p3.C3p1;
N4 = Ctest(end).C1p3.C2p3.C3p1;
fprintf('\nCtest After: %d, %d, %d, %d',N1,N2,N3,N4)
fprintf('\n')
And we will get answer
Atest Before: 0, 0, 0, 0
Btest Before: 0, 0, 0, 0
Ctest Before: 0, 0, 0, 0
Atest After: 10, 10, 10, 0
Atest After: 10, 0, 0, 0
Ctest After: 10, 10, 10, 10
This is quite interesting as it can be seen, that Atest(1,Num) = MyClass1
has 1:(end-1) elements referencing to one instance of that class, all repmat(MyClass1,1,Num)
elements referencing only to one instance and only the Btest
referencing to separate instances (anyway that is what I wanted and would have been expected from the other two). Now interestingly enough, this behaviour applies also somehow to the nested class handles. Lets change the size of third layer classes of the first element of classes with
MyNum = 5;
L1 = length(Atest(1).C1p3.C2p3(1).C3p3);
L2 = length(Atest(2).C1p3.C2p3(1).C3p3);
L3 = length(Atest(3).C1p3.C2p3(1).C3p3);
L4 = length(Atest(end).C1p3.C2p3(1).C3p3);
fprintf('\nThe length of Atest(X).C1p3.C2p3(1).C3p3 is for X=1,2,3,end: %d, %d, %d, %d',L1,L2,L3,L4)
% now run the function
Atest(1).C1p3.C2p3(1).ChangeC3p3(MyNum)
L1 = length(Atest(1).C1p3.C2p3(1).C3p3);
L2 = length(Atest(2).C1p3.C2p3(1).C3p3);
L3 = length(Atest(3).C1p3.C2p3(1).C3p3);
L4 = length(Atest(end).C1p3.C2p3(1).C3p3);
fprintf('\nThe length of Atest(X).C1p3.C2p3(1).C3p3 is for X=1,2,3,end: %d, %d, %d, %d',L1,L2,L3,L4)
fprintf('\n')
L1 = length(Btest(1).C1p3.C2p3(1).C3p3);
L2 = length(Btest(2).C1p3.C2p3(1).C3p3);
L3 = length(Btest(3).C1p3.C2p3(1).C3p3);
L4 = length(Btest(end).C1p3.C2p3(1).C3p3);
fprintf('\nThe length of Btest(X).C1p3.C2p3(1).C3p3 is for X=1,2,3,end: %d, %d, %d, %d',L1,L2,L3,L4)
% now run the function
Btest(1).C1p3.C2p3(1).ChangeC3p3(MyNum)
L1 = length(Btest(1).C1p3.C2p3(1).C3p3);
L2 = length(Btest(2).C1p3.C2p3(1).C3p3);
L3 = length(Btest(3).C1p3.C2p3(1).C3p3);
L4 = length(Btest(end).C1p3.C2p3(1).C3p3);
fprintf('\nThe length of Btest(X).C1p3.C2p3(1).C3p3 is for X=1,2,3,end: %d, %d, %d, %d',L1,L2,L3,L4)
fprintf('\n')
L1 = length(Ctest(1).C1p3.C2p3(1).C3p3);
L2 = length(Ctest(2).C1p3.C2p3(1).C3p3);
L3 = length(Ctest(3).C1p3.C2p3(1).C3p3);
L4 = length(Ctest(end).C1p3.C2p3(1).C3p3);
fprintf('\nThe length of Ctest(X).C1p3.C2p3(1).C3p3 is for X=1,2,3,end: %d, %d, %d, %d',L1,L2,L3,L4)
% now run the function
Ctest(1).C1p3.C2p3(1).ChangeC3p3(MyNum)
L1 = length(Ctest(1).C1p3.C2p3(1).C3p3);
L2 = length(Ctest(2).C1p3.C2p3(1).C3p3);
L3 = length(Ctest(3).C1p3.C2p3(1).C3p3);
L4 = length(Ctest(end).C1p3.C2p3(1).C3p3);
fprintf('\nThe length of Ctest(X).C1p3.C2p3(1).C3p3 is for X=1,2,3,end: %d, %d, %d, %d',L1,L2,L3,L4)
fprintf('\n')
The referencing will behave the same between Xtest(X) elements.
The length of Atest(X).C1p3.C2p3(1).C3p3 is for X=1,2,3,end: 1, 1, 1, 1
The length of Atest(X).C1p3.C2p3(1).C3p3 is for X=1,2,3,end: 5, 5, 5, 1
The length of Btest(X).C1p3.C2p3(1).C3p3 is for X=1,2,3,end: 1, 1, 1, 1
The length of Btest(X).C1p3.C2p3(1).C3p3 is for X=1,2,3,end: 5, 1, 1, 1
The length of Ctest(X).C1p3.C2p3(1).C3p3 is for X=1,2,3,end: 1, 1, 1, 1
The length of Ctest(X).C1p3.C2p3(1).C3p3 is for X=1,2,3,end: 5, 5, 5, 5
However, for the one element of the first layer class, then the third layer property CP3p3
after using ChangeC3p3
in the array reference each to a seprate class instance. The code:
MyNum = 7;
L1 = length(Atest(1).C1p3.C2p3(1).C3p3);
L2 = length(Atest(1).C1p3.C2p3(2).C3p3);
L3 = length(Atest(1).C1p3.C2p3(3).C3p3);
L4 = length(Atest(1).C1p3.C2p3(end).C3p3);
fprintf('\nThe length of Atest(1).C1p3.C2p3(X).C3p3 is for X=1,2,3,end: %d, %d, %d, %d',L1,L2,L3,L4)
% now run the function
Atest(1).C1p3.C2p3(1).ChangeC3p3(MyNum)
L1 = length(Atest(1).C1p3.C2p3(1).C3p3);
L2 = length(Atest(1).C1p3.C2p3(2).C3p3);
L3 = length(Atest(1).C1p3.C2p3(3).C3p3);
L4 = length(Atest(1).C1p3.C2p3(end).C3p3);
fprintf('\nThe length of Atest(1).C1p3.C2p3(X).C3p3 is for X=1,2,3,end: %d, %d, %d, %d',L1,L2,L3,L4)
fprintf('\n')
L1 = length(Btest(1).C1p3.C2p3(1).C3p3);
L2 = length(Btest(1).C1p3.C2p3(2).C3p3);
L3 = length(Btest(1).C1p3.C2p3(3).C3p3);
L4 = length(Btest(1).C1p3.C2p3(end).C3p3);
fprintf('\nThe length of Btest(1).C1p3.C2p3(X).C3p3 is for X=1,2,3,end: %d, %d, %d, %d',L1,L2,L3,L4)
% now run the function
Btest(1).C1p3.C2p3(1).ChangeC3p3(MyNum)
L1 = length(Btest(1).C1p3.C2p3(1).C3p3);
L2 = length(Btest(1).C1p3.C2p3(2).C3p3);
L3 = length(Btest(1).C1p3.C2p3(3).C3p3);
L4 = length(Btest(1).C1p3.C2p3(end).C3p3);
fprintf('\nThe length of Btest(1).C1p3.C2p3(X).C3p3 is for X=1,2,3,end: %d, %d, %d, %d',L1,L2,L3,L4)
fprintf('\n')
L1 = length(Ctest(1).C1p3.C2p3(1).C3p3);
L2 = length(Ctest(1).C1p3.C2p3(2).C3p3);
L3 = length(Ctest(1).C1p3.C2p3(3).C3p3);
L4 = length(Ctest(1).C1p3.C2p3(end).C3p3);
fprintf('\nThe length of Ctest(1).C1p3.C2p3(X).C3p3 is for X=1,2,3,end: %d, %d, %d, %d',L1,L2,L3,L4)
% now run the function
Ctest(1).C1p3.C2p3(1).ChangeC3p3(MyNum)
L1 = length(Ctest(1).C1p3.C2p3(1).C3p3);
L2 = length(Ctest(1).C1p3.C2p3(2).C3p3);
L3 = length(Ctest(1).C1p3.C2p3(3).C3p3);
L4 = length(Ctest(1).C1p3.C2p3(end).C3p3);
fprintf('\nThe length of Ctest(1).C1p3.C2p3(X).C3p3 is for X=1,2,3,end: %d, %d, %d, %d',L1,L2,L3,L4)
fprintf('\n')
With the answer
The length of Atest(1).C1p3.C2p3(X).C3p3 is for X=1,2,3,end: 5, 1, 1, 1
The length of Atest(1).C1p3.C2p3(X).C3p3 is for X=1,2,3,end: 7, 1, 1, 1
The length of Btest(1).C1p3.C2p3(X).C3p3 is for X=1,2,3,end: 5, 1, 1, 1
The length of Btest(1).C1p3.C2p3(X).C3p3 is for X=1,2,3,end: 7, 1, 1, 1
The length of Ctest(1).C1p3.C2p3(X).C3p3 is for X=1,2,3,end: 5, 1, 1, 1
The length of Ctest(1).C1p3.C2p3(X).C3p3 is for X=1,2,3,end: 7, 1, 1, 1
The exmplanation is probably quite long and the question could have been shortened, however I tought that it could propably help others to get better understanidng and it also points to possible incorrect referencing. At this point I would rather weclome a comment on how one should preallocate an array of Matlab handles classes so, that each will be pointing to a separate instance of that class and also all its nested properties will be poinitng to a separate objects. From what I tried it seems that the Btest approach will do the work, however the preallocation took the longest and the code is not that clean, so the question might be, if there is a better way to do it?
Anyway some topics that might be of interest here are here-stackoverflow or here-undocumentedmatlab or here-undocumentedmatlab.
Because I am quite new to OOP I might also not clearly understand some things about constructor etc. The interesting thing for exampele is, that if all the constructor function will be commented out, the answer to the code will be completly different and it seems that it might be equal to Ctest = repmat(MyClass1,1,Num);
approach. Another thing I am not sure about, whether in constructor function has any role the bracket (1,:) as obj.C3p3(1,:) = MyClass4;
. It seems it doesnt. Or for example if creating the object shoudl be rather A = MyClass1
or A = MyClass1()
? And whether the dimensions in property definition C1p3(1,:) = MyClass2;
plays any role with what is later in constructor function. And one more comment, the problem cannot be solved by haveing C1p3
insted of C1p3(1,:) = MyClass2;
in properties and later on having obj.C1p3(1,:) = MyClass2;
in constructor, because then the Conversion to double from MyClass2 is not possible.
error will occure.
Upvotes: 1
Views: 191
Reputation: 21
Respond to Comment:
First of all, thank @Cris Luengo for his comments. I have tried to compare the speed between value class and handle class and I think it is not actually that bad, however by manually optimizing the code (saying the interpreter that this is a pointer and this is a value) you might get some speed advantage of about 30 %. But still, for some small cases the speeds are quite comparable. To test it I created two classes (one value, one handle), each having a property 2 of some size, and then call a function of its own another number of times.
classdef MyClass5
properties
C5p1(1,:) {mustBeNumeric};
C5p2(:,:) {mustBeNumeric};
end
methods
function obj = MyClass5(val)
% MyClass5 constructor function
obj.C5p2 = rand(val,val);
end
function obj = ChangeC5p1(obj,Number)
obj.C5p1 = 1:1:Number;
end
end
end
classdef MyClass6 < handle
properties
C6p1(1,:) {mustBeNumeric};
C6p2(:,:) {mustBeNumeric};
end
methods
function obj = MyClass6(val)
% MyClass6 constructor function
obj.C6p2 = rand(val,val);
end
function ChangeC6p1(obj,Number)
obj.C6p1 = 1:1:Number;
end
end
end
NumberRepetition = 200000;
ArrayLength = 100;
Arr2Length = 5000;
A = MyClass5(Arr2Length);
B = MyClass6(Arr2Length);
fprintf('\n The value class speed:')
tic
for m = 1:1:NumberRepetition
A = A.ChangeC5p1(ArrayLength);
end
toc
fprintf('\n The handle class speed:')
tic
for m = 1:1:NumberRepetition
B.ChangeC6p1(ArrayLength);
end
toc
And for the setting above, the answer is
The value class speed:Elapsed time is 2.362391 seconds.
The handle class speed:Elapsed time is 1.804626 seconds.
Have a Matlab 2020a version on now quite older i3 (~2017 CPU). If I guess correctly the second property has size of about 5000x5000*64 bits, that is about 190 MBs data held by the object. Interestinglly if the NumberRepetition
is about the same size as ArrayLength
the times become quite even. It is probably not the best way to compare the speeds, but it is true that I would be expecting worse results.
So it might still seem, that one way to avoid problem descirbed in the original post would be to use value class only with possible small speed downbreak, or try to handle the behaviour by using the Btest allocation approach every time an array of objects are created.
Upvotes: 0