obo.dev

Структуры (Structure)

10 Dec 2022

Структуры (Structure)

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

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

var name: String = "Bob"
var age: Int = 35

В этом случае эти переменные никак не связаны. И чтоб их как-то связать с одним объектом (человеком), можно указать общую характеристику в имени переменных. Например:

var namePersonOne: String = "Bob"
var agePersonOne: Int = 35

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

Чтоб объединить эти данные для одного объекта человека (который представлен переменной), можно использовать кортежи и хранить в одной переменной несколько характеристик:

var personOne = (name: "Bob", age: 35)

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

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

Исходя из этого, требуется механизм, позволяющий гибко описывать даже самые сложные сущности, и который учитывает все возможные параметры. Структуры как раз и являются таким механизмом. Они позволяют создать определённый шаблон (“скелет”) сущности. Например, структура Int описывает сущность “целое число” со всеми доступными свойствами и методами для этого типа данных.

Таким образом, структура в Swift используется для хранения разных типов данных и объединения их для определенных объектов.

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

Синтаксис структуры

Синтаксис для объявления структуры:

struct StructureName {
    // structure definition 
}

Здесь,

  • struct - ключевое слово, используемое для объявления структуры;
  • StructName - имя структуры;
  • {...} - тело структуры (блок кода), в котором определены свойства и методы для данной структуры.

Объявляя структуру - объявляется новый тип данных. Поэтому требования к имени структуры предъявляются такие же, как и для других типов данных. Имя должно указываться в “верхнем верблюжьем регистре” (Upper Camel Case).

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

Объявим структуру, которая будет описывать сущность “человек” из примера выше:

struct Person {

}

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

Экземпляры структур

Определение структуры — это всего лишь план. Чтобы использовать структуру, нам нужно создать ее экземпляр.

Структура, как тип данных, описывает какими характеристиками должен обладать её экземпляр. Экземпляр - это определённый представитель структуры с конкретными характеристиками и данными, которые описаны в структуре.

Структуру можно представить как чертеж дома. А конкретное здание, которое построенное по этому чертежу - экземпляр структуры. В чертеже указано, что необходимы двери, а в доме определены конкретные двери - например, деревянные или металлические.

В объектно-ориентированном программировании (ООП) происходит взаимодействие между объектами. Объектом является определённый экземпляр какого-то типа данных (например структуры, класса).

Так как объект является экземпляром какого-то типа данных, то понятие объекта и экземпляра часто рассматриваются как синонимы.

Более детально про ООП можно узнать в отдельной статье. См. ссылки внизу.

Для того, чтоб создать объект, необходимо объявить переменную (константу) и инициализировать её со значением экземпляра определенного типа. Для создания экземпляра необходимо указать тип данных и в круглых скобках указать параметры инициализации. В данном случае, экземпляр создается как Person().

Создадим объект personOne, который будет экземпляром структуры Person:

var personOne = Person()

print(type(of: personOne))

// Output
// Person.Type

Можно увидеть, что в консоль выведется тип данных объекта personOne - Person.Type. То есть тип данных был автоматически определён как Person.

Так же можно объявить объект в полной форме (с указанием типа данных):

var personOne: Person = Person()

В этом примере явно указан тип данных Person для константы personOne.

Объекты в ООП могут хранить данные и взаимодействовать между собой. Для хранения данных используются переменные или константы, которые называются свойствами объекта. А для взаимодействия используются функции, которые называются методами объекта.

Свойства в структурах

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

Синтаксис для указания свойств:

struct StructureName {
    var propertyName: PropertyType
	... 
}
  • var propertyName: PropertyType - указано свойство propertyName с типом данных PropertyType.

Возвращаясь к примеру, добавляем два свойства, которые описывают имя и возраст:

struct Person {
    var name: String
    var age: Int
}

Здесь определяем структуру с именем Person. В теле структуры (внутри фигурных скобок {}) содержится два свойства name и age с указанными типами String и Int соответственно.

Встроенный инициализатор. Поэлементный инициализатор (Memberwise Initializer)

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

// create instance of struct
var personOne = Person.init(name: "Bob", age: 35)

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

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

Свойства в инициализаторе указываются в том же порядке, в котором они указаны при объявлении структуры.

Имя инициализатора (init) можно не указывать при инициализации.

// create instance of struct
var personOne = Person(name: "Bob", age: 35)

Значения свойств по умолчанию

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

struct Person {
    var name: String
    var age: Int = 18
}

Будут созданы два инициализатора, которые будут доступны при создании экземпляра структуры Person:

  • (name:) - требует указать значение для свойства name, так как для него не задано значение по умолчанию;
  • (name:,age:) - требует указать оба значения.

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

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

Пример с пустым инициализатором:

struct Person {
    var name: String = "SomeName"
    var age: Int = 18
}

var personUnknown = Person()

В этом примере у объекта personUnknown свойства будут проинициализированы со значениями по умолчанию (name = "SomeName", age = 18).

Структура как пространство имён

Структура образует отдельное пространство имён; поэтому для доступа к элементам этого пространства имён необходимо получить доступ к самому пространству. Для этого необходимо указать имя объекта и через точку (.) указать необходимое имя необходимого свойства. Этот способ доступа через точку называется dot-notation (дот-нотэйшен).

Возвращаемся к примеру выше, в котором был создан объект personOne:

personOne.name // "Bob"
personOne.age // 35

Таким образом можно не только получать данные, но и изменять данные.

print(personOne.age)

// Output
// 35

personOne.age += 1

print(personOne.age)

// Output
// 36

Если свойство задано как константа (let), то изменить такое свойство нельзя. При попытке это сделать будет ошибка: “Cannot assign to property: … is a ‘let’ constant”.

Собственные инициализаторы

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

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

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

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

Несмотря на то, что инициализатор - это метод, он объявляется без указания ключевого слова func и его имя может быть только init. Соответственно, несколько собственных инициализаторов будут иметь одинаковые имена и должны отличатся функциональным типом - каждый из них должен иметь уникальный набор входных параметров. Это называется перегрузка функции. Более детально про перегрузку функции можно узнать в статье про функции.

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

Создадим инициализатор для примера выше:

struct Person {
    var name: String = "Name"
    var age: Int = 18

    init(name: String) {
        self.name = name
    }
}
var personOne = Person(name: "Bob")
print(personOne.name)
print(personOne.age)

// Output
// Bob
// 18

let personTwo = Person() // Errror: Missing argument for parameter 'name' in call

В этом примере инициализатор принимает только аргумент name для свойства name. Свойство age проинициализировано со значением по умолчанию - 18.

Обратите внимание на объявление второго объекта personTwo. При попытке проинициализировать его с пустым инициализатором (так как свойства имеют значения по умолчанию) была получена ошибка, так как все встроенные инициализаторы были удалены. Для того, чтоб реализовать пустой инициализатор, необходимо, как говорилось выше, добавить еще один собственный инициализатор.

struct Person {
    var name: String = "Name"
    var age: Int = 18

    init() {
        self.name = "Name"
        self.age = 18
    }

	init(name: String) {
        self.name = name
    }
}

Теперь пустой инициализатор снова доступен.

Внимание! Если свойство объявлено как константа, то изменить это свойство после инициализации нельзя.

Рассмотрим еще один пример:

struct Car {
    let name: String
    let tankFull: Int
    var tankCurrent: Int
    let speedMax = 120
    var speedCurrent: Int

    init(name: String, tankFull: Int, speedCurrent: Int) {
        self.name = name
        self.tankFull = tankFull
        self.tankCurrent = tankFull / 2
        self.speedCurrent = speedCurrent
    }
}
var carOne = Car(name: "Audi", tankFull: 80, speedCurrent: 50)
print(carOne)

// Output
// Car(name: "Audi", tankFull: 80, tankCurrent: 40, speedMax: 120, speedCurrent: 50)

В этом примере была создана структура Car. При инициализации:

  • свойство tankCurrent было проинициализировано со значением tankFull / 2;
  • свойство speedMax было проинициализировано со значением по умолчанию 120.

В дальнейшей работе с объектом carOne можно модифицировать только свойства tankCurrent и speedCurrent, так как они указаны как переменные. Остальные свойства можно только читать.

Если попытаться изменить например свойство tankFull, то получим ошибку:

// carOne.tankFull = 200 // Error: Cannot assign to property: 'tankFull' is a 'let' constant

Так же обратите внимание на объявление объекта carOne - он объявлен как переменная. И значит его свойства, которые доступны для модификации, можно изменять. Создадим ещё один экземпляр структуры Car как константу:

let carTwo = Car(name: "BMW", tankFull: 100, speedCurrent: 60)
print(carTwo)

// Output
// Car(name: "BMW", tankFull: 100, tankCurrent: 50, speedMax: 120, speedCurrent: 60)

carTwo.speedCurrent = 100 // Error: Cannot assign to property: 'carTwo' is a 'let' constant

Внимание! Структуры — это типы-значения (Value type). При назначении экземпляра структуры от одного объекта в другой происходит копирование экземпляра. В следующем примере создаются два независимых экземпляра одной и той же структуры:

var carOne = Car(name: "Audi", tankFull: 80, speedCurrent: 50)
var carTwo = carOne
print(carOne)

// Output
// Car(name: "Audi", tankFull: 80, tankCurrent: 40, speedMax: 120, speedCurrent: 50)

print(carTwo)

// Output
// Car(name: "Audi", tankFull: 80, tankCurrent: 40, speedMax: 120, speedCurrent: 50)

В этом примере был объявлен экземпляр структуры Car - carOne. Потом объявлен второй объект carTwo, который проинициализирован со значением carOne. Теперь, carTwo является экземпляром структуры Car и хранит в себе копию объекта carOne. Эти экземпляры независимы и если изменить один, то второй не измениться.

carTwo.name = "BMW"
carTwo. speedCurrent = 100

print(carOne)

// Output
// Car(name: "Audi", tankFull: 80, tankCurrent: 40, speedMax: 120, speedCurrent: 50)

print(carTwo)

// Output
// Car(name: "BMW", tankFull: 80, tankCurrent: 40, speedMax: 120, speedCurrent: 100)

Более детально про типы-значения (Value type) можно узнать в отдельной статье. См. ссылки внизу.

Методы в структурах

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

Функция, определенная внутри структуры, называется методом.

Для структуры Person реализуем метод, который выводит справочную информацию о человеке:

struct Person {
    var name: String = "Name"
    var age: Int = 18

    init(name: String) {
        self.name = name
    }

	func description() {
        print("Person name: \(name), person age: \(age)")
    }
}
var personOne = Person(name: "Bob")
personOne.description()

// Output
// Person name: Bob, person age: 18

Изменяющие методы

По умолчанию методы структур не могут изменять свойства структуры, кроме инициализаторов.

Для того, чтоб метод мог изменить свойство, перед объявлением метода необходимо указать модификатор mutating.

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

struct Person {
    var name: String = "Name"
    var age: Int = 18

    init(name: String) {
        self.name = name
    }

    func description() {
        print("Person name: \(name), person age: \(age)")
    }

    mutating func addAge(year: Int) {
        age += year
    }
}
var personOne = Person(name: "Bob")
print(personOne)

// Output
// Person(name: "Bob", age: 18)

personOne.addAge(year: 20)
print(personOne)

// Output
// Person(name: "Bob", age: 38)

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

Структуры как типы данных

Так как структура - это тип данных, то можно использовать экземпляры структуры как и другие типы данных. Например, добавить в массив.

struct Person {
    var name: String
    var age: Int
}

var personOne = Person(name: "Bob", age: 35)
var personTwo = Person(name: "Jack", age: 30)
var personThree = Person(name: "Dan", age: 45)

var arrayPerson: [Person] = [personOne, personTwo]

arrayPerson.append(personThree)
arrayPerson.append(Person(name: "Mike", age: 20))

for person in arrayPerson {
    print(person)
}

// Output
// Person(name: "Bob", age: 35)
// Person(name: "Jack", age: 30)
// Person(name: "Dan", age: 45)
// Person(name: "Mike", age: 20)

Чтоб получить доступ к свойству какого-то элемента структуры в коллекции, необходимо получить доступ к элементу массива и затем в свойству экземпляра. Например свойство name экземпляра personTwo:

print(arrayPerson[1].name)
// Output
// Jack

Базовые типы данных (Int, Double, Float, String, Bool), коллекции (массивы, словари, множества) - являются структурами.

Еще пример. Создана структура, которая описывает модель солдата. У солдата есть имя, здоровье, сила атаки. Солдат может пополнить здоровье и атаковать другого солдата.

struct Soldier {
    var name: String
    var healthPoint: Int
    var attackPoint: Int

    mutating func addHealth(heal: Int) {
        self.healthPoint += heal
    }
    func attack(target: inout Soldier) {
        target.healthPoint -= self.attackPoint
        print("\(name) attacks \(target.name)")
    }
}
var sol1 = Soldier(name: "Bob", healthPoint: 30, attackPoint: 10)
var sol2 = Soldier(name: "Jack", healthPoint: 25, attackPoint: 15)
print("Health sol1: \(sol1.healthPoint), health sol2: \(sol2.healthPoint)")

// Output
// Health sol1: 30, health sol2: 25

В методе attack() был применён сквозной параметр, так как данный метод изменяет входящий параметр. Входящий параметр target - тоже является экземпляром структуры Soldier. И его свойство healthPoint уменьшается на силу удара атакующего - self.attackPoint.

Более детально про сквозные параметры в функциях можно узнать в отдельной статье. См. ссылки внизу.

Были созданы два экземпляра sol1 и sol2 структуры Soldier. Выполним атаку первым экземпляром sol1:

sol1.attack(target: &sol2)
print("Health sol1: \(sol1.healthPoint), health sol2: \(sol2.healthPoint)")

// Output
// Bob attacks Jack
// Health sol1: 30, health sol2: 15

Здоровье экземпляра sol2 уменьшилось. Немного восстановим его:

sol2.addHealth(heal: 5)
print("Health sol1: \(sol1.healthPoint), health sol2: \(sol2.healthPoint)")

// Output
// Health sol1: 30, health sol2: 20

Здоровье экземпляра sol2 увеличилось на 5.

Структуры можно вкладывать в другие структуры, как и другие типы данных - получится вложенная структура (вложенные типы данных)

struct Book {
    var title: String
    var author: String
}

struct Library {
    var arrayBook: [Book] = []
}

var library = Library()
library.arrayBook.append(Book(title: "Flowers for Algernon", author: "Daniel Keyes"))

for book in library.arrayBook {
    print("The library has book: \(book.title) by \(book.author)")
}

// Output
// The library has book: Flowers for Algernon by Daniel Keyes

Была создана структура Book, которая была применена в структуре Library.


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

Более детально про свойства, методы, сабскрипты, протоколы можно узнать в отдельных статьях. См. ссылки внизу.


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

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

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