Storing and using references to types, including
init
We can put a static function declaration in a protocol.
protocol MyProtocol {
static func doSomething()->String
}
as easily as any other type. But you may be wondering, how would we ever use it? Static methods aren't called from instances, and without an instance, how do we know which type we have?
var someType:MyProtocol.Type?
To store a reference to a type, or to a type that conforms to a protocol, use the type or protocol name, followed by
.Type
. Here, we've also made it optional.
struct MyStruct : MyProtocol {
static func doSomething()->String {
return "MyStruct did something"
}
}
someType = MyStruct.self
We provide a value for this type by using a type name, and then
.self
.
let text:String? = someType?.doSomething() //"MyStruct did something"
Calling the static method then proceeds like any other method call on an optional type:
struct MyIncompatibleType {
static func doSomething()->String {
return "MyIncompatibleType did something"
}
}
someType = MyIncompatibleType.self //compile error, type mismatch
We cannot provide a reference to a type that does not conform to the protocol.
I've found cases where a reference to a type of class was useful, but what's more useful is to be able to initialize objects of different types depending on some kind of other value.
protocol ExpressibleByJSON {
init(json:[String:Any])throws
}
To do that, we'll need to declare an
init
method in a protocol.
struct KeySignature:ExpressibleByJSON {
let baseNote:Int
init(json:[String:Any]) throws {
//...
}
}
For a struct, we just implement the init method. That's all.
But for a class, we need to think about subclasses.
class KeySignature:ExpressibleByJSON {
let baseNote:Int
required init(json:[String:Any]) throws {
//...
}
}
``
class MajorKeySignature : KeySignature {
//...
}
A subclass conforms to all the protocols its super class conforms to, which means it needs to respond to the
init(json:...
method. Since it isn't usual to inherit initializers in this way, we have to use the
required
keyword.
class MajorKeySignature : KeySignature {
required init(json:[String:Any]) throws {
//...
}
}
This means all subclasses are "required" to implement this initializer.
That completes the implementation. Here's how we use it.
Let's say we want to allocate instances of the best "key signature" given some JSON.
class MinorKeySignature : KeySignature {
required init(json:[String:Any]) throws {
//...
}
}
extension KeySignature {
static var signatureTypes:[String:KeySignature.Type] = [
"major":MajorKeySignature.self
,"minor":MinorKeySignature.self
]
static func new(json:[String:Any])throws->KeySignature {
if let commonType = json["type"] as? String
,let initingType = signatureTypes[commonType]
{
return try initingType.init(json:json)
}
return try KeySignature(json:json)
}
}
So, let's look at this dynamic initialization. We've made a
static
function on our
KeySignature
type. It extracts a single value from our JSON, and uses that as a key into a
Dictionary
of types. That
Dictionary
is also
static
l it's a property. It then calls the
init
method on the type to create an instance of the particular
KeySignature
concrete type.
class KeySignature
//...
var json:[String:Any] {
return ["base":baseNote]
}
class MajorKeySignature ...
override var json:[String:Any] {
var superJSON:[String:Any] = super.json
superJSON["type"] = "major"
return superJSON
}
One principle of writing great code is "Don't Repeat Yourself", or "DRY". We've repeated critical keys into JSON objects as string literals. Here's some serialization code that turns the
KeySignature
s back into
JSON
, we need the same keys there. These keys also appear as "magic values" in the code. I know Apple is big on magic, but "magic values" are bad, because they don't convey intention, and they're easy to break and not realize it. So let's make these more semantic.
extension KeySignature {
struct JSONKeys {
static var type:String = "type"
static var majorType:String = "major"
static var minorType:String = "minor"
static var baseNote:String = "base"
}
static var signatureTypes:[String:KeySignature.Type] = [
JSONKeys.majorType : MajorKeySignature.self
,JSONKeys.minorType : MinorKeySignature.self
]
static func new(json:[String:Any])throws->KeySignature {
if let commonType = json[JSONKeys.type] as? String
,let initingType = signatureTypes[commonType]
{
return try initingType.init(json:json)
}
return try KeySignature(json:json)
}
}
class KeySignature
//...
var json:[String:Any] {
return [JSONKeys.baseNote:baseNote]
}
class MajorKeySignature ...
override var json:[String:Any] {
var superJSON:[String:Any] = super.json
superJSON["type"] = "major"
return superJSON
}
We've added a nested type,
JSONKeys
, with
static
var
s that provide the key values for the JSON we'll be transforming into
KeySignature
s. In our serialization code somewhere else, we'd use these same references, removing the repetition of the magic values.
Unlike most of the convenient features we've added in Swift, moving our JSON key constants into static properties of a nested type added more text instead of making our code shorter. But, we've removed the magic values by making their use more semantic, and avoided repeating ourselves if we use these values other places in code. We've also name-spaced the JSON keys. If I have another type, a TimeSignature, for example, it can contain its own JSONKeys nested struct, with its own static properties.
Option Sets
Declaring OptionSets
Ok, one more piece of magic with statics. In C-based languages, it's common to pass a number of boolean conditions by packing them as bits in an integer type. Apple's Cocoa API's often call these "options". Using them usually meant taking a break from Obj-C coding to review the bitwise operators semantics, and then conjure some magic formulas to combine, remove or mask specific values or ranges.
In Swift,
Set
s are built in to the language, so it would be very nice if we had some kind of optimized way to pack
Set
s of
Bool
s into an
Int
, and that's exactly what an
OptionSet
is.
struct GZFlags : OptionSet {
}
First of all, we create a new
struct
, and have it inherit from the
OptionSet
protocol. This will be part of a GZip file reader, which has an 8-bit options field.
If we know how many options we'll need, we know how big our
Int
needs to be. For our use, we'll pick
UInt8
, which as you'll remember, is a 1-byte unsigned integer.
struct GZFlags : OptionSet {
let rawValue:UInt8
}
We add a
rawValue
field with the type that you want backing the options. This uses a feature called "associated types" for
protocol
s, which we'll cover in a later week.
struct GZFlags : OptionSet {
var rawValue: UInt8
static let isText:GZFlags = GZFlags(rawValue:1)
static let headerCRC:GZFlags = GZFlags(rawValue:2)
static let hasExtraFields:GZFlags = GZFlags(rawValue:4)
static let hasFileName:GZFlags = GZFlags(rawValue:8)
static let hasComment:GZFlags = GZFlags(rawValue:16)
}
As the last step, we add as many literal values as we like. Each is a static property, and initialized with an integer value that turns on a single bit. The magic of protocols and extension defaults and associated types is what enables this default
init(rawValue:
method to exist.
Using OptionSets
Suppose we have an function with a GZFlags as an argument:
func writeBlock(options:GZFlags) {
//...
}
We'll take advantage of two features of Swift to provide values.
writeBlock(options:[])
1) We'll use an array to initialize the Set. Swift doesn't provide a dedicated Set-initialization syntax, so we'll create an array of values.
2) We'll take advantage of Swift's type inference to reference merely the static property names.
writeBlock(options:[.isText, .hasComment])
Notice that we're not writing lots of
<<
,
&
or
|
bit-wise operators to combine these values with "masks", we're providing a list of semantic constants.
func writeBlock(options:GZFlags) {
if options.contains(.isText) {
...
When reading values from the
OptionSet
, we're also not using bit-wise operations. We use standard
SetAlgebra
functions, like
.contains
, or
.intersect
.
As you read through the Swift standard module, and the included Foundation, and Dispatch modules, you'll see OptionSets are frequently nested types.
Summary
Today, we learned how to use statics from protocols with type references. We learned to DRY our code and eliminate magic values by creating static properties on nested types. Last, we learned how to replace bit-flag integers with OptionSet types.
Treating bit-flag-packed integers as Sets, now that's Swift!
Comments for Making Use of Statics