Swift:@dynamicMemberLookup与ExpressibleByStringInterpolation链式调用问题
问题描述
我定义了如下Swift枚举:
@dynamicMemberLookup enum JSCall: ExpressibleByStringInterpolation { case getElement(String) case getInnerHTML(id: String) case removeAttribute(id: String, attributeName: String) case customScript(String) case document indirect case chainedCall(JSCall, String) var value: String { switch self { case .document: return "document" case .getElement(let id): return JSCall.chainedCall(.document, "getElementById('\(id)')").value case .getInnerHTML(let id): return JSCall.getElement(id).innerHTML.value // 使用dynamic member lookup case .removeAttribute(let id, let attribute): return JSCall.chainedCall(.getElement(id), "removeAttribute('\(attribute)')").value case .chainedCall(let prefixJs, let suffix): return "\(prefixJs.value).\(suffix)" // 修正原代码笔误:scriptString改为value case .customScript(let script): return script } } init(stringLiteral value: String) { self = .customScript(value) } subscript(dynamicMember suffix: String) -> JSCall { return .chainedCall(.customScript(self.value), suffix) } }
注:原代码中prefixJs.scriptString为笔误,已修正为prefixJs.value,否则无法正常编译。
我发现计算属性value的getInnerHTML分支里,借助@dynamicMemberLookup可以用.innerHTML的点语法实现链式调用,但尝试带字符串插值的链式调用时,比如:
let call = JSCall.document.getElementById("\(id)").value
会出现编译错误:Cannot call value of non-function type 'JSCall'。我原本以为遵守ExpressibleByStringInterpolation协议能解决这个问题,但并未奏效。请问有没有办法让这个枚举支持自然的方法链式调用语法?
解决方案
仅靠@dynamicMemberLookup只能处理属性式的点语法,无法支持带括号的方法调用。要实现自然的方法链式调用,需要结合**DynamicCallable协议**来处理动态方法调用逻辑。
1. 让枚举遵循DynamicCallable协议
添加扩展实现DynamicCallable,处理带参数的方法调用:
extension JSCall: DynamicCallable { func dynamicallyCall(withArguments arguments: [String]) -> JSCall { // 从当前JSCall实例中提取基础脚本字符串 guard let baseScript = self.value as String? else { return self } // 处理参数:将每个参数转为带单引号的字符串,用逗号分隔 let formattedArgs = arguments.map { "'\($0)'" }.joined(separator: ", ") // 拼接成新的调用脚本,包装为customScript类型 return .customScript("\(baseScript)(\(formattedArgs))") } }
2. 优化dynamicMemberLookup的实现
原来的subscript(dynamicMember:)会把当前实例的value重新包装为customScript,直接返回chainedCall更高效,避免冗余包装:
subscript(dynamicMember suffix: String) -> JSCall { return .chainedCall(self, suffix) }
3. 测试链式调用
现在就可以自然地写出带参数的链式调用代码了:
let id = "testDiv" // 获取元素innerHTML let innerHTMLCall = JSCall.document.getElementById("\(id)").innerHTML.value // 输出:"document.getElementById('testDiv').innerHTML" // 移除元素属性 let removeAttrCall = JSCall.document.getElementById("\(id)").removeAttribute("class").value // 输出:"document.getElementById('testDiv').removeAttribute('class')"
额外优化:支持带标签的参数
如果需要支持带标签的方法调用(比如.removeAttribute(id: "test", name: "class")),可以补充实现dynamicallyCall(withKeywordArguments:)方法:
func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, String>) -> JSCall { guard let baseScript = self.value as String? else { return self } let formattedArgs = args.map { "\($0.key): '\($0.value)'" }.joined(separator: ", ") return .customScript("\(baseScript)(\(formattedArgs))") }
调用示例:
let call = JSCall.document.getElementById(id: "\(id)").removeAttribute(attributeName: "class").value // 输出:"document.getElementById(id: 'testDiv').removeAttribute(attributeName: 'class')"
内容的提问来源于stack exchange,提问作者Kris2k
相关产品推荐
相关产品推荐

