作为软件开发人员,我们学到的第一课是如何将概念和功能组织成离散的单元。在最小的层面上,这意味着考虑类型、方法和属性。这些部分然后形成一个或多个模块的基础,然后可以打包到库或框架中。
这样,导入声明就是将所有内容粘合在一起的粘合剂。
然而,尽管它们很重要,大多数Swift开发人员只熟悉它们最基本的形式:
import module
本周在NS Hipster上,我们将探索斯威夫特最突出部分的其他形状。
导入声明允许您的代码访问在其他文件中声明的符号。但是,如果多个模块声明了同名的函数或类型,编译器可能无法确定您想在代码中调用哪个。
为了证明这一点,考虑代表**铁人三项和五项**多运动比赛的两个模块:
铁人三项包括三个项目:游泳、骑自行车和跑步。
// Triathlon Modulefunc swim() { print("🏊 Swim 1.5 km")}func bike() { print("🚴 Cycle 40 km")}func run() { print("🏃 Run 10 km")}
现代五项包括五个项目:击剑、游泳、马术、射击和跑步。
// Pentathlon Modulefunc fence() { print("🤺 Bout with épées")}func swim() { print("🏊 Swim 200 m")}func ride() { print("🏇 Complete a show jumping course")}func shoot() { print("🎯 Shoot 5 targets")}func run() { print("🏃 Run 3 km cross-country")}
如果我们单独导入其中一个模块,我们可以使用它们的不限定名称引用它们的每个函数,没有问题。
import Triathlonswim() // OK, calls Triathlon.swimbike() // OK, calls Triathlon.bikerun() // OK, calls Triathlon.run
但是如果我们一起导入两个模块,我们不能总是使用不合格的函数名。铁人三项和五项都包括游泳和跑步,所以对游泳()的引用是模糊的。
import Triathlonimport Pentathlonbike() // OK, calls Triathlon.bikefence() // OK, calls Pentathlon.fenceswim() // Error, ambiguous
我们如何协调这一点?一个策略是使用完全限定的名称来解决任何模糊的引用。通过包含模块名称,程序是在游泳池里游几圈还是在公开水域游一英里就不会混淆。
import Triathlonimport PentathlonTriathlon.swim() // OK, fully-qualified reference to Triathlon.swimPentathlon.swim() // OK, fully-qualified reference to Pentathlon.swim
解决应用编程接口名称冲突的另一种方法是更改导入声明,以便对每个模块包含的内容更有选择性。
导入单个声明
导入声明有一个表单,可以指定单个结构、类、枚举、协议和类型别名,以及在顶层声明的函数、常量和变量:
import kind module.symbol
这里,种类可以是以下任意一个关键词:
KindDescriptionstructStructureclassClassenumEnumerationprotocolProtocoltypealiasType AliasfuncFunctionletConstantvarVariable
例如,下面的导入声明只添加了Pentathlon模块中的游泳()函数:
import func Pentathlon.swimswim() // OK, calls Pentathlon.swimfence() // Error, unresolved identifier
解决符号名称冲突
当多个符号在代码中以相同的名称引用时,Swift编译器通过查询以下命令来解析此引用:
-
本地声明
-
进口报关单
-
进口模块
如果其中任何一个有一个以上的候选者,Swift无法解决歧义并引发编译错误。
例如,导入Triathlon模块提供了游泳()、自行车()和跑()方法。从Pentathlon导入的游泳()函数声明覆盖了Triathlon模块的声明。同样,本地声明的run()函数覆盖了Triathlon中同名的符号,也覆盖了任何导入的函数声明。
import Triathlonimport func Pentathlon.swim// Local function shadows whole-module import of Triathlonfunc run() { print("🏃 Run 42.195 km")}swim() // OK, calls Pentathlon.swimbike() // OK, calls Triathlon.bikerun() // OK, calls local run
调用这个代码的结果是什么?一个奇怪的多项运动项目,包括在游泳池里跑几圈、骑自行车和马拉松. (@ 我们,铁人)
澄清和尽量缩小范围
除了解决名称冲突之外,导入声明也可以成为澄清程序员意图的一种方式。
例如,如果您只使用像App Kit这样的大型框架中的单个函数,您可以在导入声明中单独列出它。
import func AppKit.NSUserNameNSUserName() // "jappleseed"
当导入顶级常量和变量时,这种技术尤其有用,这些常量和变量的来源通常比其他导入的符号更难辨别。
例如,达尔文框架导出——除其他外——一个顶级stderr变量。这里的显式导入声明可以抢占代码审查期间关于该变量来自哪里的任何问题。
import func Darwin.fputsimport var Darwin.stderrstruct StderrOutputStream: TextOutputStream { mutating func write(_ string: String) { fputs(string, stderr) }}var standardError = StderrOutputStream()print("Error!", to: &standardError)
导入子模块
导入声明的最终形式提供了另一种限制API暴露的方法:
import module.submodule
您最有可能在大型系统框架中遇到子模块,如AppKit和Acccerate。这些**伞式框架**不再被认为是最佳实践,但它们在20世纪初苹果向可可过渡期间发挥了重要作用。
例如,您可以仅从**Core Services框架导入Dicphaary Services子模块,以将代码与无数已弃用的API(如Carbon Core**)隔离开来。
import Foundationimport CoreServices.DictionaryServicesfunc define(_ word: String) -> String? { let nsstring = word as NSString let cfrange = CFRange(location: 0, length: nsstring.length) guard let definition = DCSCopyTextDefinition(nil, nsstring, cfrange) else { return nil } return String(definition.takeUnretainedValue())}define("apple") // "apple | ˈapəl | noun 1 the round fruit of a tree..."
实际上,隔离导入的声明和子模块除了发出程序员意图的信号之外没有任何真正的好处。这样做你的代码不会编译得更快。由于大多数子模块似乎重新导入了它们的伞形头,这种方法不会降低自动完成列表中的噪音。
像许多晦涩和高级的话题一样,你以前没有听说过这些进口申报单的最有可能的原因是你不需要了解它们。如果你已经在没有它们的情况下制作了这么多应用程序,你可以合理地保证你现在不需要开始使用它们。
相反,这里有价值的收获是理解Swift编译器如何解决名称冲突。为此,导入声明是一个非常重要的概念。