obo.dev

Сабскрипты (Subscript)

05 Dec 2022

Сабскрипты (Subscript)

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

В массивах и словарях происходит та же работа - получение значения по индексу и по ключу соответственно. Запись [Key] - и есть сабскрипт.

Пример:

var array = [1, 2, 3]
array[0] 
var dict = ["key": "value"]
dict["key"]

Можно использовать сабскрипт для получения значения или установки нового значения элемента без разделения этих двух методов (получения значения и установкой нового). Например, можно использовать сабскрипт в экземпляре массива для получения значения элемента someArray[index] или в экземпляре словаря someDictionary[key].

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

Общая конструкция - Синтаксис сабскрипта

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

Синтаксис сабскрипта аналогичный синтаксису метода экземпляра и вычисляемому свойству.

Пример сабскрипта:

class className {
	subscript(index: TypeIndex) -> ReturnType {
		get {
			// return value
		}
		set(newValue) {
			// set value
		}
	}
}

Из чего состоит сабскрипт:

  • subscript(index: TypeIndex) -> ReturnType - объявление сабскрипта, с передаваемыми параметрами и типом возвращаемого значения;
  • get {...} - возвращает значение и используется для чтения значения;
  • set {...} - устанавливает новое значения, согласно передаваемым параметрам.

Объявление (определение) сабскрипта состоит из таких этапов.

  1. Сначала определяется сабскрипт с помощью ключевого слова subscript и указываете один или более входных параметров и возвращаемый тип, точно так же как и в методах экземпляра;
  2. Далее, внутри сабскрипта, определяются геттер и сеттер, в точности так же как и в вычисляемых свойствах;

Благодаря геттеру и сеттеру сабскрипты могут быть доступны как для чтения-записи (read-write), так и только для чтения (read-only).

Тип данных newValue в сеттере, такой же как и у возвращаемого значения сабскрипта. Как и в случае с вычисляемыми свойствами, можно не указывать параметр сеттера (newValue). Параметр по умолчанию называется newValue и предоставляется, если не было назначено другого. То есть, при определении сабскрипта его можно не указывать и просто использовать значение по умолчанию - newValue. Либо можно определить свой параметр, указав его в круглых скобках после сеттера, и далее использовать его.

Определение нередактируемого сабскрипта только для чтения (read-only subscript) такое же, как и в нередактируемых вычисляемых свойствах (read-only computed property). Для этого, сеттер не указывается. В таком случае, остается только геттер и ключевое слово get можно не указывать.

Пример:

subscript(index: Int) -> Int {
   // return value
}

Использование сабскрипта

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

К примеру, массив или словарь в языке Swift использует сабскрипт для присваивания или получения значения, которое хранится в экземпляре Array или Dictionary.

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

var numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]
numberOfLegs["bird"] = 2

В этом примере сначала был определён словарь, который указывает количества ног у существ. Тип словаря определится как [String: Int]. Существа заданы через ключи словаря, а количество ног - через значения. Потом добавлен новый элемент по ключу bird со своим значением - 2.

Сабскрипты типа (Type Subscripts)

Сабскрипты сущностей являются сабскриптами экземпляров конкретного типа. Можно определить сабскрипты, которые вызываются у самого типа. Сабскрипты такого типа называются сабскриптом типа (Type Subscripts). Для этого следует указывать сабскрипт типа при помощи ключевого слова static перед ключевым словом subscript. Классы могут использовать ключевое слово class вместо static, чтобы позволить подклассам переопределять реализацию родительского класса этого сабскрипта.

Пример ниже показывает, как можно определить и вызывать сабскрипт типа у перечисления:

enum Planet: Int {
    case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
    static subscript(n: Int) -> Planet {
        return Planet(rawValue: n)!
    }
}
let myPlanet = Planet[3]
print(myPlanet)
// earth

Сабскрипты и структуры

Создание структуры с сабскриптом:

struct Multy {
	var multyplier: Int
	
	subscript(index: Int) -> Int {
		get {
			return multiplier * index
		}
	}
}

Если в сабскрипте установить только get, то установить значение нельзя будет. В этом случае ключевое слово get можно не писать.

Для применения сабскрипта необходимо создать экземпляр структуры и вызвать сабскрипт:

var numResult = Multy(multiplier: 5)
numResult[10] // 50
numResult[20] // 100
numResult[25] // 125

Еще один пример со структурой:

struct Man {
	var man1 = "Man1 say: Hello"
	var man2 = "Man2 say: Hi!"
	var man3 = "Man3 say: How are you?"

	subscript(index: Int) -> String? {
		get {
			switch index {
				case 0: return man1
				case 1: return man2
				case 3: return man3

				default: return ""
			}
		}
		set {
			let value = newValue ?? ""

			switch index {
				case 0: return man1 = value
				case 1: return man2 = value
				case 3: return man3 = value

				default: return ""
			} 
		}
	}
}

var sayMan = Man()
print(sayMan.man1)
// Man1 say: Hello
print(sayMan[0])
// Optional("Man1 say: Hello")

sayMan[0] = "Hi Jim"
print(sayMan.man1)
// Hi Jim

При установке значения обратились к свойству man1 через сабскрипт и установили новое значение.

Иногда в работе с кодом проще обращаться через сабскрипт, чем через свойство.

Сабскрипты позволяют работать с элементами структур и классов как с коллекциями.

Пример:

struct Number {
    var array = [Int]()

    subscript(index: Int) -> Int {
        get {
            return array[index]
        }
        set (new) {
            array[index] = new
        }
    }
}

var element = Number(array: [2, 4, 6, 8])

Для обращения к свойству array с индексом 0 надо записать так:

print(element.array[0])
// 2

Но можно и по сабскрипту:

print(element[0])
// 2

Можно установить новое значение по сабскрипту:

element[3] = 20
print(element)
// Number(array: [2, 4, 6, 20])

Сабскрипты и классы

Создадим два класса: один будет описывать книгу, второй - библиотеку с книгами:

// класс книги 
class Book {
     
    var name: String
    
	init(name: String) {     
        self.name = name
    }
}

// класс библиотеки
class Library { 
     
    var books: [Book] = [Book]()
     
    init() {
        books.append(Book(name: "Война и мир"))
        books.append(Book(name: "Отцы и дети"))
        books.append(Book(name: "Чайка"))
    }
     
    subscript(index: Int) -> Book{
         
        get{
            return books[index]
        }
        set(newValue){
            books[index] = newValue
        }
    }
}

В классе библиотеки написан инициализатор, который при инициализации объекта библиотеки добавит в свойство books (массив книг по типу Book) три объекта класса книга.

Создадим объект библиотеки и получим элемент по индексу:

var myLibrary: Library = Library()
var firstBook: Book = myLibrary[0]  // получаем элемент по индексу 0
print(firstBook.name)
// Война и мир

Изменим у объекта библиотеки его свойство - установим элемент по индексу 2:

myLibrary[2] = Book(name: "Мартин Иден")    // установка элемента по индексу 2
// доступ к свойству имени объекта книги внутри свойства объекта библиотеки (Используя сабскрипт)
print(myLibrary[2].name)
// Мартин Иден
// доступ к свойству имени объекта книги внутри свойства объекта библиотеки (НЕ Используя сабскрипт)
print(myLibrary.books[2].name)
// Мартин Иден

Опции сабскрипта

Сабскрипты могут принимать любое количество входных параметров, и эти параметры могут быть любого типа. Сабскрипты так же могут возвращать любой тип. Этим они похожи на функции. Однако в отличие от функций, сабскрипты не могут использовать сквозные параметры.

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

Определение множественных сабскриптов так же известно как перегрузка сабскрипта.

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

Следующий пример определяет структуру Matrix, которая представляет собой двухмерную матрицу значений типа Double. Сабскрипт структуры Matrix принимает два целочисленных параметра.

Пример:

struct Matrix {
    let rows: Int, columns: Int
    var grid: [Double]
    init(rows: Int, columns: Int) {
        self.rows = rows
        self.columns = columns
        grid = Array(repeating: 0.0, count: rows * columns)
    }

    func indexIsValid(row: Int, column: Int) -> Bool {
        return row >= 0 && row < rows && column >= 0 && column < columns } 
    
    subscript(row: Int, column: Int) -> Double {
        get {
            assert(indexIsValid(row: row, column: column), "Index out of range")
            return grid[(row * columns) + column]
        }
        set {
            assert(indexIsValid(row: row, column: column), "Index out of range")
            grid[(row * columns) + column] = newValue
        }
    }
}

Структура Matrix предоставляет инициализатор, который принимает два параметра - rows и columns, - и создает массив типа Double, который имеет размер rows * columns. Каждой позиции в матрице дается начальное значение 0.0. Чтобы этого достичь, размер массива и начальное значение клетки равное 0.0 передаются в инициализатор массива, который создает и инициализирует новый массив необходимого размера.

Можно создать новый экземпляр типа Matrix, и передать количество рядов и столбцов в его инициализатор:

var matrix = Matrix(rows: 2, columns: 2)

Этот пример создает новый экземпляр Matrix, который имеет всего два ряда и два столбца. Массив grid для экземпляра Matrix фактически упрощенный вариант версии матрицы, который читается с левой верхней части в правую нижнюю часть: первые два элемента массива grid соответствуют первой строке матрицы, а следующие два элемента массива grid - второй строке матрицы.

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

matrix[0, 1] = 2.5
matrix[1, 0] = 4.8

Эти два выражения в сеттере сабскрипта устанавливают значения 2.5 для верхней правой позиции (где row равен 0, column равен 1), и значение 4.8 для нижней левой позиции (где row равен 1, а column равен 0.

Массив grid станет такого вида: [0.0, 2.5, 4.8, 0].

Геттер и сеттер сабскрипта Matrix содержат утверждения (assert) для проверки валидности значений row и column. Для помощи утверждениям, у Matrix есть удобный метод под названием indexIsValid(row:column:), который проверяет наличие запрашиваемых row и column в существующей матрице:

func indexIsValid(row: Int, column: Int) -> Bool {
  return row >= 0 && row < rows && column >= 0 && column < columns
}

Утверждения (assert) срабатывают, если попытаться получить доступ к сабскрипту по значениям, которые находится за пределами матрицы:

let someValue = matrix[2, 2]
// Assertion failed: Index out of range

Это вызывает утверждение (assert), потому что [2, 2] находится за пределами матрицы.

Пример использования сабскрипта: Список и Узлы

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

Создам класс списка (List) и класс узлов (Node).

У списка есть:

  • head - вершина - свойство, которое означает начало списка (опционально - так как его может и не быть при пустом инициализаторе);
  • lenght - длина списка;
  • метод добавления узла в конец списка;
  • метод удаления последнего узла в списке и возврат его значения

У узла есть:

  • имя узла
  • следующий узел (опционально - так как его может и не быть)

Пример:

class Node {
    var name: String
    var next: Node?
    
    // Если есть следующий узел
    init(name: String, next: Node?) {
        self.name = name
        self.next = next
    }
    // Если нет следующего узла
    init(name: String) {
        self.name = name
        self.next = nil
    }
}

class List {
    // Вершина списка
    var head: Node?
	// Длина списка
    var lenght: Int {
        var count = 0
        guard var node = head else { return 0 }
        count += 1
        while node.next != nil {
            node = node.next!
            count += 1
        }
        return count
    }
    
    subscript(index: Int) -> Node {
        guard var currentNode = head else { fatalError("Index out of range!") }
        for _ in 0..<index {
            if currentNode.next != nil {
                currentNode = currentNode.next!
            } else {
                fatalError("Index out of range!")
            }
        }
        return currentNode
    }
    
    func add(node: Node) {
		// Проверка если вершины списка нет 
        if head == nil {
            head = node
            return
        }
		// Перебор всего списка до последнего узла
        var currentNode = head
        while currentNode != nil {
            currentNode = currentNode?.next
        }
		// Присвоение свойству next последнего узла значение нового добавляемого узла 
        currentNode?.next = node
    }

    func removeLast() -> Node {
        var node = head
        var nodePrevios = head
        
        while node?.name != nil {
            nodePrevios = node
            node = node?.next
        }
        
        nodePrevios?.next = nil
        return node!
    }
}

Создадим экземпляры классов и получим длину списка после инициализации:

let bob = Node(name: "Bob")
let nameList = List()
// Получаем длинну списка
print(nameList.lenght)
// 0

Добавляем узел в список и проверяем длину списка:

nameList.add(node: bob)
print(nameList.lenght)
// 1

Получаем доступ к свойству узла списка через сабскрипт по индексу:

let someNode = nameList[0]
print(someNode.name)
// Bob

Если обратиться к несуществующему индексу - будет ошибка:

let someNodeTwo = nameList[1] // Error: Index out of range!

Вот такой пример создания списка с узлами и использования сабскриптов.


Еще полезные ссылки

Также информацию по сабскриптам можно получить на странице официальной документации.

Ссылки на официальную документацию: