子类构造函数拒绝接受任何名称-值参数

Subclass constructor refuses to accept any name-value arguments

TL;DR

我在使用 arguments 功能时遇到问题,如果将 any 名称-值参数对传递给子类,则子类无法实例化。

(Class定义在最后。)


arguments functionality, introduced in R2019b, brings great promise in terms of simplifying argument validation and removing boilerplate code from functions [1]. However, upon trying to implement name-value (NV) arguments taken from public class properties,我得到以下错误:

Invalid argument list. Check for wrong number of positional arguments or placement of positional arguments after 
name-value pairs. Also, check for name-value pairs with invalid names or not specified in pairs. 

甚至在执行任何子类代码之前。此消息令人困惑,因为制表符补全似乎对 NV 对按预期工作:

此外,如果根本不传递参数,一切正常:

>> FluidLayer()
ans = 
  FluidLayer with properties:

    prop1: NaN
    prop2: NaN

对比

>> FluidLayer('prop1',1)
Error using FluidLayer
Invalid argument list. ...

问题:

  1. 为什么我会收到错误消息?我认为我没有以某种无意或未记录的方式使用 arguments 机制,所以我对我可能做错了什么感到双重困惑(假设这不是错误)。
  2. 除了放弃整个 arguments 方法(我想保留参数名称建议)之外,还能做些什么来解决这个问题?我考虑过使用 functionSignatures.json approach 过渡到 varargin and/or - 但这些需要更多的工作。

classdef Layer < handle & matlab.mixin.Heterogeneous

  properties (GetAccess = public, SetAccess = protected)
    prop1(1,1) double {mustBeNonempty} = NaN
    prop2(1,1) double {mustBeNonempty} = NaN
  end % properties

  methods (Access = protected)

    function layerObj = Layer(props)
      % A protected/private constructor means this class cannot be instantiated
      % externally, but only through a subclass.
      arguments
        props.?Layer
      end

      % Copy field contents into object properties
      fn = fieldnames(props);
      for idxF = 1:numel(fn)
        layerObj.(fn{idxF}) = props.(fn{idxF});
      end
    end % constructor
  end % methods

end
classdef FluidLayer < Layer

  properties (GetAccess = public, SetAccess = protected)
    % Subclass-specific properties

  end % properties

  methods 
    function layerObj = FluidLayer(props)
      arguments
        props.?FluidLayer
      end

      % Create superclass:
      propsKV = namedargs2cell(props);
      layerObj = layerObj@Layer(propsKV{:});

      % Custom modifications:

    end % constructor
  end % methods

end

我已经能够将您的示例简化为:

classdef Layer

    properties (GetAccess = public, SetAccess = protected)
        prop1
        prop2
    end % properties

    methods

        function layerObj = Layer(props)
            arguments
                props.?Layer
            end
            disp(props)
        end % constructor

    end % methods

end

现在 Layer('prop1',1) 按照您的描述抛出错误。

因此,它与subclassing 或继承无关。

但是,如果我们删除 SetAccess = protected 限制(保留具有 public get 和 set 访问权限的属性),那么一切都会如您所愿。

我不知道为什么限制设置访问会限制这个用例,因为它与编写这些属性无关,而且 class 方法无论如何都应该设置访问。我的猜测是这是一个错误。

R2020a 的文档指出 structName.?ClassName 语法只能用于“ 由 class 定义的可设置属性(即所有具有 [=14= 的属性) ] SetAccess)”。因此,明确不支持将其与 protected 属性一起使用。

因此,如果我们要使用"automatic" arguments验证机制,我们别无选择,只能将SetAccess设置为public。但是,此解决方案会将对象属性暴露给不需要的外部更改,因此建议根据以下几个原则采取解决方法:

    根据文档的要求,
  1. properties 现在有 public SetAccess
  2. 添加了自定义 setter,可根据 dbstackmeta.class 比较执行访问验证。

Layer.m(注意 2 个新的 methods 块):

classdef Layer < handle & matlab.mixin.Heterogeneous

  properties (GetAccess = public, SetAccess = public)
    prop1(1,1) double {mustBeNonempty} = NaN
    prop2(1,1) double {mustBeNonempty} = NaN
  end % properties

  %% Constructor
  methods (Access = protected)

    function layerObj = Layer(props)
      % A protected/private constructor means this class cannot be instantiated
      % externally, but only through a subclass.
      arguments
        props.?Layer
      end

      % Copy field contents into object properties
      fn = fieldnames(props);
      for idxF = 1:numel(fn)
        layerObj.(fn{idxF}) = props.(fn{idxF});
      end
    end % constructor

  end % protected methods  

  %% Setters & Getters  
  methods

    function set.prop1(obj, val)
      if Layer.getCallerMetaclass() <= ?Layer
        obj.prop1 = val;
      else
        Layer.throwUnprotectedAccess();
      end
    end

    function set.prop2(obj, val)
      if Layer.getCallerMetaclass() <= ?Layer
        obj.prop2 = val;
      else
        Layer.throwUnprotectedAccess();
      end
    end

  end % no-attribute methods

  %% Pseudo-protected implementation  
  methods (Access = protected, Static = true)

    function throwUnprotectedAccess()
      stack = dbstack(1);
      [~,setterName,~] = fileparts(stack(1).name);      
      throw(MException('Layer:unprotectedPropertyAccess',...
        ['Unable to set "', stack(1).name(numel(setterName)+2:end),...
        '", as it is a protected property!']));
    end    

    function mc = getCallerMetaclass()
      stack = dbstack(2, '-completenames');
      if isempty(stack)
        mc = ?meta.class;
      else
        [~,className,~] = fileparts(stack(1).file);
        mc = meta.class.fromName(className);
      end
    end

  end % protected static methods
end % classdef

FluidLayer.m(添加了foo方法):

classdef FluidLayer < Layer

  properties (GetAccess = public, SetAccess = protected)
    % Subclass-specific properties

  end % properties

  methods
    %% Constructor
    function layerObj = FluidLayer(props)
      arguments
        props.?Layer
      end

      % Create superclass:
      propsKV = namedargs2cell(props);      
      layerObj = layerObj@Layer(propsKV{:});
    end % constructor

    function obj = foo(obj)
      obj.prop1 = obj.prop1 + 1;
    end

  end % methods
end % classdef

下面是其工作原理的演示:

>> fl = FluidLayer('prop1', 2, 'prop2', 1)
fl = 
  FluidLayer with properties:

    prop1: 2
    prop2: 1

>> fl.prop1 = 5;  % attempting to set property directly (unintended)
Error using Layer/throwUnprotectedAccess (line 51)
Unable to set "prop1", as it is a protected property!
Error in Layer/set.prop1 (line 32)
        Layer.throwUnprotectedAccess(); 

>> fl.foo()       % attempting to set property through method (intended)
ans = 
  FluidLayer with properties:

    prop1: 3
    prop2: 1
>> 

结论:使用setter方法可以克服SetAccess = public限制,但需要对class代码进行复杂的修改。

实用笔记:

  1. getCallerMetaclass 的局限性在于它无法正确识别包 - 导致可能出现空的 metaclass 对象。所以请记住,如果是这种情况,应该修改此功能。
  2. dbstack 被多次调用,虽然没有必要(它可以在 setter 中调用一次,然后可以传递结果)。
  3. 不同属性的 setter 代码有 5 行长,大部分都是重复的(属性 名称除外)- 这可以通过抓取 属性 名称来改进通过 dbstack.