Dynamically assign the getter for a dependent prop

2019-03-30 03:59发布

In Matlab, I can define a class as such:

classdef klass < handle
    properties(Dependent)
        prop
    end
end

Matlab is perfectly happy instantiating an object of this class, even without defining a getter for prop. It only fails when I try to access it (understandably). I'd like to set the GetMethod dynamically based upon the property's name.

Unfortunately, even when the property is Dependent, the meta.property field for GetMethod is still read-only. And while inheriting from dynamicprops could allow adding a property and programmatically setting its GetMethod in every instance, I don't believe it could be used to change an existing property. I may have to go this route, but as prop must exist for every object I'd prefer to simply set the getter on a class-by-class basis. Is such a thing possible?

An alternative solution could be through some sort of catch-all method. In other languages, this could be accomplished through a Ruby-like method_missing or a PHP-like __get(). But as far as I know there's no (documented or otherwise) analog in Matlab.


(My use case: this class gets inherited by many user-defined subclasses, and all their dependent properties are accessed in a similar way, only changing based on the property name. Instead of asking users to write get.* methods wrapping a call to the common code for each and every one of their dependent properties, I'd like to set them all dynamically with anonymous function pointers containing the necessary metadata).

2条回答
叛逆
2楼-- · 2019-03-30 04:40

Here is my proposal: create a method in the superclass called add_dyn_prop. This method is to be called in the subclasses instead of creating a dependent property the usual way.

The idea is that the superclass inherit from dynamicprops and use addprop to add a new property, and set its accessor methods manually based on its name.

classdef klass < dynamicprops
    methods (Access = protected)
        function add_dyn_prop(obj, prop, init_val, isReadOnly)
            % input arguments
            narginchk(2,4);
            if nargin < 3, init_val = []; end
            if nargin < 4, isReadOnly = true; end

            % create dynamic property
            p = addprop(obj, prop);

            % set initial value if present
            obj.(prop) = init_val;

            % define property accessor methods
            % NOTE: this has to be a simple function_handle (@fun), not
            % an anonymous function (@()..) to avoid infinite recursion
            p.GetMethod = @get_method;
            p.SetMethod = @set_method;

            % nested getter/setter functions with closure
            function set_method(obj, val)
                if isReadOnly
                    ME = MException('MATLAB:class:SetProhibited', sprintf(...
                      'You cannot set the read-only property ''%s'' of %s', ...
                      prop, class(obj)));
                    throwAsCaller(ME);
                end
                obj.(prop) = val;
            end
            function val = get_method(obj)
                val = obj.(prop);
            end
        end
    end
end

now in the subclass, instead of defining a dependent property the usual way, we use this new inherited function in the constructor to define a dynamic property:

classdef subklass < klass
    %properties (Dependent, SetAccess = private)
    %    name
    %end
    %methods
    %    function val = get.name(obj)
    %        val = 'Amro';
    %    end
    %end

    methods
        function obj = subklass()
            % call superclass constructor
            obj = obj@klass();

            % define new properties
            add_dyn_prop(obj, 'name', 'Amro');
            add_dyn_prop(obj, 'age', [], false)
        end            
    end
end

The output:

>> o = subklass
o = 
  subklass with properties:

     age: []
    name: 'Amro'
>> o.age = 10
o = 
  subklass with properties:

     age: 10
    name: 'Amro'
>> o.name = 'xxx'
You cannot set the read-only property 'name' of subklass. 

Of course now you can customize the getter method based on the property name as you initially intended.


EDIT:

Based on the comments, please find below a slight variation of the same technique discussed above.

The idea is to require the subclass to create a property (defined as abstract in the superclass) containing the names of the desired dynamic properties to be created. The constructor of the superclass would then create the specified dynamic properties, setting their accessor methods to generic functions (which could customize their behavior based on the property name as you requested). I am reusing the same add_dyn_prop function I mentioned before.

In the subclass, we are simply required to implement the inherited abstract dynamic_props property, initialized with a list of names (or {} if you dont want to create any dynamic property). For example we write:

classdef subklass < klass
    properties (Access = protected)
        dynamic_props = {'name', 'age'}
    end

    methods
        function obj = subklass()
            obj = obj@klass();
        end
    end
end

The superclass is similar to what we had before before, only now is it its responsibility to call the add_dyn_prop in its constructor for each of the property names:

classdef klass < dynamicprops        % ConstructOnLoad
    properties (Abstract, Access = protected)
        dynamic_props
    end
    methods
        function obj = klass()
            assert(iscellstr(obj.dynamic_props), ...
                '"dynamic_props" must be a cell array of strings.');
            for i=1:numel(obj.dynamic_props)
                obj.add_dyn_prop(obj.dynamic_props{i}, [], false);
            end
        end
    end

    methods (Access = private)
        function add_dyn_prop(obj, prop, init_val, isReadOnly)
            % input arguments
            narginchk(2,4);
            if nargin < 3, init_val = []; end
            if nargin < 4, isReadOnly = true; end

            % create dynamic property
            p = addprop(obj, prop);
            %p.Transient = true;

            % set initial value if present
            obj.(prop) = init_val;

            % define property accessor methods
            p.GetMethod = @get_method;
            p.SetMethod = @set_method;

            % nested getter/setter functions with closure
            function set_method(obj,val)
                if isReadOnly
                    ME = MException('MATLAB:class:SetProhibited', sprintf(...
                      'You cannot set the read-only property ''%s'' of %s', ...
                      prop, class(obj)));
                    throwAsCaller(ME);
                end
                obj.(prop) = val;
            end
            function val = get_method(obj)
                val = obj.(prop);
            end
        end
    end
end

Note: I did not use ConstructOnLoad class attribute or Transient property attribute, as I am still not sure how they would affect loading the object from a saved MAT-file in regards to dynamic properties.

>> o = subklass
o = 
  subklass with properties:

     age: []
    name: []

>> o.name = 'Amro'; o.age = 99
o = 
  subklass with properties:

     age: 99
    name: 'Amro'
查看更多
不美不萌又怎样
3楼-- · 2019-03-30 04:52

Check if this is what you want. The problem is that the user will need to get the properties using (), which may be quite boring, but anyway, I think this way you can change the variables. You can't change them directly on the class, but you can change the objects property values on demand. It doesn't need to change the values on the constructor, you can do that using another function that will be inherited by the classes.

klass1.m

classdef(InferiorClasses = {?klass2}) klass < handle

  methods
    function self = klass
      selfMeta = metaclass(self);
      names = {selfMeta.PropertyList.Name};
      for name = names
        switch name{1}
        case 'prop_child_1'
          self.(name{1}) = @newGetChild1PropFcn;
        case 'prop_child_2'
          self.(name{1}) = @newGetChild2PropFcn;
        end
      end
    end
  end
  methods(Static)
    function out = prop
      out = @defaultGetPropFcn;
    end
  end
end

function out = defaultGetPropFcn
  out = 'defaultGetPropFcn';
end

function out = newGetChild1PropFcn
  out = 'newGetChild1PropFcn';
end

function out = newGetChild2PropFcn
  out = 'newGetChild2PropFcn';
end

klass2.m

classdef klass2 < klass
  properties
    prop_child_1 = @defaultGetChildPropFcn1
    prop_child_2 = @defaultGetChildPropFcn2
  end
  methods
    function self = klass2
      self = self@klass;
    end
  end
end

function out = defaultGetChildPropFcn1
  out = 'defaultGetChildPropFcn1';
end
function out = defaultGetChildPropFcn2
  out = 'defaultGetChildPropFcn2';
end

Output:

a = klass2
b=a.prop_child_1()


b =

newGetChild1PropFcn
查看更多
登录 后发表回答