前言很少有Swift功能像自定义运算符的使用一样引起如此多的激烈争论。虽然有些人发现它们对于减少代码冗余或实现轻量级语法扩展非常有用,但其他人则认为应该完全避免使用它们。爱他们或恨他们-无论哪种方式,我们都可以使用自定义操作做一些非常有趣的事情-无论我们是重载现有的东西还是定义我们自己的东西。本周,让我们看看可以使用自定义运算符的一些情况,以及使用它们的一些优势。数字容器有时我们定义的值类型本质上只是容纳更多原始值的容器。例如,在策略游戏中,玩家可以收集两种资源——木材和黄金。为了在代码中对这些资源进行建模,我使用了一个Resource结构,它是木材和黄金值的容器,如下所示:,跟踪玩家当前可用的资源:structPlayer{varresources:Resources}您可以在游戏中花费资源的其中一件事是为您的军队训练新单位。执行此类操作时,我只需从当前玩家的资源中减去该单位的黄金和木材成本:kind.cost.goldcurrentPlayer.resources.wood-=kind.cost.wood}完全有效,但是由于游戏中有很多操作会影响玩家的资源,所以代码中有很多地方必须是gold重复和两次减法木。这不仅很容易忘记减少其中一个值,而且还使引入新资源类型(例如银币)变得更加困难,因为我必须遍历整个代码并更新所有处理的内容资源。运算符重载让我们尝试使用运算符重载来解决上述问题。在大多数语言(包括Swift)中使用运算符时,您有两个选择,重载现有运算符,或创建一个新运算符。重载就像方法重载一样工作,您可以创建具有新输入或输出的运算符的新版本。在这种情况下,我们将定义-=运算符的重载,它适用于两个Resources值,如下所示:goldlhs.wood-=rhs.wood}}就像遵循Equatable协议一样,Swift中的运算符重载只是一个可以在类型上声明的普通静态函数。这里的-=中,运算符左边是一个inoiut参数,也就是我们要修改的值。通过我们的运算符重载,我们现在可以简单地直接在当前玩家的资源上调用-=,就像我们将它放在任何原始值上一样:currentPlayer.resources-=kind.cost这不仅好读,而且对我们有帮助消除代码重复问题。由于我们总是希望所有外部逻辑修改完整的Resource实例,我们可以将gold和wood属性作为只读属性开放给其他类:structResources{private(set)vargold:Intprivate(set)varwood:Intinit(gold:Int,wood:Int){self.gold=goldself.wood=wood}}另一种方法——可变函数我们可以解决上述资源问题的另一种方法是使用可变函数而不是运算符重载。我们可以添加一个函数,通过另一个实例减少资源值的属性,如下所示:extensionResources{mutatingfuncreduce(byresources:Resources){gold-=resources.goldwood-=resources.wood}}可变函数方法更明确。但是,您也不希望数学的标准减法API变成:5.reduce(by:3),所以也许这是运算符重载表现完美的地方。布局计算让我们看看另一个使用运算符重载非常好的场景。尽管我们有自动布局和强大的布局API,但有时我们会发现自己处于需要进行手动布局计算的情况。在这种情况下,必须对二维值执行数学运算是很常见的——例如CGPoint、CGSize和CGVector。例如,我们可能需要使用图像视图的大小和一些额外的边距来计算标签的原点,如下所示:label.frame.origin=CGPoint(x:imageView.bounds.width+10,y:imageView.bounds.height+20)如果我们可以简单地添加它们而不是必须总是扩展点和大小来使用它们的底层组件(就像我们在上面的资源中所做的那样),那不是很好吗?为了能够做到这一点,我们可以接受两个CGSize实例作为输入并通过重载+运算符输出CGPoint值:extensionCGSize{staticfunc+(lhs:CGSize,rhs:CGSize)->CGPoint{returnCGPoint(x:lhs.width+rhs).width,y:lhs.height+rhs.height)}}通过上面的代码,我们现在可以写下我们的布局计算:label.frame.origin=imageView.bounds.size+CGSize(width:10,height:20)这很酷,但是必须为我们的位置创建CGSize感觉有点奇怪。使它更好一点的一种方法是定义另一个+重载,它接受一个包含两个CGFloat值的元组,像这样:extensionCGSize{staticfunc+(lhs:CGSize,rhs:(x:CGFloat,y:CGFloat))->CGPoint{returnCGPoint(x:lhs.width+rhs.x,y:lhs.height+rhs.y)}}这让我们可以编写布局计算://Usetuplelabels:label.frame.origin=imageView.bounds.size+(x:10,y:20)//或者不写:label.frame.origin=imageView.bounds.size+(10,20)很紧凑,nice!但现在我们正在处理导致运营商争论的核心问题——平衡冗余与可读性。由于我们仍在处理数字,我认为大多数人会发现上面的内容易于阅读和理解,但随着我们继续自定义运算符的使用,尤其是当我们开始引入全新的运算符时,它会变得更加复杂。用于处理错误的自定义运算符到目前为止,我们只是重载了系统中已经存在的运算符。然而,如果我们想开始使用不真正映射到现有功能的运算符,我们需要定义我们自己的。让我们看另一个例子。Swift的do、try、catch错误处理机制在处理不可用的同步操作方面非常出色。它允许我们在出错后轻松安全地退出函数。例如,当加载一个保存在磁盘上的数据模型时:returnnote}}这样做的唯一主要缺点是我们直接向函数的调用者抛出任何潜在的错误,需要减少API可以抛出的错误数量,否则进行有意义的错误处理和测试就变得非常困难。理想情况下,我们想要的是给定API可以抛出的错误数量有限,以便我们可以轻松地单独处理每种情况。假设我们还想捕获所有潜在的错误,让我们同时拥有所有好东西。因此,我们定义了一个带有显式案例的错误枚举,每个案例都使用底层错误的关联值,如下所示:错误并将它们转换为自己的类型是很棘手的。我们必须写一个类似的标准错误处理机制::data)}catch{throwLoadingError.decodingFailed(error)}}catch{throwLoadingError.invalidData(error)}}catch{throwLoadingError.invalidFile(error)}}}我认为没有人愿意阅读上面的代码。一种选择是引入一个perform函数,我们可以使用它来将一个错误转换为另一个错误:)letdata=tryperform(file.read(),orThrow:LoadingError.invalidData)letnote=tryperform(Note(data:data),orThrow:LoadingError.decodingFailed)returnnote}}funcperform
