神刀安全网

Implementing Custom Subscripts in Swift

Implementing Custom Subscripts in Swift

Power up your own types with native-looking subscripts!

Update note: This tutorial was updated for Swift 2.2 and Xcode 7.3 by Mikael Konutgan.Original post by Tutorial Team memberEvan Dekhayser.

Subscripts are a powerful language feature that, when used properly, can significantly enhance the convenience factor and readability of your code.

Likeoperator overloading, subscripts let you use native Swift constructs: something like checkerBoard[2][3] rather than the more verbose checkerBoard.objectAt(x: 2, y: 3) .

In this tutorial, you’re going to explore subscripts by building the foundations for a basic checkers game in a playground. You’ll see how easy it is to use subscripting to move pieces around the board. When you’re done, you’ll be well on your way to building a new game to keep your fingers occupied during all of your spare time.

Oh, and you’ll know a lot more about subscripts too! :]

Note: This tutorial assumes you already know the basics of Swift development. If you are new to Swift, check out some of our beginner Swift tutorials or read theSwift Apprentice first.

Getting Started

Create a new playground and add the following code:

struct Checkerboard {     enum Square: String {     case Empty = "/u{25AA}/u{fe0f}" // Black square     case Red = "/u{1f534}"          // Red piece     case White = "/u{26AA}/u{fe0f}" // White piece   }     typealias Coordinate = (x: Int, y: Int)     private var squares: [[Square]] = [     [ .Empty, .Red,   .Empty, .Red,   .Empty, .Red,   .Empty, .Red   ],     [ .Red,   .Empty, .Red,   .Empty, .Red,   .Empty, .Red,   .Empty ],     [ .Empty, .Red,   .Empty, .Red,   .Empty, .Red,   .Empty, .Red   ],     [ .Empty, .Empty, .Empty, .Empty, .Empty, .Empty, .Empty, .Empty ],     [ .Empty, .Empty, .Empty, .Empty, .Empty, .Empty, .Empty, .Empty ],     [ .White, .Empty, .White, .Empty, .White, .Empty, .White, .Empty ],     [ .Empty, .White, .Empty, .White, .Empty, .White, .Empty, .White ],     [ .White, .Empty, .White, .Empty, .White, .Empty, .White, .Empty ]   ] }   extension Checkerboard: CustomStringConvertible {   var description: String {     return squares.map { row in row.map { $0.rawValue }.joinWithSeparator("") }                   .joinWithSeparator("/n") + "/n"   } }

Checkerboard contains three definitions:

  • Square represents the state of a square on the board. .Empty represents an empty square while .Red and .White represent the presence of a red or white piece on that square.
  • Coordinate is an alias for a tuple of two integers. You will use this type to access the squares on the board.
  • squares is the two-dimensional array that stores the state of the board.

Finally, there’s an extension to add conformance to CustomStringConvertible that lets you print a checkerboard to the console.

Open the console using View/Debug Area/Show Debug Area , then enter the following lines at the bottom of the playground:

var checkerboard = Checkerboard() print(checkerboard)

This code initializes an instance of Checkerboard then prints the description property of the CustomStringConvertible implementation to the console. The output in your console should look like this:

:black_small_square::black_small_square::black_small_square::black_small_square: :black_small_square::black_small_square::black_small_square::black_small_square: :black_small_square::black_small_square::black_small_square::black_small_square: :black_small_square::black_small_square::black_small_square::black_small_square::black_small_square::black_small_square::black_small_square::black_small_square: :black_small_square::black_small_square::black_small_square::black_small_square::black_small_square::black_small_square::black_small_square::black_small_square: :white_circle:️:black_small_square::white_circle:️:black_small_square::white_circle:️:black_small_square::white_circle:️:black_small_square: :black_small_square::white_circle:️:black_small_square::white_circle:️:black_small_square::white_circle:️:black_small_square::white_circle:️ :white_circle:️:black_small_square::white_circle:️:black_small_square::white_circle:️:black_small_square::white_circle:️:black_small_square:

Getting And Setting Pieces

Looking at the console, it’s pretty easy for you to know what piece occupies a given square, but your program doesn’t have those powers yet. It can’t know which player is at a specified coordinate because the squares array is marked as private . There’s an important point to make here: the squares array is the implementation of the the board. However, the user of type Checkerboard shouldn’t know anything about the implementation of that type.

A type should shield its users from its internal implementation details; that’s why the squares array is kept private.

With that in mind, you’re going to add two methods to Checkerboard to find and set a piece at a given coordinate.

Add the following methods to Checkerboard , after the spot you assign the squares array:

func pieceAt(coordinate: Coordinate) -> Square {   return squares[coordinate.y][coordinate.x] }   mutating func setPieceAt(coordinate: Coordinate, to newValue: Square) {   squares[coordinate.y][coordinate.x] = newValue }

Notice how the squares array is accessed – using a Coordinate tuple – rather than accessing the array directly. The actual storage mechanism of an array-of-arrays is exactly the kind of implementation detail the user should be shielded from!

Defining Subscripts

You may have noticed these methods look an awful lot like a property getter and setter combination. Maybe they should be implemented as a computed property instead? Unfortunately, that won’t work. Your methods require a coordinate parameter, and computed properties can’t have parameters. Does that mean you’re stuck with methods?

Well no – this special case is exactly what subscripts are for! :]

Look at how you define a subscript:

subscript(parameterList) -> ReturnType {   get {     // return someValue of ReturnType   }     set (newValue) {     // set someValue of ReturnType to newValue   } }

Subscript definitions mix both function and computed property definition syntax:

  • The first part looks a lot like a function definition, with a parameter list and a return type. Instead of the func keyword and function name, you use the special subscript keyword.
  • The main body looks a lot like a computed property, with a getter and a setter.

This combination of function and property syntax highlights the power of subscripts: to provide a shortcut to accessing the elements of an indexed collection. You’ll learn more about that soon, but first, consider the following example.

Replace methods pieceAt(_:) and setPieceAt(_:to:) with the following subscript:

subscript(coordinate: Coordinate) -> Square {   get {     return squares[coordinate.y][coordinate.x]   }   set {     squares[coordinate.y][coordinate.x] = newValue   } }

The getter and setter of this subscript are implemented exactly like the methods they replace:

  • Given a Coordinate , the getter returns the square at the column and row.
  • Given a Coordinate and value, the setter accesses the square at the column and row and replaces its value.

Give your new subscript a test drive by adding the following code to the end of the playground:

let coordinate = (x: 3, y: 2) print(checkerboard[coordinate]) checkerboard[coordinate] = .White print(checkerboard)

The playground will tell you the piece at (3, 2) is red. After changing it to white, the output in the console will be:

:black_small_square::black_small_square::black_small_square::black_small_square: :black_small_square::black_small_square::black_small_square::black_small_square: :black_small_square::black_small_square::white_circle:️:black_small_square::black_small_square: :black_small_square::black_small_square::black_small_square::black_small_square::black_small_square::black_small_square::black_small_square::black_small_square: :black_small_square::black_small_square::black_small_square::black_small_square::black_small_square::black_small_square::black_small_square::black_small_square: :white_circle:️:black_small_square::white_circle:️:black_small_square::white_circle:️:black_small_square::white_circle:️:black_small_square: :black_small_square::white_circle:️:black_small_square::white_circle:️:black_small_square::white_circle:️:black_small_square::white_circle:️ :white_circle:️:black_small_square::white_circle:️:black_small_square::white_circle:️:black_small_square::white_circle:️:black_small_square:

You can now find out which piece is at a given coordinate, and set it, by using checkerboard[coordinate] in both cases. A shortcut indeed!

Comparing Subscripts, Properties And Functions

Subscripts are similar to computed properties in many regards:

  • They consist of a getter and setter.
  • The setter is optional, meaning a subscript can be either read-write or read-only.
  • A read-only subscript doesn’t explicitly state get or set ; the entire body is a getter.
  • In the setter, there’s a default parameter newValue with a type that equals the subscript’s return type. You typically only declare this parameter when you want to change its name to something other than newValue .
  • Users expect subscripts to be fast, preferably O(1), so keep them short and sweet!

Implementing Custom Subscripts in Swift

The major difference with computed properties is that subscripts don’t have a property name per se. Likeoperator overloading, subscripts let you override the language-level square brackets [] usually used for accessing elements of a collection.

Subscripts are similar to functions in that they have a parameter list and return type, but they differ on the following points:

  • Subscript parameters don’t have external names by default. If you want to use them, you’ll need to explicitly add them.
  • Subscripts cannot use inout or default parameters. However, variadic ( ... ) parameters are allowed.
  • Subscripts cannot throw errors. This means a subscript getter must report errors through its return value and a subscript setter cannot throw or return any errors at all.

Adding a Second Subscript

There is one other point where subscripts are similar to functions: they can be overloaded. This means a type can have multiple subscripts, as long as they have different parameter lists or return types.

Add the following code after the existing subscript definition in Checkerboard :

subscript(x: Int, y: Int) -> Square {   get {     return self[(x: x, y: y)]   }   set {     self[(x: x, y: y)] = newValue   } }

This code adds a second subscript to Checkerboard that two integers rather than a Coordinate tuple. Notice how the second subscript is implemented using the first through self[(x: x, y: y)] .

Try out this new subscript by adding the following lines to the end of the playground:

print(checkerboard[1, 2]) checkerboard[1, 2] = .White print(checkerboard)

You should see the piece at (1, 2) change from red to white.

Where to Go From Here?

You can download the completed playground for this tutorialhere.

Why not extend this game a bit further with some gameplay logic and turn this into a playable checkers game? Here’s a nice checkerboard collection view cell class to get you started.

Now that you’ve added subscripts to your toolkit, look for opportunities to use them in your own code. When used properly, they can make your code more readable and intuitive. That being said, you don’t always want to revert to subscripts. If you’re writing an API, your users are used to using subscripts to access elements of an indexed collection. Using them for other things will likely feel unnatural and forced.

For more details, check out the subscripts chapter of The Swift Programming Language by Apple for further information on subscripts.

If you have any questions or comments, please leave them below!

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Implementing Custom Subscripts in Swift

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址