@dynamicMemberLookup如何在Swift内部进行工作(+创建自定义Swift属性)

在Swift 4.2中引入了@dynamicMemberLookup属性,以向语言中添加某种程度的动态性,类似于在Python之类的语言中。 应用于类型时,该类型的属性将在运行时解析,这意味着您可以调用未明确定义的事物,这些事物不一定存在:

  @dynamicMemberLookup类MyClass { 
下标(dynamicMember输入:字符串)->字符串{
返回输入==“ foo”吗? “ bar”:“ SwiftRocks”
}
}

MyClass()。foo //酒吧
MyClass()。notFoo // SwiftRocks
//这些属性不存在,但是可以调用,因为类型为@dynamicMemberLookup。

如代码片段所示,该属性的使用改为强制类型提供dynamicMember下标,该下标接收“ fake”属性名称作为参数并对其进行操作。

最初的动机是它可以与动态语言一起用于互操作性层,从而使您可以像在Python本身中一样在Swift中调用Python代码。 尽管此属性并不考虑纯粹的Swift,但您当然可以为其使用它。 我可能永远不会在常规的iOS开发中使用它,但是我最喜欢的用例是能够改善JSON解析:

 让数据:字符串?  = dict [“ data”]如? 串 
//
让数据:字符串? = dict.data //动态搜索“数据”

以类似的方式,在Swift 5.0中添加了@dynamicCallable作为后续操作,以增加从dynamicCallable类型动态调用方法的能力:

 让myType:MyDynamicType = MyDynamicType() 
myType(someArg2:someVal,someArg2:someVal2)

我很想了解属性在编译器内部的工作方式,以了解有关它们如何将这些假表达式转换为合法表达式的更多信息,因此我再次对Swift编译器进行反向工程以找到这些答案,并利用了这些知识创建我自己的Swift属性。

免责声明:与往常一样,这是我自己的研究和反向工程的结果,其纯粹目的是学习新知识。 由于我显然与这些属性的原始发展无关,因此某些假设可能并不完全正确。 如果您知道编译器的工作原理,请随时纠正我!

如果您发现难以在Medium上阅读该代码,则 我的博客SwiftRocks.com上的帖子格式更好。

本文将重点介绍@dynamicMemberLookup的内部@dynamicMemberLookup@dynamicCallable工作方式@dynamicCallable不同,但总体上遵循相同的想法。

快速回购搜索向我们显示,在Swift中,所有属性主要在Attr.def文件中定义。 这是@dynamicMemberLookup的定义:

  SIMPLE_DECL_ATTR(dynamicMemberLookup,DynamicMemberLookup,   OnNominalType,9) 

“简单属性”是不保存数据的属性(与@available包含参数不同)。 这里的第一个参数是源文件中属性的名称,第二个参数是将在编译器内部使用的属性的名称(它将解析为DynamicMemberLookupAttr ),第三个参数定义其范围(在本例中为NominalType是指类,结构,枚举和协议的集合),最后一个是内部用于确定有效属性的唯一代码。

Swift编译器与属性的第一次联系是在解析过程中。 词法分析生成代码的标记化版本后,解析器将逐步浏览这些标记,以生成基本的抽象语法树(您的代码呈树状结构),以后将用于进一步“理解”您的代码进行类型检查。 如果找到了意外的内容,则该过程可能会失败—关键字输入错误,属性在错误的位置等等。 您肯定已经以“预期的X标识符”错误的形式看到了这一点。

您可以通过运行swiftc -dump-parse告诉编译器打印解析器生成的AST,但是不幸的是,出于文章目的,它不打印类型的属性(拉请求机会?)。 但是好消息是,我们可以通过查看解析声明的代码来确认这一点。 解析器会做大量的事情,所以我会很高兴地选择相关的回溯信息:

ParseStmt.cpp:387–391 —如果当前标记表示一个声明,请尝试对其进行解析。

 如果isStartOfDecl() 
parseDecl(...)

ParseDecl.cpp:2639–2710 —对于常规声明表达式,属性应该是第一个标记,因此它们是要解析的第一个标记:

  Parser :: parseDecl(ParseDeclOptions Flags,llvm :: function_ref 处理程序){ 
//删除:处理#if /#warning /#error
parseDeclAttributeList(...)
//其余的声明解析
}

parseDeclAttributeList()作用是在当前令牌为@的情况下do while循环来解析属性,然后调用parseDeclAttribute()开始解析属性:

  bool Parser :: parseDeclAttribute(DeclAttributes&Attributes,SourceLoc AtLoc) 
如果(Tok.isNot(tok :: identifier)&&
Tok.isNot(tok :: kw_in)&&
Tok.isNot(tok :: kw_inout)){
诊断(Tok,diag :: expected_attribute_name); //“预期属性名称”的编译器错误
返回true;
}
DeclAttrKind DK = DeclAttribute :: getAttrKindFromString(Tok.getText());

// FIXME:这种重命名发生在Swift 3之前,我们可能可以删除
//特定时间的特定后备路径。
checkInvalidAttrName(“ availability”,“ available”,DAK_Available,diag :: attr_renamed); //检查属性名称是否与旧名称匹配并失败,建议使用新名称
//更多检查所有重命名或不推荐使用的属性

if(DK ==“来自Attr.def的有效属性”)//第1805行
返回parseNewDeclAttribute(Attributes,AtLoc,DK);
诊断(Tok,diag :: unknown_attribute,Tok.getText()); //“未知属性%@”的编译错误
}

我喜欢这种方法,因为我们可以看到Swift如何处理重命名的属性-只需明确地检查当前令牌是否与旧名称匹配,并抛出错误说明它现在被称为其他名称。 简而言之,我们只是在查看属性的名称是否与Attr.def定义的属性匹配,如果不是,则停止编译。 如果该属性存在,则parseNewDeclAttribute将使用该令牌并将其添加到该AST的属性列表中。

通过使用带有-dump-parse属性的Swift编译器,我们将告诉编译器开始编译,但在解析步骤结束后立即停止。 这使我们可以确认这确实是执行此逻辑的地方:

  swiftc -dump-parse attrs.swift@swiftRocks类Foo {} //错误:未知属性'swiftRocks' 
@availability类Foo {} //错误:“ @ availability”已重命名为“ @available”

在了解此属性如何产生动态成员之前,如何使用此知识来实际创建我们自己的属性?

这个简短的介绍向我们展示了属性的准系统根本不是那么复杂,并且我们可以使用该信息来创建基本的@swiftRocks属性。

为此,我将在Attr.def为class属性添加一个条目:

  SIMPLE_DECL_ATTR(swiftRocks,SwiftRocks,OnClass,83) 

这样做迫使我将我的属性添加到一些列表中,并在TypeCheckAttr.cpp中添加了visitSwiftRocksAttr()方法,我这样做了,但是将其留空,因为我的属性目前不起作用:

 无效AttributeChecker ::   visitSwiftRocksAttr(SwiftRocksAttr * attr){} 

这足以进行@swiftRocks类型的编译,尽管由于没有逻辑@swiftRocks ,所以什么也不会发生。 为了解情况,我将通过在parseDeclAttribute处添加新的检查来假装旧版Swift版本将此非常有用的属性用作parseDeclAttribute

  checkInvalidAttrName(“ rockingSwift”,“ swiftRocks”,DAK_SwiftRocks,diag :: attr_renamed); 

…导致:

  @rockingSwift类Foo {} //错误:“ @ rockingSwift”已重命名为“ @swiftRocks” 

我们待会儿再讲。

解析后, @dynamicMemberLookup将在语义分析期间再次播放。 为了确认您的代码是合法的,编译器将使用各自的类型注释AST的节点,并确认它们可以执行正在执行的操作。 一些调试显示,声明的类型检查会触发其包含的每个属性的类型检查调用–首先确认该属性为正确的类型(在本例中为NominalType ),其次是为了确认该属性正在正确使用。 后者发生在必须创建visitSwiftRocksAttr方法的同一位置,但是发生在visitDynamicMemberLookupAttr 。 简而言之,此方法检查类型是否实现一个或多个有效subscript(dynamicMember) ,如果不是,则抛出编译错误:

 无效AttributeChecker :: 
visitDynamicMemberLookupAttr(DynamicMemberLookupAttr * attr){
//此属性仅适用于名义类型。
auto decl =强制转换(D);
自动类型= decl-> getDeclaredType();

//查找`subscript(dynamicMember:)`候选者。
自动subscriptName = DeclName(TC.Context,DeclBaseName :: createSubscript(),
TC.Context.Id_dynamicMember);
自动候选= TC.lookupMember(decl,type,subscriptName);

//如果没有候选,则该属性无效。
如果(candidates.empty()){
TC.diagnose(attr-> getLocation(),diag :: invalid_dynamic_member_lookup_type,
类型);
attr-> setInvalid();
返回;
}

//如果没有有效的候选人,则拒绝候选人。
自动oneCandidate =候选人.front();
候选人。过滤器([&](LookupResultEntry条目,bool isOuter)-> bool {
自动Cand = cast (entry.getValueDecl());
TC.validateDeclForNameLookup(cand);
返回isValidDynamicMemberLookupSubscript(cand,decl,TC);
});

如果(candidates.empty()){
TC.diagnose(oneCandidate.getValueDecl()-> getLoc(),
diag :: invalid_dynamic_member_lookup_type,类型);
attr-> setInvalid();
}
}

就开发属性而言,标准似乎到此为止。 因为属性几乎可以用于任何事物,所以每个属性都在有意义的地方进行开发。 在@dynamicMemberLookup的情况下,这是在语义分析期间发生的-当约束系统无法通过常规方法解决我们不存在的属性时,检查此属性的存在是最后的选择:(为简化起见,此处使用原始方法)

  MemberLookupResult ConstraintSystem :: 
performMemberLookup(...){
//已删除:通过多种方式尝试解析成员,但由于该属性不存在而失败

//如果我们将要失败的查找,但我们正在寻找一种类型的成员
//使用@dynamicMemberLookup属性,然后我们解析一个引用
//传递给subscript(dynamicMember :)方法,并将成员名称作为
//字符串参数。
如果(cantResolveIt && isSimpleName){
自动名称= memberName.getBaseIdentifier();
如果(hasDynamicMemberLookupAttribute(...)){
自动&ctx = getASTContext();
//查找这种类型的`subscript(dynamicMember:)`方法。
自动subscriptName = DeclName(ctx,DeclBaseName :: createSubscript(),ctx.Id_dynamicMember);
自动下标= performMemberLookup(constraintKind,
下标名称,
baseTy,functionRefKind,
memberLocator,
includeInaccessibleMembers);
的(自动候选:subscripts.ViableCandidates){
自动decl = cast (candidate.getDecl());
如果(isValidDynamicMemberLookupSubscript(decl,DC,TC))
result.addViable(OverloadChoice :: getDynamicMemberLookup(baseTy,decl,name));
}
}
}

通过确认假属性来自使用该属性的类型(请记住该属性已添加到声明的AST中),求解程序得出结论,可以通过将类型的subscript(dynamicMember:)声明重载来解决该问题。

CS解析了属性的预期返回类型后,Sema的解决方案应用程序阶段将检测所需的重载解决方案,并生成与该类型内部的原始定义匹配的subscript表达式。 最后,此表达式替换原始属性调用。 (此处为原始文件)

  case OverloadChoiceKind :: DynamicMemberLookup:{ 
// DynamicMemberLookup结果的应用将使成员访问
//将x.foo转换为x [dynamicMember:“ foo”]。

//出于可读性而删除

//生成一个(dynamicMember:T)表达式。
自动fieldName = selected.choice.getName()。getBaseIdentifier()。str();
自动索引= buildDynamicMemberLookupIndexExpr(fieldName,...);

//生成并返回使用此字符串作为索引的下标。
返回buildSubscript(base,index,ctx.Id_dynamicMember,...)
}

正如上面的评论所宠爱的,这意味着@dynamicMemberLookup属性只是下标调用的语法糖! 因为我们的伪属性确实不存在,所以编译器将其与对属性所需的下标方法的调用交换。

您可以通过-dump-ast参数进行编译来确认这一点。 与-dump-parse相似,此参数将在执行类型检查后停止编译,使您可以查看AST的完整版本。 对于let foo: String = myType.bar ,结果将是这样的:

  (pattern_named type ='字符串''foo') 
(subscript_expr type ='String'
(tuple_expr隐式类型='(dynamicMember:String)'名称= dynamicMember
(string_literal_expr隐式type ='String'value =“ bar”)))

…隐约意味着let foo: String = myType[dynamicMember: "bar"]

既然已经发现@dynamicMemberLookup ,我们已经准备@dynamicMemberLookup我们的自定义属性实际 @dynamicMemberLookup

我要更改的第一件事是创建属性时必须添加的检查器功能。 我希望此属性仅在称为ClassThatRocks类中ClassThatRocks 。 如果不是这种情况,则编译必须失败。

为了做到这一点,我在编译器的已知标识符列表中添加了一个名为id_ClassThatRocks的新标识符,并在与语义分析相关的错误的编译器列表中添加了“ not ClassThatRocks”错误:

 错误(invalid_swiftrocks_name,无,   “ @swiftRocks要求将%0称为'ClassThatRocks'”,(类型)) 

有了这个,我只需要在visitSwiftRocksAttr()比较声明的名称:

 无效AttributeChecker :: 
visitSwiftRocksAttr(SwiftRocksAttr * attr){
auto decl =强制转换(D);
自动类型= decl-> getDeclaredType();
如果(decl-> getName()!= TC.Context.Id_ClassThatRocks){
TC.diagnose(attr-> getLocation(),
diag :: invalid_swiftrocks_name,键入);
attr-> setInvalid();
}
}

结果是:

  @swiftRocks类Foo {} //错误:@swiftRocks要求将“ Foo”称为“ ClassThatRocks”   @swiftRocks类ClassThatRocks {} //工作! 

对于实际用途,我认为这样一个令人难以置信的属性应该具有同样令人难以置信的用途:当将其应用于类型时,编译器将对所有名称中没有“ ThatRocks”的属性发出警告。表现出色,值得认可。

为此,我将拦截类型检查器以访问所有getter声明。 给定一个吸气剂,我可以递归地让其父母查看某人是否具有@swiftRocks属性,并检查该吸气剂的名称是否不包含“ ThatRocks”,以便向编码员发送友好的警告。

经过很长一段时间的搜索,找到适合该实现的位置之后,我发现typeCheckDecl()具有我需要的所有信息。 这可能是一个可怕的地方,但是SwiftRocks的成员一致认为,此属性比编码实践更重要。 又过了很长时间试图弄清楚如何获取吸气剂的“类型树”,这就是我最终得到的结果

  void TypeChecker :: typeCheckDecl(Decl * D){ 
如果(auto AD = dyn_cast (D)){
DeclName名称= AD-> getStorage()-> getFullName();
如果(自动标称= D-> getDeclContext()-> getSelfNominalTypeDecl()){
自动类型=标称值-> getDeclaredType();
如果(name.isSimpleName()&&!name.isSpecial()&& hasSwiftRocksAttribute(type)){
StringRefrocks =“ ThatRocks”;
StringRef strName = name.getBaseIdentifier()。str();
如果(!strName.contains(rocks)){
诊断(AD-> getLoc(),
diag :: invalid_swiftrocks_property_name,
strName);
}
}
}
}
//删除:方法的其余部分
}

我将为您hasSwiftRocksAttribute()的详细信息,因为我只是复制了hasDynamicMemberLookupAttribute()并更改了属性名称,但是它会检查类型的父级,直到找到该属性。 如果您好奇的话,这是原始的。

构建编译器并运行以下代码段后, AwesomeClass所有属性AwesomeClass得到了认可!

  @swiftRocks类AwesomeClass { 
let number:Int = 1 //警告:属性'number'处于最佳状态。 考虑将其命名为“ numberThatRocks”。
让stringThatRocks:字符串=“字符串”
}

我喜欢研究这些功能,因为它们向您介绍了该语言的工作原理。 在这种情况下,我们可以看到属性具有无限的可能性-从愚蠢的名称检查到使属性从空中冒出来。 有人可能会说,与其他语言相比,它们并不“敏捷”,但在未来几年中,它们很可能仍将继续成为该语言的组成部分。

在我的Twitter上关注我-@rockthebruno,让我知道您想分享的任何建议和更正。

SE-0195 — @dynamicMemberLookup
SE-0195的原始实现
Typechecker文件
Swift源代码


最初发布于 swiftrocks.com