CaseIterable在Swift内部如何工作

CaseIterable是我在Swift 4.2中最喜欢的功能之一。 尽管它是一个简单的协议,但它解决了一个常见问题(我个人曾多次遇到),该问题需要访问包含某个枚举的所有情况的数组。

如果我们看一下标准库中CaseIterable的实现方式,我们可以看到该协议正是人们所期望的:一系列案例的简单定义。

 public protocol CaseIterable { 
/// A type that can represent a collection of all values of this type.

associatedtype AllCases: Collection where AllCases.Element == Self

/// A collection of all values of this type.

static var allCases: AllCases { get }

}

但是本文与该协议的Swift方面无关。 如您所知,该协议很特殊:您不需要定义和填充allCases类型- 编译器会为您完成。

 enum MyEnum: CaseIterable { 
case foo

case bar

// Code generated by the compiler

static var allCases: AllCases { // alias for [MyEnum]
return [.foo, .bar]

}

//
}

这种行为并不是什么新鲜事物-相同的概念已应用于其他许多协议中,例如RawRepresentableCodable (现在也包括Equatable / Hashable ),但是我从未真正研究过如何实现。 由于我最近一直在研究编译器以能够解决SwiftShield的极端情况,因此我借此机会跳进了Swift的源代码,学习了一些东西并向您展示了它是如何完成的。

检索.swift文件的抽象语法树

(在Medium上,代码有些难于理解-您也可以在我的SwiftRocks博客上阅读此代码,以获取更好的格式!)

为了找出Swift生成的代码是如何生成的,我们需要知道此生成的代码的实际外观。

您可以对生成的二进制文件进行逆向工程,但要理解程序集的含义将非常困难。 另一个选择是派生Swift编译器并将lldb附加到它,但是您首先需要知道断点是什么-我不知道。

幸运的是,您的Xcode工具链中的Swift编译器提供了几个参数,使您可以提取代表人类可读的文件,这些文件表示Swift源文件的“已处理”版本,并且其中一个选项允许您检索文件的抽象语法树(AST) 。文件。

尽管AST只是以树状结构编写的文件内容,但是Swift编译器返回的AST将包含对文件所做的所有优化和整体处理。 这使我们可以看到在编译后使用CaseIterable的枚举是什么样的。

首先,我将在名为enum.swift文件中创建一个基本的枚举:

 enum MyEnum: CaseIterable { 
case foo

case bar

}

现在,要获取AST信息,我将使用-dump-ast参数运行swiftc

 swiftc -dump-ast enum.swift 

由于Swift中涉及的所有代码生成,这将返回一个巨大的树结构,但是我提取了与allCases声明相关的部分:

 (var_decl implicit "allCases" type='[MyEnum]' interface type='[MyEnum]' access=internal type storage_kind=computed 
(accessor_decl implicit 'anonname=0x7fa86f015c28' interface type='(MyEnum.Type) -> () -> [MyEnum]' access=internal type getter_for=allCases
(parameter_list
(parameter "self" interface type='MyEnum.Type'))
(parameter_list)
(brace_stmt
(return_stmt implicit
(array_expr type='[MyEnum]'
(dot_syntax_call_expr implicit type='MyEnum' nothrow
(declref_expr implicit type='(MyEnum.Type) -> MyEnum' decl=moduletest.(file).MyEnum.foo@/Users/bruno.rocha/Desktop/moduletest.swift:2:10 function_ref=double)
(type_expr implicit type='MyEnum.Type' typerepr='<>'))
(dot_syntax_call_expr implicit type='MyEnum' nothrow
(declref_expr implicit type='(MyEnum.Type) -> MyEnum' decl=moduletest.(file).MyEnum.bar@/Users/bruno.rocha/Desktop/moduletest.swift:3:10 function_ref=double)
(type_expr implicit type='MyEnum.Type' typerepr='<>')))))))

AST非常冗长,但是名称有助于我们理解其实际含义。 我们有一个allCases属性的声明( var_decl ),该属性( var_decl )返回( return_stmt )一个MyEnum数组( array_expr ),其中包含.foo (由隐式类型MyEnumdot_syntax_call_expr定义, dot_syntax_call_expr MyEnumdeclref_expr引用)和.bar (与以前一样)。

除了详细程度,这与上面显示的return [.foo, .bar]相同。 但是,代码注入在哪里发生?

调试Swift编译器

由于CaseIterable是一个相对简单的协议(在Swift方面),我们可以通过在GitHub上搜索开源Swift仓库来发现其内部。 我这样做是只有大约2页的参考书-大多数是单元测试。

结果之一是对发生魔术的方法的引用:在名为deriveCaseIterable_enum_getter的文件中,一个名为deriveCaseIterable_enum_getter的可疑方法,该属性采用属性的主体并附加一些内容。 答对了!

但是在分析此方法的作用之前,我有兴趣首先了解编译器是如何到达这里的。

通过制作一个Swift编译器的分支并在调试模式下构建它,我们可以将lldb附加到它,对这个方法进行断点并调用bt以打印其回溯。

 lldb -- /swift-fork/build/Ninja-ReleaseAssert+swift-DebugAssert/swift-macosx-x86_64/bin/swiftc -dump-ast enum.swift 

(注)由于我自己进行了全部研究,因此某些假设可能并不完全正确。 如果您知道Swift编译器,请随时纠正我!

如果您查看有问题的文件,则会发现未直接调用deriveCaseIterable_enum_getter 。 取而代之的是,它从另一个称为deriveCaseIterable()方法作为引用传递。 这意味着回溯将不会显示我们想要的信息-因此,我将直接回溯deriveCaseIterable本身,而不是直接回溯。

 (At lldb:) 
b DerivedConformanceCaseIterable.cpp:82
run
## Process 15104 stopped
## frame #0: 0x0000000101ca809e
## DerivedConformance::deriveCaseIterable() at
## DerivedConformanceCaseIterable.cpp:82
bt

回溯有很长的路要走,但是如果我们采用最后七个堆栈节点,则最终结果是:

 deriveCaseIterable() at DerivedConformanceCaseIterable.cpp:82 
deriveProtocolRequirement() at TypeCheckProtocol.cpp:5137
resolveWitnessViaDerivation() at TypeCheckProtocol.cpp:3081
checkConformance() at TypeCheckProtocol.cpp:3665
checkIndividualConformance() at TypeCheckProtocol.cpp:1707
checkAllConformances() at TypeCheckProtocol.cpp:1328
checkConformancesInContext() at TypeCheckProtocol.cpp:4720

快速查看这些符号的每个文件后,我们可以看到,在解析了文件的结构之后,编译器开始运行几个工作流,以确定所有协议和条件是否正确符合(从回溯到自己看看!)。

checkConformancesInContext ,编译器可以访问上下文(我们的枚举的声明)。 它从中提取一个一致性数组(在这种情况下为checkAllConformances )并调用checkAllConformances

checkAllConformances循环一致性数组,并为每个一致性调用checkIndividualConformance 。 如果未满足要求,则会发出编译警告/错误。

checkIndividualConformance似乎对一致性进行了表面检查,例如检查它是否在class之外使用了class协议,或者是否是试图遵循Swift协议的OBJ-C对象。 如果编译器仍然无法确认需求(因为我们实际上缺少整个属性),则调用checkConformance

checkConformance将尝试通过一些过程来验证协议。 这是我对编译器的了解resolveWitnessViaDerivation ,但是我能够掌握对我们重要的过程的含义: resolveWitnessViaDerivation 。 这是通过注入相关缺少的代码来尝试确认需求的地方。

派生协议

但是在resolveWitnessViaDerivation之前,将调用两个不在回溯中的重要方法: getDerivableRequirementderivesProtocolRequirement 。 您可以在这里看到它们。

getDerivableRequirement确定某个需求是否甚至支持这种代码生成。 如果需求的名称与已知协议中的需求匹配,我们将继续:

 // CaseIterable.allValues 
if (name.isSimpleName(ctx.Id_allCases))
return getRequirement(KnownProtocolKind::CaseIterable);

然后,来自return语句的getRequirement调用derivesProtocolRequirement ,它将尝试将需求与协议自身的规则集进行匹配。

对于“ CaseIterable内部枚举”功能,规则为:

 case KnownProtocolKind::CaseIterable: 
return !enumDecl->hasPotentiallyUnavailableCaseValue()
&& enumDecl->hasOnlyCasesWithoutAssociatedValues();

老实说,我不太确定PotentiallyUnavailableCaseValue指的是什么(如果知道,请在Twitter上告诉我!),但是第二个条件是我们知道:推导仅在您的案例不包含关联值的情况下有效,因为编译器可能无法知道您要在哪个值。 MyEnum并非如此,所以我们很好!

有了可能的派生,我们就返回到backtrace,因为调用了deriveProtocolRequirement 。 现在,编译器将尝试生成其余代码。

在此方法中会发生相同的对象/协议名称匹配,但实际上是为了执行代码生成。 对于CaseIterable ,这导致调用了deriveCaseIterable

 case KnownProtocolKind::CaseIterable: 
return derived.deriveCaseIterable(Requirement);

deriveCaseIterable执行更多检查,例如查看协议是否已在扩展中添加(对派生而言是不行)。 如果一切顺利,它将定义一个空的allCases属性,并最终调用填充该属性的方法:我们首先看到的deriveCaseIterable_enum_getter

 auto *returnTy = computeAllCasesType(Nominal); // [MyEnum] 

VarDecl *propDecl;
PatternBindingDecl *pbDecl;
std::tie(propDecl, pbDecl) = declareDerivedProperty(TC.Context.Id_allCases, returnTy, returnTy, *isStatic=*/true, /*isFinal=*/true);

// Define the getter.
auto *getterDecl = addGetterToReadOnlyDerivedProperty(TC, propDecl, returnTy);

// Set the getter's body.
getterDecl->setBodySynthesizer(&deriveCaseIterable_enum_getter);

这是deriveCaseIterable_enum_getter的定义:

 void deriveCaseIterable_enum_getter(AbstractFunctionDecl *funcDecl) { 
auto *parentDC = funcDecl->getDeclContext();
auto *parentEnum = parentDC->getSelfEnumDecl();
auto enumTy = parentDC->getDeclaredTypeInContext();
auto &C = parentDC->getASTContext();

SmallVector elExprs;
for (EnumElementDecl *elt : parentEnum->getAllElements()) {
auto *ref = new (C) DeclRefExpr(elt, DeclNameLoc(), /*implicit*/true);
auto *base = TypeExpr::createImplicit(enumTy, C);
auto *apply = new (C) DotSyntaxCallExpr(ref, SourceLoc(), base);
elExprs.push_back(apply);
}
auto *arrayExpr = ArrayExpr::create(C, SourceLoc(), elExprs, {}, SourceLoc());

auto *returnStmt = new (C) ReturnStmt(SourceLoc(), arrayExpr);
auto *body = BraceStmt::create(C, SourceLoc(), ASTNode(returnStmt), SourceLoc());
funcDecl->setBody(body);
}

这种方法的有趣之处在于,它比像我这样的非编译人员所期望的要简单得多。 因为我们处于编译的中间阶段,所以编译器可以访问上面所看到的AST的可变版本,并且可以直接访问表示表示CaseIterable-semi-conformant枚举的主要声明的节点。 要将所有allCases添加到其中,我们只需按字面意义将其以AST形式编写并将其附加到枚举的节点即可。

尽管C ++不是最容易理解的语言,但是您可以看到,这只是迭代枚举的情况,并创建返回语句作为一堆表达式,这些表达式与我们上面看到的AST表达式匹配。 参数funcDeclfuncDecl的空主体,它是由deriveCaseIterable生成的。 生成表达式后,它将应用于身体。

娱乐时间:向CaseIterable添加更多属性

现在我们已经弄清楚了它是如何工作的,如何向其中添加我们自己的属性? 我认为我的假CaseIterable将受益于拥有first返回第一个已定义案例的属性。

从标准库的角度来看,这很简单,因为我们只需要定义一个新的静态变量:

 public protocol CaseIterable { 
/// A type that can represent a collection of all values of this type.
associatedtype AllCases: Collection where AllCases.Element == Self

/// A collection of all values of this type.
static var allCases: AllCases { get }

/// The first case of this type.
static var first: Self { get }
}

但是,如果该协议的用户正在枚举中使用first属性,则无需填充它,因此我也希望该属性也由编译器派生。

为此,我将首先克隆生成case数组的deriveCaseIterable_enum_getter方法并对其进行修改,以便表达式返回第一个case而不是数组:

 void deriveCaseIterable_first(AbstractFunctionDecl *funcDecl) { 
auto *parentDC = funcDecl->getDeclContext();
auto *parentEnum = parentDC->getSelfEnumDecl();
auto enumTy = parentDC->getDeclaredTypeInContext();
auto &C = parentDC->getASTContext();

EnumElementDecl *elt = parentEnum->getAllElements().front();
auto *ref = new (C) DeclRefExpr(elt, DeclNameLoc(), /*implicit*/true);
auto *base = TypeExpr::createImplicit(enumTy, C);
auto *dotExpr = new (C) DotSyntaxCallExpr(ref, SourceLoc(), base);

auto *returnStmt = new (C) ReturnStmt(SourceLoc(), dotExpr);
auto *body = BraceStmt::create(C, SourceLoc(), ASTNode(returnStmt), SourceLoc());
funcDecl->setBody(body);
}

完成之后,我们现在需要调用此方法。 之前我们已经看到, deriveCaseIterable_enum_getterderiveCaseIterable()调用–如果我们检查该方法的内容,我们将发现它能够检测被检查参数的名称:

 ValueDecl *DerivedConformance::deriveCaseIterable(ValueDecl *requirement) { 
// Deleted to make stuff shorter: Some pre-checks

if (requirement->getBaseName() != TC.Context.Id_allCases) {
// Deleted to make stuff shorter: Throw compilation error
}

auto *returnTy = computeAllCasesType(Nominal); // Define the [MyEnum] return type
  // Deleted to make stuff shorter: Define allCases's getter 
  declareDerivedProperty(TC.Context.Id_allCases, returnTy, returnTy, *isStatic=*/true, /*isFinal=*/true); 
  // Deleted to make stuff shorter: Prepare allCases's getter 
  getterDecl->setBodySynthesizer(&deriveCaseIterable_enum_getter); 
}

经过一番搜索,我发现Id_allCases属性来自名为KnownIdentifiers.def的文件。 我已经对其进行了编辑,以为我们的功能添加新的Id_first属性。 我还将Id_first添加到上述的getDerivableRequirement()方法中,以便编译器知道可以派生此属性。

为了使此功能正常工作,我们需要保留旧的allCases逻辑,但要添加else块来处理新的first要求。

first创建一个块之后,我们需要将returnTy更改为MyEnum而不是[MyEnum]并让declareDerivedProperty()使用Id_first作为属性名而不是Id_allCases ,最后使setBodySynthesizer使用新方法。

为了使returnTy成为MyEnum ,我只是查看了computeAllCasesType()如何检索枚举的类型,最终通过调用Nominal->getDeclaredInterfaceType();

经过一些编码后,最终方法如下所示:(您可以在此处查看完整版本。)

 ValueDecl *DerivedConformance::deriveCaseIterable(ValueDecl *requirement) { 
// Deleted to make stuff shorter: Some pre-checks

Type returnTy;
Identifier propertyId;

if (requirement->getBaseName() == TC.Context.Id_allCases) {
returnTy = computeAllCasesType(Nominal);
propertyId = TC.Context.Id_allCases;
} else if (requirement->getBaseName() == TC.Context.Id_first) {
returnTy = Nominal->getDeclaredInterfaceType();
propertyId = TC.Context.Id_first;
} else {
// Deleted to make stuff shorter: Throw compilation error
}

// Deleted to make stuff shorter: Define allCases's getter
declareDerivedProperty(propertyId, returnTy, returnTy, /*isStatic=*/true, /*isFinal=*/true);
// Deleted to make stuff shorter: Prepare allCases's getter

if (requirement->getBaseName() == TC.Context.Id_allCases) {
getterDecl->setBodySynthesizer(&deriveCaseIterable_enum_getter);
} else {
getterDecl->setBodySynthesizer(&deriveCaseIterable_first);
}
}

构建编译器之后,我们无需明确定义即可获得CaseIterable枚举的第一种情况!

 enum MyEnum: CaseIterable { 
case foo
case bar
}

print(MyEnum.first) // .foo

结论

编译器很恐怖,而Swift也不例外。 我仍在尝试弄清大多数事情是如何工作的(如果您是编译专家,我正在寻找有关优秀书籍和资源的技巧!),但是我之前在帖子中说的一件事是内部语言可以真正帮助您编写高效的代码。 检查此功能让我很开心,并希望它对您有所帮助。

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

参考文献和优秀读物

Swift源代码