Книга: Swift. Основы разработки приложений под iOS, iPadOS и macOS. 5-е изд. дополненное и переработанное
Назад: 25. Свойства
Дальше: 27. Наследование

26. Сабскрипты

С сабскриптами вы уже знакомы. Мы использовали их при работе с массивами. Там сабскриптом был индекс, указываемый для доступа к значению элементов. Однако сабскрипты позволяют также упростить работу со структурами и классами.

26.1. Назначение сабскриптов

С помощью сабскриптов структуры и классы можно превратить в некое подобие коллекций. Таким образом можно организовать доступ к свойствам экземпляра с использованием специальных ключей (индексов).

Предположим, что нами разработан класс Chessboard, моделирующий сущность «шахматная доска». Экземпляр данного класса хранится в переменной desk:

var desk = Chessboard()

В одном из свойств данного экземпляра содержится информация о том, какая клетка поля какой шахматной фигурой занята. Для доступа к информации относительно определенной клетки мы можем разработать специальный метод, которому в качестве входных параметров будут передаваться координаты:

desk.getCellInfo("A", 5)

С помощью сабскриптов можно организовать доступ к ячейкам клетки, передавая координаты, подобно ключам массива, непосредственно экземпляру класса:

desk["A", 5]

ПРИМЕЧАНИЕ Сабскрипты доступны для структур и классов.

26.2. Синтаксис сабскриптов

В своей реализации сабскрипты являются чем-то средним между методами и вычисляемыми свойствами. От первых им достался синтаксис определения выходных параметров и типа возвращаемого значения, от вторых — возможность создания геттера и сеттера.

Синтаксис

subscript(входные_параметры) —> тип_возвращаемого_значения {

    get{

        // тело геттера

    }

    set(ассоциированныйПараметр){

        // тело сеттера

    }

}

Сабскрипты объявляются в теле класса или структуры с помощью ключевого слова subscript. Далее указываются входные параметры (в точности так же, как у методов) и тип значения. Входные параметры — это значения, которые передаются в виде ключей. Тип значения указывает на тип данных устанавливаемого (в случае сеттера) или возвращаемого (в случае геттера) значения.

Тело сабскрипта заключается в фигурные скобки и состоит из геттера и сеттера по аналогии с вычисляемыми значениями. Геттер выполняет код при запросе значения с использованием сабскрипта, сеттер — при попытке установить значение.

Сеттер также дает возможность дополнительно указать имя ассоциированного параметра, которому будет присвоено устанавливаемое значение. Если данный параметр не будет указан, то новое значение автоматически инициализируется локальной переменной newValue. При этом тип данных параметра будет соответствовать типу возвращаемого значения.

Сеттер является необязательным, и в случае его отсутствия может быть использован сокращенный синтаксис:

subscript(входные_параметры) —> возвращаемое_значение {

// тело геттера

}

Сабскрипты поддерживают перегрузку, то есть в пределах одного объектного типа может быть определено произвольное количество сабскриптов, различающихся лишь набором входных аргументов.

ПРИМЕЧАНИЕ С перегрузками мы встречались, когда объявляли несколько функций с одним именем или несколько инициализаторов в пределах одного объектного типа. Каждый набор одинаковых по имени объектов отличался лишь набором входных параметров.

Для изучения сабскриптов вернемся к теме шахмат и создадим класс, описывающий сущность «шахматная доска». При разработке модели шахматной доски у нее можно выделить одну наиболее важную характеристику: коллекцию игровых клеток с указанием информации о находящихся на них шахматных фигурах. Не забывайте, что игровое поле — это матрица, состоящая из отдельных ячеек.

В данном примере будет использоваться созданный ранее тип Chessman, описывающий шахматную фигуру, включая вложенные в него перечисления.

При разработке класса шахматной доски реализуем метод, устанавливающий переданную ему фигуру на игровое поле. При этом стоит помнить о двух моментах:

• фигура, возможно, уже находилась на поле, а значит, ее требуется удалить со старой позиции;

• фигура имеет свойство сoordinates, которое также необходимо изменять.

В листинге 26.1 показан код класса gameDesk, описывающего шахматную доску.

Листинг 26.1

class gameDesk {

    var desk: [Int:[String:Chessman]] = [:]

    init(){

        for i in 1...8 {

            desk[i] = [:]

        }

    }

    func setChessman(chess: Chessman , coordinates: (String, Int)){

        let rowRange = 1...8

        let colRange = "A"..."Z"

        if( rowRange.contains( coordinates.1 ) && colRange.contains

                             ( coordinates.0 )) {

            self.desk[coordinates.1]![coordinates.0] = chess

            chess.setCoordinates(char: coordinates.0,

                                 num: coordinates.1)

        } else {

            print("Coordinates out of range")

        }

    }

}

var game = gameDesk()

var queenBlack = Chessman(type: .queen, color: .black, figure:

                 "\u{265B}", coordinates: ("A", 6))

game.setChessman(chess: queenBlack, coordinates: ("B",2))

queenBlack.coordinates //(.0 "B", .1 2)

game.setChessman(chess: queenBlack, coordinates: ("A",3))

queenBlack.coordinates //(.0 "A", .1 3)

Класс gameDesk описывает игровое поле. Его единственным свойством является коллекция клеток, на которых могут располагаться шахматные фигуры (экземпляры класса Chessman).

При создании экземпляра свойству desk устанавливается значение по умолчанию «пустой словарь». Во время работы инициализатора в данный словарь записываются значения, соответствующие номерам строк на шахматной доске. Это делается для того, чтобы обеспечить безошибочную работу при установке фигуры на шахматную клетку. В противном случае при установке фигуры нам пришлось бы сначала узнать состояние линии (существует ли она в словаре), а уже потом записывать фигуру на определенные координаты.

Метод setChessman(chess:coordinates:) не просто устанавливает ссылку на фигуру в свойство desk, но также проверяет переданные координаты на корректность и устанавливает их в экземпляре фигуры.

Пока что в классе GameDesk отсутствует возможность запроса информации о произвольной ячейке. Реализуем ее с использованием сабскрипта (листинг 26.2). В сабскрипт будут передаваться координаты необходимой ячейки в виде двух отдельных входных аргументов. Если по указанным координатам существует фигура, то она возвращается, в противном случае возвращается nil.

Листинг 26.2

class GameDesk {

    var desk: [Int:[String:Chessman]] = [:]

    init(){

        for i in 1...8 {

            desk[i] = [:]

        }

    }

    //сабскрипт из листинга 2

    subscript(alpha: String, number: Int) -> Chessman? {

        get{

            return self.desk[number]![alpha]

        }

    }

    func setChessman(chess: Chessman , coordinates: (String, Int)){

        let rowRange = 1...8

        let colRange = "A"..."Z"

        if( rowRange.contains( coordinates.1 ) && colRange.contains

                             ( coordinates.0 )) {

            self.desk[coordinates.1]![coordinates.0] = chess

            chess.setCoordinates(char: coordinates.0, num:

                                       coordinates.1)

        } else {

            print("Coordinates out of range")

        }

    }

}

var game = GameDesk()

var queenBlack = Chessman(type: .queen, color: .black, figure:

                 "\u{265B}", coordinates: ("A", 6))

game.setChessman(chess: queenBlack, coordinates: ("A",3))

game["A",3]?.coordinates //(.0 "A", .1 3)

Реализованный сабскрипт имеет только геттер, причем в данном случае можно было использовать краткий синтаксис записи (без ключевого слова get).

Так как сабскрипт возвращает опционал, перед доступом к свойству coordinates возвращенной шахматной фигуры необходимо выполнить извлечение опционального значения.

Теперь мы имеем возможность установки фигур на шахматную доску с помощью метода setChessman(chess:coordinates:) и получения информации об отдельной ячейке с помощью сабскрипта.

Мы можем расширить функционал сабскрипта, добавив в него сеттер, позволяющий устанавливать фигуру на новые координаты (лис­тинг 26.3).

Листинг 26.3

class GameDesk {

    var desk: [Int:[String:Chessman]] = [:]

    init(){

        for i in 1...8 {

            desk[i] = [:]

        }

    }

    subscript(alpha: String, number: Int) -> Chessman? {

        get{

            return self.desk[number]![alpha]

        }

        set{

            if let chessman = newValue {

                self.setChessman(chess: chessman, coordinates:

                                (alpha, number))

            }else{

                self.desk[number]![alpha] = nil

            }

        }

    }

    func setChessman(chess: Chessman , coordinates: (String, Int)){

        let rowRange = 1...8

        let colRange = "A"..."Z"

        if( rowRange.contains( coordinates.1 ) && colRange.contains

                             ( coordinates.0 )) {

            self.desk[coordinates.1]![coordinates.0] = chess

            chess.setCoordinates(char: coordinates.0, num:

                                       coordinates.1)

        } else {

            print("Coordinates out of range")

        }

    }

}

var game = GameDesk()

var queenBlack = Chessman(type: .queen, color: .black, figure:

                 "\u{265B}", coordinates: ("A", 6))

game["C",5] = queenBlack

game["C",5] // Chessman

game["C",5] = nil

game["C",5] // nil

Тип данных переменной newValue в сеттере сабскрипта соответствует типу данных возвращаемого сабскриптом значения (Chessman?). По этой причине необходимо извлечь значение перед тем, как установить фигуру на шахматную клетку.

ПРИМЕЧАНИЕ Запомните, что сабскрипты не могут использоваться как хранилища, то есть через них мы организуем только доступ к хранилищам значений.

Сабсрипты действительно привносят в Swift много интересного. Согласитесь, что к сущности «шахматная доска» обращаться намного удобнее через индексы, чем без них.

Реализовать шахматную доску и шахматные фигуры с помощью Swift можно большим количеством способов. Все зависит от того, как вы будете использовать созданные экземпляры в вашей программе. К примеру, можно отказаться от указания координат в типе Chessman и работать с ними исключительно в рамках класса GameDesk. Но это, в свою очередь, может создать проблемы: вы не сможете напрямую обратиться к фигуре, например, для проверки ее наличия на поле. Каждый раз вам потребуется обходить все клетки поля. С другой стороны, такой подход поможет избежать дублирования, когда необходимо следить за тем, чтобы информация была актуальна и в экземпляре типа GameDesk, и в экземпляре типа Chessaman.

Назад: 25. Свойства
Дальше: 27. Наследование