.
Ray Wenderlich原文加笔记,该文讲述了Swift协议的继承,协议的默认实现,默认方法的重写,协议的扩展等新的属性。
Swift中Protocol
基本具备了calss
的特性。允许继承、扩展等。原文链接
Protocol-Oriented Programming will help you fly!
Update Note: This tutorial has been updated to Swift 3.0 by Niv Yahel. The original tutorial was written by Erik Kerber.
Imagine you’re developing a racing game. You can drive a car, ride a motorcycle, or even fly a plane. A common approach to creating this type of application is by using object oriented design, encapsulating all of the logic inside of an object that gets inherited to all of those that share similarity.
This design approach works, but does come with some drawbacks. For example, if you add the ability to create machines that also require gas, birds that fly in the background, or anything else that may want to share game logic, there isn’t a good way to separate the functional components of vehicles into something reusable.
This scenario is where protocols really shine.
Swift has always let you specify interface guarantees on existing class
, struct
and enum
types using protocols. This lets you interact with them generically. Swift 2 introduced a way to extend protocols and provide default implementations. Finally, Swift 3 improves operator conformance and uses these improvements for the new numeric protocols in the standard library.
Protocol are extremely powerful and can transform the way you write code. In this tutorial, you’ll explore the ways you can create and use protocols, as well as use protocol-oriented programming
patterns to make your code more extensible.
You’ll also see how the Swift team was able to use protocol extensions to improve the Swift standard library itself, and how it impacts the code you write.
Getting Started
Begin by creating a new playground. In Xcode, select File*New*Playground… and name the playground SwiftProtocols. You can select any platform, since all the code in this tutorial is platform-agnostic. Click Next to choose where you would like to save it, and finally click Create.
Once your new playground is open, add the following code to it:
1 | protocol Bird { |
This defines a simple protocol Bird
with properties name
and canFly
, as well as a Flyable
protocol which defines airspeedVelocity
.
In a pre-protocol world, you might have started with Flyable
as a base class and then relied on object inheritance to define Bird
as well as other things that fly, such as airplanes. Note that here, everything is starting out as a protocol! This allows you to encapsulate the functional concept in a way that doesn’t require a base class.
You’ll see how this makes the entire system more flexible when you start to define actual types next.
Defining Protocol-Conforming Types
Add the following struct
definition to the bottom of the playground:
1 | struct FlappyBird: Bird, Flyable { |
This defines a new struct FlappyBird
, which conforms to both the Bird
and Flyable
protocols. Its airspeedVelocity
is calculated as a function of flappyFrequency
and flappyAmplitude
. Being flappy, it returns true
for canFly
. :]
Next, add the following two struct definitions to the bottom of the playground:
1 | struct Penguin: Bird { |
A Penguin
is a Bird
, but cannot fly. A-ha — it’s a good thing you didn’t take the inheritance approach, and make all birds flyable after all! Using protocols allows you to define functional components and have any relevant object conform to them.
Already you can see some redundancies. Every type of Bird
has to declare whether it canFly
or not, even though there’s already a notion of Flyable
in your system.Bird
协议需要实现canFly
参数,但是实际上遵守了Flyable
协议就说明该对象的canFlay
是true的。
Extending Protocols With Default Implementations
设置默认的实现,只要是遵守了Flyable
协议那么canFlay
都返回true
。解决的办法就是设置协议的扩展
With protocol extensions, you can define default behavior for a protocol. Add the following just below the Bird
protocol definition:
1 | extension Bird { |
This defines an extension on Bird
that sets the default behavior for canFly
to return true
whenever the type is also Flyable
. In other words, any Flyable
bird no longer needs to explicitly declare so!
Delete the let canFly = ...
from FlappyBird
, SwiftBird
and Penguin
struct declarations. You’ll see that the playground successfully builds since the protocol extension now handles that requirement for you.
Why Not Base Classes?
Protocol extensions and default implementations may seem similar to using a base class or even abstract classes in other languages, but they offer a few key advantages in Swift:
- Because types can conform to more than one protocol, they can be decorated with default behaviors from multiple protocols. Unlike multiple inheritance of classes which some programming languages support, protocol extensions do not introduce any additional state.
- Protocols can be adopted by classes, structs and enums. Base classes and inheritance are restricted to class types.
In other words, protocol extensions provide the ability to define default behavior for value types and not just classes.
You’ve already seen this in action with a struct. Next, add the following enum definition to the end of the playground:
1 | enum UnladenSwallow: Bird, Flyable { |
As with any other value type, all you need to do is define the correct properties so UnladenSwallow
conforms to the two protocols. Because it conforms to both Bird
and Flyable
. It also gets the default implementation for canFly
!
Did you really think this tutorial involving airspeedVelocity
wouldn’t include a Monty Python reference? :]
Overriding Default Behavior
但是对于UnladenSwallow
的Enum的情况,如果是.unknown
,因为遵守了Bird
和Flyable
协议,那么.unknown
默认canFlay
也会是true
,怎么解决这个问题。解决的办法就是定义UnladenSwallow
的扩展覆盖canFlay
的实现。
Your UnladenSwallow
type automatically got an implementation for canFly
by virtue of conforming to the Bird
protocol. However, you really want UnladenSwallow.unknown
to return false
for canFly
. Is it possible to override the default implementation? Yes, it is. Add this to the end of your playground.
1 | extension UnladenSwallow { |
Now only .african
and .european
will return true
for canFly
. Test it out by adding the following to the end of your playground:
1 | UnladenSwallow.unknown.canFly // false |
In this way, it is possible to override properties and methods much like you can with virtual methods in object oriented programming.
Extending Protocols
你也可以扩展标准协议,例如CustomStringConvertible
的行为,例如你想给遵守了Bird
协议的时候同事也遵守CustomStringConvertible
You can utilize protocols from the standard library and also define default behaviors.
Modify the Bird
protocol declaration to conform to the CustomStringConvertible
protocol:
定义Bird协议也遵守CustomStringConvertible
协议。遵守CustomStringConvertible
协议,那么就意味着你需要定义description
属性,也就是说所有遵守Bird
协议的,你都需要给他们定义description
属性,如果我想自动实现怎么办呢?
1 | protocol Bird: CustomStringConvertible { |
Conforming to CustomStringConvertible
means your type needs to have a description
property so it acts like a String. Does that mean you now have to add this property to every current and future Bird
type?
Of course, there’s an easier way with protocol extensions. Add the code underneath the Bird
definition:
定义CustomStringConvertible
的扩展,但是这个扩展只是用在遵守了Bird
协议的对象上。利用canFly
属性自动提供两个版本的description
的值。
1 | extension CustomStringConvertible where Self: Bird { |
This extension will make the canFly
property represent each Bird
type’s description
value.
To try it out, add the following to the bottom of the playground:
1 | UnladenSwallow.african |
You should see “I can fly!”
appear in the assistant editor. But more notably, you just extended your own protocol!
Effects on the Swift Standard Library
You’ve seen how protocol extensions are a great way to customize and extend the capabilities. What may surprise you is how the Swift team was able to use protocols to improve the way the Swift standard library is written as well.
Add the following code to the end of your playground:
1 | let numbers = [10,20,30,40,50,60] |
This should look pretty straightforward, and you might even be able to guess the answer that is printed. What might be surprising are the types involved. slice
, for example, is not an Array
of integers but an ArraySlice<Int>
. This special wrapper type acts as a view into the original array and avoids costly memory allocations that can quickly add up. Similarly, reversedSlice
is actually a ReversedRandomAccessCollection<ArraySlice<Int>>
which is again just a wrapper type view into the original array.
Fortunately, the geniuses developing the standard library defined the map
method as an extension to the Sequence
protocol and all of the collection wrappers (of which there are dozens) to conform to this protocol. This makes it possible to call map on Array
just as easily as it is ReversedRandomAccessCollection
and not notice the difference. You will borrow this important design pattern shortly.
Off to the Races
So far you defined several Bird
conforming types. Now add something totally different to the end of your playground.
1 | class Motorcycle { |
This class that has nothing to do with birds or flying things you have defined so far. But you want to race motorcycles as well as penguins. Time to bring all of the pieces together.
Bringing it Together
It is time to unify all of these disparate types with a common protocol for racing. You can do this with out even going back and touching the original model definitions. The fancy term for this is retroactive modeling. Just add the following to your playground:
1 | protocol Racer { |
上面的代码,让所有的物种都准售了Racer
协议,最后吧所有的物种都放在了一个racers
的数组中。
In this code, you first define the protocol Racer
and then you make all of the different types conform. Some types, such as Motorcycle
conform trivially. Others, such as UnladenSwallow
need a bit more logic. In the end, you have a bunch of conforming Racer
types.
With all of the types conforming, you then create an array of racers.
Top Speed
找到所有Racer
里面速度最快的
Now it’s time to write a function that determines the top speed of the racers. Add this to the end of your playground:
1 | func topSpeed(of racers: [Racer]) -> Double { |
This function uses the standard library max
to find the racer with the largest speed and return that. You return 0 if the user passes in an empty array in for racers
.
Looks like it’s Swift 3 FTW. As if it were ever in doubt! :]
Making it more generic
让我们把它变得更通用一点,当你想传入切片到该函数的时候,这时候编译器报错了。
There is a problem though. Suppose you want to find the top speed for a subset (slice) of racers
. Adding this to your playground you get an error:
1 | topSpeed(of: racers[1...3]) // ERROR |
Swift complains it cannot subscript a value of type [Racer]
with an index of type CountableClosedRange
. Slicing returns one of those wrapper types.
解决办法就是
The solution is to write your code against a common protocol instead of the concrete Array
. Add the following before the topSpeed(of:)
call.
1 | func topSpeed(of racers: RacerType) -> Double where RacerType.Iterator.Element == Racer { |
This might look a bit scary, so let’s break it down. RacerType
is the generic type for this function and it can be any type that conforms to the Swift standard library’s Sequence
protocol. The where
clause specifies that the element type of the sequence must conform to your Racer
protocol. All Sequence
types have an associated type named Iterator
that can loop through types of Element
. The actual method body is mostly the same as before.
This method works for any Sequence
type including array slices.
1 | topSpeed(of: racers[1...3]) // 42 |
Making it More Swifty
You can do even a little better. Borrowing from the standard library play book, you can extend Sequence
type itself so that topSpeed()
is readily discoverable. Add the following to the end of your playground:
1 | extension Sequence where Iterator.Element == Racer { |
Now you have a method that is easily discoverable but only applies (and autocompletes) when you are dealing with sequences of racers.
Protocol Comparators
One Swift 3 improvement to protocols is how you create operator requirements.
Add the following to the bottom of the playground:
1 | protocol Score { |
Having a Score
protocol means that you can write code that treats all scores the same way. However, by having different concrete types such as RacingScore
you are sure not to mix up these scores with style scores or cuteness scores. Thanks compiler!
You really want scores to be comparable so you can tell who has the high score. Before Swift 3, you needed to add global operator functions to conform to these protocols. Now you can define these static method that is part of the model. Do so now by replacing the definition of Score
and RacingScore
with the following:
1 | protocol Score: Equatable, Comparable { |
You just encapsulated all of the logic for RacingScore
in one place. Now you can compare scores, and, with the magic of protocol extension default implementations, even use operators such as greater-than-or-equal-to that you never explicitly defined.
RacingScore(value: 150) >= RacingScore(value: 130) // true
Where To Go From Here?
You can download the complete playground with all the code in this tutorial
here.
You’ve seen the power of protocol-oriented programming by creating your own simple protocols and extending them using protocol extensions. With default implementations, you can give existing protocols common and automatic behavior, much like a base class but better since it can apply to structs and enums too.
In addition, protocol extensions can not only be used to extend your own protocols, but can extend and provide default behavior to protocols in the Swift standard library, Cocoa, Cocoa Touch, or any third party library.
To continue learning more about protocols, you should read the official Apple documentation.
You can view an excellent WWDC session on Protocol Oriented Programming on Apple’s developer portal for a more in-depth look into the theory behind it all.
The rationale for operator conformance can be found on the Swift evolution proposal. You might also want to learn more about Swift collection protocols and learn how to build your own.
Finally, as with any “new” programming paradigm, it is easy to get overly exuberant and use it for all the things. This interesting blog post by Chris Eidhof reminds us that we should beware of silver bullet solutions and using protocols everywhere “just because”.
Have any questions? Let us know in the forum discussion below!