Swift:没有人会打扰的常见错误-一流的功能和保持循环

您好,我亲爱的开发人员,

最近,我认识的一家基于洛杉矶的创业公司联系我,要求我进行代码评估。 他们在代码不稳定,随机崩溃和类似问题上遇到了可怕的问题。 我能说什么 代码中充满了我在上一篇文章中提到的错误(尤其是强制拆包和强制转换,我向之致敬,因为它们是导致大量崩溃的原因)。 但是,由于人们了解Swift中的所有功能都是一等的高阶函数,因此在不同的项目中一遍又一遍地出现了一个特别的错误。

您可以放入变量,将其作为参数发送给其他函数,并作为执行某些函数的结果来接收。

因此,让我们看一下一般的简化情况:

免责声明这是概述方法的示例代码,其他代码段也是如此。 请原谅我,我的读者,我的确理解这一点。 令我震惊的是,有多少人没有得到我所有的文章都描述了通用方法,而不是具体用例。

处理器是执行处理并在处理完成后调用回调的某种实体。 在这种情况下, 数据是拥有处理器并对处理完成时调用的回调感兴趣的某个实体。 最简单的用户故事之一是DataUIViewController子类,而ProcessorUIView子类,当用户以某种方式与视图交互时,该类从控制器调用回调。

那么,这段代码有什么问题呢? 我们这里有一个保留循环。 DataProcessor都不会被释放。 检查我的陈述确实是真的很简单。 只需将调试打印放入ProcessorData的 deinit方法中即可。 如您所见,它从未打印任何内容,因为从未调用过deinit 。 那是因为保留循环。

但是为什么在这种情况下实际上会出现保留循环? 为了掌握这一点,您需要了解swift中的任何structclass实例函数都是闭包。 闭包是捕获和存储外部上下文(例如变量)的一种函数。 一个完美的关闭示例是:

autoincrementedIDGenerator返回一个函数,该函数捕获id并对其进行操作。 每次我们调用该函数时,它都会创建一个名为id的新变量,并返回捕获该变量的函数。

当我们测试输出时,我们将收到:

user id = 0 
user id = 1
user id = 2
image id = 0
image id = 1
image id = 2

这符合我们的期望。 生成器生成的每个函数调用都返回一个单独的函数,以捕获其自己的id

因此,作为前进的一步,让我们看一下Data类的实例方法的类型(同样适用于结构,枚举等):

实例签名显然是(Processor)->() ,那么第一个Data参数出现在哪里? 这是因为Swift实例方法是如何工作的:从类型方法生成实例方法,类型方法接收实例并返回一个闭包,该闭包捕获该实例。 因此,实际上,下面的函数调用都是等效的:

这里的怪癖是,生成实例方法的类型方法会针对引用类型强烈捕获实例本身。 因此,尽管实例方法位于某个强变量( Processor.callback )中,但与之相关的实例也不会被释放。 快速测试:

因此,重要的问题是,对于数据处理器的原始情况,我们如何处理该问题?

最简单的解决方案是修复Data.init

但是,这只是太多样板代码。 该解决方案也有一些重复,显然很糟糕。 试想一下,这种无意义的怪物会通过施加大量语法来满足整个代码中的语言而使这个想法模糊不清。 因此,让我们通过使用Swift泛型和类型推断的力量来改进解决方案并将其概括化:

如果您想知道为什么我要使用map ,请看一下我上一篇文章Swift中的Type Inference。

除了一件事以外,其他所有内容都应基于前面的解释而清楚。 我认为您也想知道Parameters是什么。 您会看到,在幕后类型方法中,第二个参数应该是一个元组:

请注意, ((Int,Int))用双括号括起来,这意味着它是一个用作函数参数的元组。 因此, 参数只是元组的通用类型,其中将包含实例方法参数。 参数的数量可以是空的元组,也可以是具有约10个参数或更多参数的元组(具有10个参数的函数意味着分解时会遇到问题)。

在我们也使用了weakifyFunction之后, Data.init看起来会更好。

好多了吧? 作为保留消除循环的奖励,我们获得了可重用性和意图明确性。

就是这样,伙计们。 祝您有美好的一天,无论您身在何处,都要保持干燥。

为了方便起见,PS Weakify位于文件底部。