Swift 5.1中面向协议的编程

协议是Swift的基本属性。它们在标准Swift库中起着重要作用,并且是编码抽象的常用方法。在许多方面,它们类似于其他编程语言中的接口

在本教程中,我们将向您介绍一种称为面向协议编程的应用程序开发方法,该方法几乎已成为Swift的核心。这是学习Swift时真正需要了解的内容!

在本指南中,您:

  • 了解面向对象编程和面向协议编程之间的区别;
  • 了解标准协议的实现;
  • 了解如何扩展标准Swift库的功能;
  • 了解如何使用泛型扩展协议。

入门


想象您正在开发赛车游戏。您的玩家可以驾驶汽车,摩托车和飞机。甚至是飞鸟,这都是游戏,对吗?这里最主要的是,您可以在其中驾驶,飞行等各种“事物”的自由度。

开发此类应用程序的常用方法是面向对象的编程。在这种情况下,我们将所有逻辑都封装在一些基类中,此后我们从中继承。因此,基类必须包含“ lead”和“ pilot”的逻辑。

我们通过为每种车辆创建类来开始开发。我们将“ Birds”推迟到以后,稍后再返回。

我们看到汽车摩托车在功能上非常相似,因此我们创建了基类MotorVehicle。汽车和摩托车将从MotorVehicle继承。以同样的方式,我们创建Aircraft基类,然后从中创建Plane

您认为一切都很好,但是-ba! -您的游戏动作发生在XXX世纪,有些汽车已经可以飞行

所以,我们有一个插科打.。 Swift没有多重继承。您的飞行汽车如何从MotorVehicle和Aircraft继承?创建另一个结合了这两种功能的基类?由于没有明确,简单的方法可以实现此目标,因此很可能不会。

在这种可怕的情况下,什么可以拯救我们的比赛呢?面向协议的编程迫切需要帮助!

面向协议的编程是什么样的?


协议允许您为类,结构和枚举分组相似的方法,函数和属性。但是,只有类允许您使用基类的继承。

Swift中协议的优点是一个对象可以符合多种协议。

使用此方法时,您的代码将变得更加模块化。将协议视为功能的构建块。当您向对象添加新功能以使其符合某个协议时,您并不是从头开始创建一个全新的对象,这太长了。相反,您可以添加不同的构造块,直到对象准备就绪为止。

从基类过渡到协议将解决我们的问题。使用协议,我们可以创建同时匹配MotorVehicle和Aircraft的FlyingCar类。不错,是吗?

让我们来编写代码


运行Xcode,创建一个Playground,将其另存为SwiftProtocols.playground,添加以下代码:

protocol Bird {
  var name: String { get }
  var canFly: Bool { get }
}

protocol Flyable {
  var airspeedVelocity: Double { get }
}


使用Command-Shift-Return进行编译以确保一切正常。

在这里,我们定义了一个简单的Bird协议,具有namecanFly属性。然后,我们使用airspeedVelocity属性定义Flyable协议 在“协议前时代”,开发人员将从Flyable类作为基类开始,然后使用继承定义Bird和其他所有可能飞行的东西。 但是在面向协议的编程中,一切都始于协议。这种技术使我们可以在没有基类的情况下封装功能草图。 如您现在所见,这使类型设计过程更加灵活。







确定协议对应的类型


在操场的底部添加此代码:

struct FlappyBird: Bird, Flyable {
  let name: String
  let flappyAmplitude: Double
  let flappyFrequency: Double
  let canFly = true

  var airspeedVelocity: Double {
    3 * flappyFrequency * flappyAmplitude
  }
}


该代码定义了既符合Bird协议又符合Flyable协议的新FlappyBird结构它的airspeedVelocity属性是波动的频率和波动的幅度的产物。canFly属性返回true。

现在添加另外两个结构的定义:

struct Penguin: Bird {
  let name: String
  let canFly = false
}

struct SwiftBird: Bird, Flyable {
  var name: String { "Swift \(version)" }
  let canFly = true
  let version: Double
  private var speedFactor = 1000.0
  
  init(version: Double) {
    self.version = version
  }

  // Swift is FASTER with each version!
  var airspeedVelocity: Double {
    version * speedFactor
  }
}


企鹅是鸟,但是不会飞。最好不要使用继承,也不要让所有鸟都可以

使用协议时,定义功能的组件并使所有合适的对象都符合协议,

然后定义SwiftBird,但是在我们的游戏中有几种不同的版本。版本号越大,其airspeedVelocity越大,其被定义为计算属性。

但是,有一些冗余。尽管我们有Flyable协议的定义,但是每种类型的Bird都必须定义canFly属性的显式定义。看来我们需要一种方法来确定协议方法的默认实现。好吧,对此有协议扩展。

以默认行为扩展协议


协议扩展使您可以设置默认协议行为。在Bird协议定义之后立即编写以下代码:

extension Bird {
  // Flyable birds can fly!
  var canFly: Bool { self is Flyable }
}


这段代码定义了Bird协议的扩展此扩展名确定当类型符合Flyable协议时canFly属性将返回true 换句话说,任何可飞鸟都不再需要显式设置canFly。 现在从FlappyBird,Penguin和SwiftBird的定义中删除let canFly =...。编译代码,并确保一切正常。



让我们进行转移


Swift中的枚举可能符合协议。添加以下枚举定义:

enum UnladenSwallow: Bird, Flyable {
  case african
  case european
  case unknown
  
  var name: String {
    switch self {
    case .african:
      return "African"
    case .european:
      return "European"
    case .unknown:
      return "What do you mean? African or European?"
    }
  }
  
  var airspeedVelocity: Double {
    switch self {
    case .african:
      return 10.0
    case .european:
      return 9.9
    case .unknown:
      fatalError("You are thrown from the bridge of death!")
    }
  }
}


通过定义适当的属性,UnladenSwallow符合两种协议-Bird和Flyable。这样,便实现了canFly的默认定义。

覆盖默认行为


根据Bird协议 我们的类型UnladenSwallow自动收到canFly的实现但是,我们需要UnladenSwallow.unknown为canFly 返回false 在下面添加以下代码:



extension UnladenSwallow {
  var canFly: Bool {
    self != .unknown
  }
}

现在,只有.african和.european才能为canFly返回true。一探究竟!在我们的游乐场底部添加以下代码:

UnladenSwallow.unknown.canFly         // false
UnladenSwallow.african.canFly         // true
Penguin(name: "King Penguin").canFly  // false

编译操场并检查上面注释中指示的值。

因此,我们以与在面向对象编程中使用虚拟方法几乎相同的方式重新定义属性和方法。

扩展协议


您还可以使自己的协议符合Swift标准库中的另一个协议,并定义默认行为。用以下代码替换Bird协议声明:

protocol Bird: CustomStringConvertible {
  var name: String { get }
  var canFly: Bool { get }
}

extension CustomStringConvertible where Self: Bird {
  var description: String {
    canFly ? "I can fly" : "Guess I'll just sit here :["
  }
}

符合CustomStringConvertible协议意味着您的类型必须具有description属性。我们没有定义此属性到Bird类型及其所有派生类中,而是定义了协议扩展CustomStringConvertible,它将仅与Bird类型相关联。在操场的底部

键入UnladenSwallow.african进行编译,您将看到“我会飞”。

Swift标准库中的协议


如您所见,协议是扩展和自定义类型的有效方法。在Swift标准库中,此属性也得到了广泛使用。

将此代码添加到操场上:

let numbers = [10, 20, 30, 40, 50, 60]
let slice = numbers[1...3]
let reversedSlice = slice.reversed()

let answer = reversedSlice.map { $0 * 10 }
print(answer)

您可能知道此代码将输出什么,但是您可能会对此处使用的类型感到惊讶。

例如,slice不是Array,而是ArraySlice。这是一个特殊的“包装器”,提供了一种有效的方式来处理阵列的各个部分。因此,reversedSlice是ReversedCollection <ArraySlice>。

幸运的是,map函数被定义为Sequence协议的扩展,该协议对应于所有集合类型。这使我们可以将map函数应用于Array和ReversedCollection,而无需注意差异。很快,您将利用这个有用的技巧。

在你的标记上


到目前为止,我们已经确定了几种符合Bird协议的类型。现在,我们将添加完全不同的内容:

class Motorcycle {
  init(name: String) {
    self.name = name
    speed = 200.0
  }

  var name: String
  var speed: Double
}

这种类型与鸟类和飞行无关。我们想和企鹅一起参加摩托车比赛。是时候把这家奇怪的公司开始了。

放在一起


为了以某种方式团结这样的不同赛车手,我们需要一个通用的赛车协议。借助称为追溯建模的奇妙功能,我们甚至可以在不触及之前创建的所有类型的情况下完成所有这些工作。只需将其添加到操场上:

// 1
protocol Racer {
  var speed: Double { get }  // speed is the only thing racers care about
}

// 2
extension FlappyBird: Racer {
  var speed: Double {
    airspeedVelocity
  }
}

extension SwiftBird: Racer {
  var speed: Double {
    airspeedVelocity
  }
}

extension Penguin: Racer {
  var speed: Double {
    42  // full waddle speed
  }
}

extension UnladenSwallow: Racer {
  var speed: Double {
    canFly ? airspeedVelocity : 0.0
  }
}

extension Motorcycle: Racer {}

// 3
let racers: [Racer] =
  [UnladenSwallow.african,
   UnladenSwallow.european,
   UnladenSwallow.unknown,
   Penguin(name: "King Penguin"),
   SwiftBird(version: 5.1),
   FlappyBird(name: "Felipe", flappyAmplitude: 3.0, flappyFrequency: 20.0),
   Motorcycle(name: "Giacomo")]

这是我们在这里所做的:首先,定义Racer协议。这就是可以参加比赛的全部。然后,我们将所有先前创建的类型都转换为Racer协议。最后,我们创建一个包含每个类型实例的Array。

编译操场,以便一切都井井有条。

最高速度


我们编写一个函数来确定骑手的最大速度。在操场的尽头添加以下代码:

func topSpeed(of racers: [Racer]) -> Double {
  racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}

topSpeed(of: racers) // 5100

在这里,我们使用max函数以最大速度找到骑手并将其返回。如果数组为空,则返回0.0。

使功能更通用


假设Racers数组足够大,我们需要找到不是在整个数组中而是在它的某些部分中的最大速度。解决方案是更改()的topSpeed使得它不是一个专门用于数组的参数,而是所有符合Sequence协议的参数。

替换我们对()的topSpeed的实现如下:

// 1
func topSpeed<RacersType: Sequence>(of racers: RacersType) -> Double
    /*2*/ where RacersType.Iterator.Element == Racer {
  // 3
  racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}

  1. RacersType是我们函数的通用参数类型。可以是符合Sequence协议的任何内容。
  2. 其中确定序列内容必须符合Racer协议。
  3. 函数主体本身保持不变。

通过在操场的尽头添加以下内容进行检查:

topSpeed(of: racers[1...3]) // 42

现在,我们的函数可用于任何符合Sequence协议的类型,包括ArraySlice。

使功能更“快捷”


秘诀:您可以做得更好。在最底部添加此代码:

extension Sequence where Iterator.Element == Racer {
  func topSpeed() -> Double {
    self.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
  }
}

racers.topSpeed()        // 5100
racers[1...3].topSpeed() // 42

现在我们使用topSpeed()扩展了Sequence协议本身。仅当Sequence包含Racer类型时才适用。

协议比较器


Swift协议的另一个功能是如何定义对象的相等运算符或它们的比较。我们编写以下内容:

protocol Score {
  var value: Int { get }
}

struct RacingScore: Score {
  let value: Int
}

使用Score协议,您可以编写以一种方式处理该类型所有元素的代码。但是,如果您获得了非常特定的类型,例如RacingScore,则不会将其与Score协议的其他派生类混淆。

我们希望比较分数以查看谁得分最高。在Swift 3之前,开发人员需要编写全局函数来定义协议的运算符。现在,我们可以在模型本身中定义这些静态方法。为此,我们替换了Score和RacingScore的定义,如下所示:

protocol Score: Comparable {
  var value: Int { get }
}

struct RacingScore: Score {
  let value: Int
  
  static func <(lhs: RacingScore, rhs: RacingScore) -> Bool {
    lhs.value < rhs.value
  }
}

我们将RacingScore的所有逻辑放在一个地方。可比协议要求您仅为小于功能定义实现。根据我们创建的“小于”功能的实现,所有其他比较功能将自动实现。

测试:

RacingScore(value: 150) >= RacingScore(value: 130) // true

对对象进行更改


到目前为止,每个示例都演示了如何添加功能。但是,如果我们要制定一个协议来更改对象中的某些内容,该怎么办?这可以使用我们协议中的变异方法来完成

添加新协议:

protocol Cheat {
  mutating func boost(_ power: Double)
}

在这里,我们定义了一种使我们能够作弊的协议。怎么样?任意更改boost的内容。

现在创建一个符合作弊协议的SwiftBird扩展:

extension SwiftBird: Cheat {
  mutating func boost(_ power: Double) {
    speedFactor += power
  }
}

在这里,我们实现了boost(_ :)函数,通过传输的值增加了speedFactor。变异关键字使结构了解其价值的其中一个将被此函数所改变。

一探究竟!
var swiftBird = SwiftBird(version: 5.0)
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5015
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5030

结论


在这里,您可以下载游乐场的完整源代码。

您通过创建简单协议并借助扩展来增强其功能,了解了面向协议编程的可能性。使用默认实现,您可以为协议赋予适当的“行为”。几乎与基类类似,但是更好,因为所有这些也适用于结构和枚举。

您还看到协议扩展适用于基础的Swift协议。

在这里,您将找到官方协议指南

您还可以观看有关面向协议编程的WWDC精彩讲座

与任何编程范例一样,存在被带走并开始左右使用协议的危险。这是关于子弹式决策的危险性的有趣注释

Source: https://habr.com/ru/post/undefined/


All Articles