How do I create a property in a class that is a direct handle to another class object

93 vues (au cours des 30 derniers jours)
Currently I have two classes, a class that contains a structured definition, and another class that is supposed to fit that definition. The myDefinition class loads the definition from a data file.
Here is the classdef and are some example properties from the definition class:
classdef myDefinition
properties(constant)
lengthMin = 8;
lenthMax = 64;
scaleDefault = [1 1];
end
properties( GetAccess = public, SetAccess = immutable )
type;
definitionFile;
map;
elements = {};
end
methods
function obj = myDefinition (thistype, filenameDefinition)
elementTable = readtable(filenameDefinition);
%Loops through elements here...
end
end
end
The function is really not important, but for an overview, the constructor for myDefinition reads the file in the path specified by the string or char array in filenameDefintion (which is a csv file), reads it in with readtable, and loops through the rows to create the elements.
I then have a class which has to follow one of the definitions in a myDefinition class.
classdef myDefinedObject
properties ( Access = private )
mindatabits = 0;
maxdatabits = 64;
end
properties ( SetAccess = immutable, GetAccess = public )
mydef;
type;
name;
a;
x;
end
methods
function obj = myDefinedObject( num, thisDefinition, data )
if isa(thisDefinition, 'myDefinition')
obj.mydef = thisDefinition;
else
obj.mydef = [];
warning('No valid definition file provided');
end
%Based on the number input, gets name and other properties from
%the definition, then parses the data based on the properties
%from the lookup in the definition
end
end
end
I have other methods in myDefinedObject other than the constructor, and the key point is that some of those methods need the definition that's stored in mydef. To determine what needs to be done. That is why I currently have mydef as an internal property of the myDefinedObject class.
In a typical run of my program, I have one or two objects of myDefinition (which can each be a very large object, memory wise) but could have dozens of objects of myDefinedObject. So, ideally, mydef would actually be a handle to a myDefinition object, rather than a copy of the object itself.
I know I can make a handle class
classdef myDefinitionHandle < handle
properties(setAccess = immutable)
def;
end
function obj = myDefinitionHandle(thisDefinition)
if isa(thisDefinition, 'myDefinition')
obj.def = thisDefinition;
else
obj.def = [];
warning('No valid definition file provided');
end
end
end
And then change myDefinedObject to check for a myDefinitionHandle and thus effectively have a handle to the definition.
However, the handle is now another layer away when trying to access it in my methods versus when I just stored the definition itself in every myDefinedObject object. For example, I have a method to check that a obj.a is correct based on the obj.type value and what is defined for that type in the definition:
function obj = comply(obj)
if strlength(obj.a) > obj.mydef.def.elements{obj.type}.max
obj.a = obj.a(1:obj.mydef.def.elements{obj.type}.max);
end
end
(Note: comply is a method that is called from set.a, and this is a very simplified example)
I would much prefer just to call obj.mydef.elements{obj.type}.max instead of obj.mydef.def.elements{obj.type}.max as the additional .def object layer in the class really serves no purpose.
Is there a better way I can structure these classes to make it so that mydef directly points to the thisDefinition object passed in the constructor?
  4 commentaires
Chris
Chris le 17 Jan 2023
Modifié(e) : Chris le 17 Jan 2023
Does something like this work? I think you would have to set it like this in myDefinedObject, and the myDefinition object would have to exist (so, pass it to the constructor or setter). The handle is only set once--if something changes in the myDefinition object, the value obj.mydef.elements points to remains.
obj.mydef.elements = @() myDefinitionObj.elements
some background, in case you haven't seen it:
https://undocumentedmatlab.com/articles/handle-object-as-default-class-property-value
Kyle
Kyle le 20 Oct 2023
Hi Captain Karnage,
I have another idea that might work for your use case which might not require the impressive subsref and subsasgn methods that you have written. To save on memory allocations and copying, you could have a single instance of all of your definitions stored in a separate singleton handle object. This singleton handle object would have a struct of all the definitions you want to keep track of. To add a new definition for it to keep track of, you could write a method.
To achieve your goal of calling obj.mydef.elements, you could make mydef a read-only dependent property of your myDefinedObject class. To keep track of the definition in all of your defined objects, you could pass in the DefinitionStore handle object and the name of the definition you care about to the myDefinedObject constructor. Storing them as private properties would give you the necessary information to implement the more convenient mydef get method.
This way, all of your defined objects have a reference to the one handle object that has copies of all of your definitions. Each one does not need to store its own definition, just the means of accessing it. With a dependent property, doing this access can be read-only and as a bonus, gets displayed for free as if it is a property.
Here is a mockup that I came up with in R2023b. It appears to work for all of the simple use cases I came up with.
classdef DefinitionStore < handle
properties (GetAccess=public, SetAccess=private)
Definitions (1,1) struct
end
methods
function obj = addDefinition(obj, definitionName, definition)
arguments
obj
definitionName string
definition myDefinition
end
% Can do other validation here
if obj.hasDefinition(definitionName)
warning("Definition name already exists");
return;
end
obj.Definitions.(definitionName) = definition;
end
function tf = hasDefinition(obj, definitionName)
tf = (isfield(obj.Definitions, definitionName));
end
end
end
classdef myDefinedObject
properties ( Access = private )
mindatabits = 0;
maxdatabits = 64;
DefinitionStore;
DefinitionName;
end
properties ( SetAccess = immutable, GetAccess = public )
type;
name;
a;
x;
end
properties (Dependent)
mydef;
end
methods
function obj = myDefinedObject( num, definitionStore, definitionName, data )
obj.DefinitionStore = definitionStore;
if definitionStore.hasDefinition(definitionName)
obj.DefinitionName = definitionName;
else
obj.DefinitionName = "";
warning('No valid definition provided');
end
%Based on the number input, gets name and other properties from
%the definition, then parses the data based on the properties
%from the lookup in the definition
end
function def = get.mydef(obj)
% Could add logic to
def = obj.DefinitionStore.Definitions.(obj.DefinitionName);
end
end
end
% Create a DefinitionStore with 2 definitions
store = DefinitionStore
store.addDefinition("DefinitionA", definitionA)
store.addDefinition("DefinitionB", definitionB)
% Add a few objects with access to their definitions
obj1 = myDefinedObject(1, store, "DefinitionA", "DataForObject1")
obj2 = myDefinedObject(2, store, "DefinitionA", "DataForObject2")
obj3 = myDefinedObject(3, store, "DefinitionB", "DataForObject3")
% Access elements of obj1's definition
theElements = obj1.mydef.elements;
% Display obj1's definition
obj1.mydef
All indexing you try to do to the definition will automatically be forwarded to the definition object. And you should also get tab completion for free on obj.mydef.*. The only downside that I can think of is that you don't get to show off the impressive RefObject class that you wrote!

Connectez-vous pour commenter.

Réponse acceptée

Captain Karnage
Captain Karnage le 31 Mar 2023
Here is a lighter weight Class that achieves my original goal. Similar to HandleObject, it stores an existing object inside as a "Master" property. It then stores one of two possible versions of this "Master" property:
  • You can make it read-only (default, or by passing the false flag to the allowwrite argument of the constructor) and the constructor stores it in an immutable Master object, immutableMaster, that never changes.
  • You can make it read-write (by passing the true flag to the allowwrite argument of the constructor) and the constructor makes a private object, privateMaster, that changes via the internal methods. Note that even though privateMaster istelf is a private property, it is essentially a public property as most operations on the RefObject object will actually be done on the private Master. You just can't access the .privateMaster property with dot-indexing directly to it like you would with a public property.
Dependent property myMaster will return either immutableMaster or privateMaster depending on the immutable writeable property, which is set = allowwrite in the constructor. The other property is subsquently unused for the life the object.
Where my HandleObject class re-created the original object using dynamicprops, RefObject simply uses the overloaded functions subsref and subsasgn to point all subscripted references and subscripted assignments, respectively, back to the myMaster property. This way, the RefObject class variable acts like it is a variable of the same class as the myMaster Property,
There are two exceptions I know of (so far):
  1. The class() function will state RefObject instead of the Master object's class. However, I added the .Class method which will report the internal class of the master
  2. The metaclass() function will similarly give the metaproperties of the RefObject class instead of the Master object's class. However, I added the .Meta method which will report the metaproperties of the Master's class.
If I find other MATLAB functions that don't behave as intended, I will add them as well.
Note: this means the names Class and Meta would not be useable as properties, methods, or fields of the Master object. The RefObject class would override any access to them and therefore they would not get called from the Master and thus, could be lost when creating a RefObject version. However, I suspect that any such named property, method, or field would be designed to give the same output in the vast majority of cases.
One thing I needed to clear up (for my own edification, but sharing here in case anyone else has the same misconception) - when you use a handle function like HandleObject or RefObject and assign other variables, object. etc. to the properties within, it makes an initial copy of that object and stores it in memory. It does not create a handle / pointer to that original object. You can delete the original object and it will not affect the handle objects that were assigned it's value. Rather, once you create a handle object, any copies / assignments of that handle object will simply point back to the original. Even though I had this misconception, this still meets my original intent to not use up memory with multiple copies of the same object.
RefObject is also more of a true Handle / Pointer - as I found that HandleObject never changes the original master. It just copies its properties and manipulates its own copy of the properties. It still has usefulness as a pointer when you make copies of the HandleObject itself, but uses up more overall memory. It also got very tricky when trying to point to any methods in the master and didn't work for all methods because function_handle has its limits. RefObject so far as I've tested it, has worked with every class, property, field, and/or method I have tried with every type and combination of subscript I could think of, without having to make ugly work-arounds, even native MATLAB types.
Why did I create these classes? To create a handle object that can be pointed to rather than making new copies, but does not add an extra subscripted layer to access the original desired values. I was getting very long chains of variable names from objects nested within objects nested within objects by using a master handle class that simply stored other objects / variables as properties. This keeps my variable names shorter and allows more flexible use of a large class with many other classes in its properties and without using up vast amounts of memory. Now I'm free to make large master classes that get reused and can simply create a RefObject version of any such class to make copies of inside of other classes for reference without copying the entire original object everytime.
Other differences / potential Issues:
  • MATLAB suggests instead of using subsref and subsasgn that one should use matlab.mixin.indexing.RedefinesParen, matlab.mixin.indexing.RedefinesDot, and matlab.mixin.indexing.RedefincesBrace instead, suggesting that MathWorks might deprecate subsref and subsasgn in the future. For now, this was much faster and easier to implement for me, though.
  • subsasgn logic could probably use improvement, particularly in resource efficiency. It has to loop through all passed subscripts twice - once to get the original value, then backwards through to assign the value. This is how I made it compatible with virtually every type of class - it will dive down through the referece chain, keeping a copy of each object on the way, then change just the elements that need to be changed in the final object in the chain, then copy that object and subsequent higher objects over the originals. This ensures all types / classes / structs are maintainted.

Plus de réponses (2)

Captain Karnage
Captain Karnage le 23 Jan 2023
After contemplating this for a while, I came up with my own solution. I attempted a "universal" solution that will create a handle to an object of nearly any class.
In theory, the attached class could be used to clone any class and create a handle to it and all of its properties, with a few relatively minor limitations. It uses the metaproperties of the class it is copying to determine all of its properties and methods. If they are publically readable, it then creates its own properties (using dynamicprops) to those public properties and function_handles to thos public methods. It cannot, of course, copy any private properties or methods - but since it is a handle, those private properties and methods would be invoked whenever using a public property or method that is dependent upon them, anyway, so I can't think of any issues that will create off hand.
In practice, it has worked with all of my custom classes so far, but I haven't been able to do thorough testing of it to verify. If anyone else happens across this, would love feedback as to whether it works or not on other various classes.
  2 commentaires
Captain Karnage
Captain Karnage le 31 Jan 2023
Found two issues in my Class:
1 - copying any class with a disp or display methods causes an error due to not being supported by MATLAB due to temporary nature of their output. I have a fix for that I'll upload later.
2 - the function handles are all 0 argument function handles - so yeah, they won't work with any functions that need an input argument. Haven't figured out how to work that one out yet.
Captain Karnage
Captain Karnage le 31 Mar 2023
Here's a more upated version in case anyone downloaded / used my file. I wouldn't recommend this class, though. It still has some issues and I think it's overly complicated. I came up with a better solution for my needs (called "RefObject" in another answer), but here's my latest version of HandleObject just in case this works in a way RefObject does not.
The code / comments are not entirely cleaned up, but I probably won't make any more updates as I don't think I'll use this anymore.

Connectez-vous pour commenter.


Chris
Chris le 18 Jan 2023
Modifié(e) : Chris le 18 Jan 2023
mydef = myDefinition;
a = myDefinedObject([],mydef,[]);
whos mydef a
Name Size Bytes Class Attributes a 1x1 80 myDefinedObject mydef 1x1 800 myDefinition
a.type
ans = function_handle with value:
@()thisDefinition.type
a.type()
ans = 10×10
92 99 1 8 15 67 74 51 58 40 98 80 7 14 16 73 55 57 64 41 4 81 88 20 22 54 56 63 70 47 85 87 19 21 3 60 62 69 71 28 86 93 25 2 9 61 68 75 52 34 17 24 76 83 90 42 49 26 33 65 23 5 82 89 91 48 30 32 39 66 79 6 13 95 97 29 31 38 45 72 10 12 94 96 78 35 37 44 46 53 11 18 100 77 84 36 43 50 27 59
  1 commentaire
Captain Karnage
Captain Karnage le 20 Jan 2023
Interesting approach. I hadn't considiered that doing a function handle to a varible / object would function that way. My first thought was "is that another one of those undocumented features?" I saw your answer before your additional comment... later I looked back and saw your comment and followed the link, which confirmed that thought.
Before your reply was posted, I did a "brute force" solution of making a handle version of myDefinition which took in a "real" myDefinition object and copied its properties to the handle version. It did the job, but I wish I had seen this solution first. Though going through that made me wonder if I could make a universal "handle" type that could copy any other type of object, but a handle version by making use of the handle subclass of dynamicprops as a superclass.
At first, I was thinking using this solution I still would have to define function handles to every property I wanted to access as well (as you had with obj.type = @() thisDefinition.type; in myDefinedObject so that a.type would return the type from mydef as well. However, I see that I can reference the sub by doing a.mydef().type. Odd because it's really a property and it's being called as a function... but works.
I'm looking back at my question and realize I wasn't completely clear on one thing. If I'm in a myDefinedObject class variable, I'm fine having a layer to access the definition. So, a.mydef.type is fine. When I was making a separate handle class containing a myDefinition object inside, I would have had to do something like a.mydefhandle.mydef.type - that is what I wanted to avoid. And I did not want myDefinedObject class to be a handle in and of itself.

Connectez-vous pour commenter.

Catégories

En savoir plus sur Enumerations dans Help Center et File Exchange

Produits


Version

R2022b

Community Treasure Hunt

Find the treasures in MATLAB Central and discover how the community can help you!

Start Hunting!

Translated by