目标:将北太天元 inputParser 的行为完全对齐 MATLAB R2021b 文档语义
最终结果:95/95 有效用例 100% 通过,10 项缺陷全部修复,端到端验收通过,具备上线条件
代码块
PlainText
classdef inputParser < handle
% inputParser 函数的输入解析器
%
% 属性:
% CaseSensitive 是否区分大小写,逻辑值
% KeepUnmatched 是否保留未匹配的参数,逻辑值
% StructExpand 是否展开结构体,逻辑值
% Parameters 参数列表,字符向量元胞数组
% Results 结果,结构体
% Unmatched 未匹配的参数,结构体
% UsingDefaults 使用默认值的参数列表,字符向量元胞数组
%
% 函数:
% addParameter 添加可选参数
% addOptional 添加可选的位置参数
% addRequired 添加必须的位置参数
% parse 解析输入参数
%
% 使用方法(handle 类,语句式与链式两种写法均可):
% obj = inputParser;
%
% obj.addParameter(paramName, defaultVal, validationFcn);或
% obj = obj.addParameter(paramName, defaultVal, validationFcn);
%
% obj.addOptional(paramName, defaultVal, validationFcn);或
% obj = obj.addOptional(paramName, defaultVal, validationFcn);
%
% obj.addRequired(paramName, validationFcn);或
% obj = obj.addRequired(paramName, validationFcn);
%
% obj.parse(varargin);或 obj = obj.parse(varargin); 其中 varargin 需要分开输
% 入到parse,不支持作为一个cell传入。
%
% 输出:
% obj.Results.paramName
properties
CaseSensitive = false
KeepUnmatched = false
StructExpand = true
FunctionName = '';
PartialMatching = true;
end
properties (SetAccess=private,GetAccess=public)
Parameters = {};
Results = struct();
Unmatched = struct();
UsingDefaults = {};
end
properties(Hidden)
RequiredList = {}; % { {name, validator} }
OptionalList = {}; % { {name, default, validator} }
ParameterList = {}; % { {name, default, validator} }
end
methods
function obj = set.CaseSensitive(obj, val)
if ~isscalar(val) || ~islogical(val)
error(message('CaseSensitive 必须是逻辑值标量。'));
end
obj.CaseSensitive = val;
end
function obj = set.KeepUnmatched(obj, val)
if ~isscalar(val) || ~islogical(val)
error(message('KeepUnmatched 必须是逻辑值标量。'));
end
obj.KeepUnmatched = val;
end
function obj = set.StructExpand(obj, val)
if ~isscalar(val) || ~islogical(val)
error(message('StructExpand 必须是逻辑值标量。'));
end
obj.StructExpand = val;
end
function obj = set.PartialMatching(obj, val)
if ~isscalar(val) || ~islogical(val)
error(message('PartialMatching 必须是逻辑值标量。'));
end
obj.PartialMatching = val;
end
function obj = set.FunctionName(obj, val)
if ~ischar(val) && ~(isstring(val) && isscalar(val))
error(message('FunctionName 必须是字符向量或字符串标量。'));
end
obj.FunctionName = char(val);
end
function obj = addParameter(obj, varargin)
% 支持签名:
% addParameter(p, name, defaultVal)
% addParameter(p, name, defaultVal, validationFcn)
% addParameter(p, name, defaultVal, 'PartialMatchPriority', N)
% addParameter(p, name, defaultVal, validationFcn, 'PartialMatchPriority', N)
narginchk(3, 6)
paramName = varargin{1};
defaultVal = varargin{2};
validationFcn = [];
priority = 1; % PartialMatchPriority 默认值
k = 3;
if numel(varargin) >= 3
a = varargin{3};
if isempty(a) || isa(a, 'function_handle')
validationFcn = a;
k = 4;
elseif ~ischar(a) && ~(isstring(a) && isscalar(a))
% 第3参既非验证器也非选项名起始
error(message('validator 必须是函数句柄类型。'));
end
end
% 解析 Name-Value 选项(当前仅 PartialMatchPriority)
rest = varargin(k:end);
if mod(numel(rest), 2) ~= 0
error(message('addParameter 的选项参数必须成对出现。'));
end
for i = 1:2:numel(rest)
optName = rest{i};
if ischar(optName) || (isstring(optName) && isscalar(optName))
if strcmp(char(optName), 'PartialMatchPriority')
priority = rest{i+1};
if ~isscalar(priority) || ~isnumeric(priority) || priority < 1 || floor(priority) ~= priority
error(message('PartialMatchPriority 必须是正整数标量。'));
end
else
error(message('无法识别的选项: %s。', char(optName)));
end
else
error(message('选项名必须是字符向量或字符串标量。'));
end
end
paramName = checkArgs(obj.Parameters, paramName, validationFcn);
obj.ParameterList{end+1} = {paramName, defaultVal, validationFcn, priority};
obj.Parameters{end+1} = paramName;
end
function obj = addParamValue(obj, varargin)
% addParameter 的历史别名(MATLAB 标记为 Not recommended)
obj = obj.addParameter(varargin{:});
end
function obj = addOptional(obj, paramName, defaultVal, validationFcn)
narginchk(3, 4)
if nargin < 4
validationFcn = [];
end
paramName = checkArgs(obj.Parameters, paramName, validationFcn);
obj.OptionalList{end+1} = {paramName, defaultVal, validationFcn};
obj.Parameters{end+1} = paramName;
end
function obj = addRequired(obj, paramName, validationFcn)
narginchk(2, 3)
if nargin < 3
validationFcn = [];
end
paramName = checkArgs(obj.Parameters, paramName, validationFcn);
obj.RequiredList{end+1} = {paramName, validationFcn};
obj.Parameters{end+1} = paramName;
end
function obj = parse(obj, varargin)
% 解析输入参数,并将解析结果存储到 Results 属性中
% 同时处理未匹配参数(Unmatched)和使用默认值的参数(UsingDefaults)
obj.Results = struct(); % 初始化存储解析结果的结构体
obj.Unmatched = struct(); % 初始化未匹配的参数集合
obj.UsingDefaults = {}; % 初始化使用了默认值的参数名列表
hasName = obj.makeHead(); % 设置函数名用于错误消息显示(若存在)
% --- StructExpand 功能 ---
% 如果启用 StructExpand 且仅传入一个结构体参数,则将其转换为名称-值对格式
% 注意: 字段名仅做精确匹配(S-03a), 通过 structMode 局部标志实现,
% 不再修改 obj.PartialMatching 属性(修复副作用缺陷)
structMode = false;
if obj.StructExpand && numel(varargin) == 1 && isstruct(varargin{1})
structMode = true;
s = varargin{1};
fn = fieldnames(s);
vargs = cell(1, 2 * numel(fn));
for i = 1:numel(fn)
vargs{2*i-1} = fn{i};
vargs{2*i} = s.(fn{i});
end
varargin = vargs; % 替换原始输入参数
end
pos = 1; % 当前参数的位置指针
try
% 处理 Required(必需)参数
for i = 1:numel(obj.RequiredList)
[name, validator] = obj.RequiredList{i}{:};
if pos > numel(varargin)
error(message("丢失必须参数: %s", name));
end
val = varargin{pos};
obj = processResult(obj,name,val,validator,true);
pos = pos + 1;
end
% 预设 Optional 和 Parameter 的默认值
for i = 1:numel(obj.OptionalList)
name = obj.OptionalList{i}{1};
default = obj.OptionalList{i}{2};
obj.Results.(name) = default;
obj.UsingDefaults{end+1} = name;
end
for i = 1:numel(obj.ParameterList)
name = obj.ParameterList{i}{1};
default = obj.ParameterList{i}{2};
obj.Results.(name) = default;
obj.UsingDefaults{end+1} = name;
end
% 处理 Optional(可选位置参数)
% 交界算法(对齐 MATLAB 语义, 见行为依据表 J-01/J-02):
% optional 位置遇到文本 v 时:
% 1) 该 optional 带验证器且 v 通过验证 → 消费为该 optional 的值
% (即使 v 恰好是已注册的参数名)
% 2) 否则 v 视为名值区起点, 本 optional 及其后的 optional
% 均保持默认值, 剩余输入转入名值对解析
% 名值区起点判定仅针对 addParameter 注册名, optional 名不参与
for i = 1:numel(obj.OptionalList)
if pos > numel(varargin)
break;
end
spec = obj.OptionalList{i};
name = spec{1};
validator = spec{3};
val = varargin{pos};
isText = ischar(val) || (isstring(val) && isscalar(val));
consumed = false;
if ~isText
consumed = true; % 非文本必为该 optional 的位置值
elseif ~isempty(validator)
try
validate(validator, name, val);
consumed = true; % 文本通过验证 → 消费为值
catch
consumed = false; % 未通过 → 转名值判定
end
end
if consumed
obj = processResult(obj, name, val, validator, false);
pos = pos + 1;
else
break; % 转入名值区, 后续 optional 保持默认
end
end
% 处理 Parameter(名称-值对参数)
remaining = varargin(pos:end);
if mod(numel(remaining), 2) ~= 0
error(message("名称-值对组参数需要一个名称并后跟一个值。"));
end
pairs = numel(remaining) / 2;
knownNames = cellfun(@(x) x{1},obj.ParameterList,'UniformOutput', false); % 获取所有合法参数名
prios = ones(1, numel(obj.ParameterList)); % 各参数的 PartialMatchPriority
for i = 1:numel(obj.ParameterList)
prios(i) = obj.ParameterList{i}{4};
end
for i = 1:2:numel(remaining)
rawKey = remaining{i};
value = remaining{i+1};
checkName(rawKey)
key = char(rawKey);
nameList = knownNames;
if ~obj.CaseSensitive
key = lower(key);
nameList = lower(nameList); % 不区分大小写时统一转小写
end
idx = obj.matchName(key, nameList, knownNames, prios, structMode); % 查找匹配的参数名
if isempty(idx)
if obj.KeepUnmatched
% 保留未匹配参数(N-09 定案: 保留用户原始拼写,
% 依据官方 KeepUnmatched 示例输出 tName: 1 未被小写化;
% 不可用 lower 后的 key)
obj.Unmatched.(char(rawKey)) = value;
else
error(message("无法识别的参数名: %s", key));
end
else
% 匹配成功后,获取验证器并验证输入值
paramSpec = obj.ParameterList{idx};
validator = paramSpec{3};
paramName = paramSpec{1};
obj = processResult(obj,paramName,value,validator,false);
end
pairs = pairs - 1;
end
if pairs ~= 0
error(message("有额外的输出,请检查输入"));
end
catch ME
% 错误处理:若指定函数名则使用 makeMsg 包装错误
if hasName
obj.makeMsg(ME);
else
error(ME);
end
end
end
end
methods(Access=private,Hidden)
function [hasName,head] = makeHead(obj)
if ~isempty(obj.FunctionName)
hasName = true;
head = message('错误使用 %s\n', obj.FunctionName);
else
hasName = false;
head = '';
end
end
function idx = matchName(obj, key, nameList, knownNames, priorities, exactOnly)
% exactOnly=true 时仅精确匹配(struct 展开字段名等场景, 不做前缀匹配)
if nargin < 5
priorities = [];
end
if nargin < 6
exactOnly = false;
end
idx = find(strcmp(key, nameList), 1);
if isempty(idx) && obj.PartialMatching && ~exactOnly
matches = strncmp(key, nameList, length(key));
matchIdx = find(matches);
if isempty(matchIdx)
idx = [];
elseif numel(matchIdx) > 1
% 依 PartialMatchPriority 消歧: 取优先级数值最小者
resolved = [];
if ~isempty(priorities)
prios = priorities(matchIdx);
cand = matchIdx(prios == min(prios));
if numel(cand) == 1
resolved = cand;
warning(message("'%s' 与多个参数名称匹配, 已按 PartialMatchPriority 选取 '%s'。", key, knownNames{resolved}));
end
end
if ~isempty(resolved)
idx = resolved;
elseif obj.KeepUnmatched
idx = [];
else
lists = join(knownNames(matchIdx),',');
error(message("'%1$s' 与多个参数名称匹配: %2$s。 为避免多义性,请指定参数的完整名称。",key,lists{1}));
end
else
idx = matchIdx;
end
end
end
function makeMsg(obj, msg)
[~,head] = obj.makeHead();
msg = strcat(head, msg);
error(msg);
end
function obj = processResult(obj,name,value,validator,isRequired)
validate(validator, name, value); % 调用验证函数进行检查
obj.Results.(name) = value; % 存入解析结果
if ~isRequired
% 移除该参数名出 UsingDefaults
obj.UsingDefaults(strcmp(obj.UsingDefaults, name)) = [];
end
end
end
end
function name = checkArgs(list, value, validator)
% 注册层统一校验: 名称类型/合法变量名/重复注册/验证器类型
% 返回规范化(char)后的参数名
if ~ischar(value) && ~(isstring(value) && isscalar(value))
error(message('参数名必须是字符向量或字符串标量。'));
end
name = char(value);
if ~isNameOK(name)
error(message('参数名 %s 不是有效的变量名。', name));
end
if ismember(name, list)
error(message('参数名 %s 已存在,不允许重新定义。', name));
end
if ~isempty(validator) && ~isa(validator, 'function_handle')
error(message('validator 必须是函数句柄类型。'));
end
end
function tf = isNameOK(name)
% 合法变量名 = isvarname 且非关键字
% (注: 北太天元 isvarname 不拒绝关键字, 需叠加 iskeyword)
tf = isvarname(name) && ~iskeyword(name);
end
function checkName(name)
if ~ischar(name) && ~(isstring(name) && isscalar(name))
error(message('参数名必须是字符向量或字符串标量。'));
end
end
function validate(validator, name, value)
% 双模式验证(修复 B-16):
% 模式1 赋值式: 逻辑返回型验证器(@isnumeric、@(x)x>0)——返回 false 即失败
% 模式2 语句式: 无返回值抛错型验证器(mustBe*)——赋值式调用会报
% "输出参数过多", 改以语句式调用: 抛错即失败, 无异常即通过;
% validateattributes 型匿名验证器(同样无返回值)自动落入此模式
if ~isempty(validator)
ok = 'NA';
try
ok = validator(value); % 先按逻辑返回型尝试
catch
ok = 'NA'; % 赋值式失败: 无返回值型, 待语句式复核
end
if ischar(ok)
% 模式2: 语句式复核
try
validator(value); % 抛错即验证失败
catch
error(message("%1$s 参数验证失败。它必须满足函数: %2$s", name, func2str(validator)));
end
elseif ~all(ok(:))
% 模式1: 逻辑返回 false 即失败
error(message("%1$s 参数验证失败。它必须满足函数: %2$s", name, func2str(validator)));
end
end
end
复制成功
免责声明:本文系网络转载或改编,未找到原创作者,版权归原作者所有。如涉及版权,请联系删