Данная публикация является второй частью ранее опубликованного материала и рассчитана на ознакомление с ним. Ссылка на первую часть - Тыц

Подводя итоги прошлой публикации

Мы знаем, что class:

  • обладает свойствами и методами

  • может описывать объект (экземпляр)

  • может описывать тип (глобальные возможности)

  • может использоваться как тип и как экземпляр одновременно, причем свойства и методы типа не пересекаются со свойствами и методами экземпляра

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

  • может быть родителем (Супер классом) и иметь родителя (унаследовать реализацию супер класса)

  • может переопределять наследие Супер класса

  • может запрещать переоперделение и ограничивать доступы к свойствам и методам

  • может наследовать несколько классов по цепочке (Супер класс всегда один)

  • является ссылочным типом значений

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

class ExampleClass {
    static var shared: ExampleClass = ExampleClass()
    var name: String = "name"
}

мы имеем возможность инициализировать экземпляр в экземпляр

class ExampleClass {
    var instance: ExampleClass?
    var name: String = "name"
}

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

демонстрация использования экземпляра внутри экземпляра
демонстрация использования экземпляра внутри экземпляра

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

пример переопределения дефолтного инициализатора
пример переопределения дефолтного инициализатора

Кроме того, инициализаторов может быть множество и они могут использовать друг друга (делегировать инициализацию).

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

Обратите внимание! На изображении выше, Вы видите имена «Единичный» и «Множественный», они относятся к передаваемым параметрам, а не к инициализаторам т.е. экземпляр с именем «Единичный» получает Int, а с именем «Множественный» [Int].

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

  • Вспомогательный инициализатор обязан вызывать другой инициализатор

  • Вспомогательный инициализатор обязан вызвать назначеный инициализатор, но не обязательно напрямую

пример вызова назначеного инициализатора из вспомогательного не напрямую
пример вызова назначеного инициализатора из вспомогательного не напрямую

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

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

реализация без использования удобных инициализаторов
реализация без использования удобных инициализаторов

Кроме того, Swift позволяет организовывать более сложную логику в рамках одного инициализатора.

усложнение логики инициализатора
усложнение логики инициализатора

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

Небольшое отступление т.к. хочу обратить внимание на switch в Swift т.к. он более мощен чем кажется на первый взгляд (не проваливается подобно switch в C, работает с диапазонами и т.д.), а так же с лихвой заменит громоздкую конструкцию if else if...

замена if на switch
замена if на switch

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

пример использования проваливающегося инициализатора
пример использования проваливающегося инициализатора

Грубо говоря, проваливающийся инициализатор позволяет прекратить инициализацию класса и вернуть nil в каких-либо ситуациях.

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

вызов инициализатора супер класса
вызов инициализатора супер класса
более сложный вызов инициализатора супер класса
более сложный вызов инициализатора супер класса

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

class Product {
    var collection: Collection!
    var category: Category!
}

class Collection {
    var category : Category?
    let product : Product
}

class Category {
    var collection : Collection?
    let product : Product
}

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

...///Product

    init(){
        self.collection = Collection(nil, self)
        self.category = Category(self.collection, self)
        self.collection.category = self.category
    }

...///Collection

    init(_ cat: Category?, _ prodo: Product){
        self.category = cat
        self.product = prodo
    }

...///Category

    init(_ col: Collection, _ prodo: Product){
        self.collection = col
        self.product = prodo
    }
    
...

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

...///Product  

func removeCategory() {
  collection.category = nil  
  self.category = nil 
}

...

Так же добавим категории деинициализатор

...///Category  

deinit{
	print("категория освобождена")
}

...

Запустим и вызовем метод

пример использования деинициализатора
пример использования деинициализатора

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

освобождаем третий объект через деинициализатор
освобождаем третий объект через деинициализатор

Теперь удалим из метода removeCategory удаление ссылки в коллекции, а в коллекции сделаем слабую ссылку с помощью weak

использование слабой ссылки
использование слабой ссылки

Код продолжает работать как ожидалось т.к. в коллекции нет сильной ссылки на категорию, а только слабая (weak) и соответственно при удалении из продукта, экземпляр не удерживается в памяти.

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

Нам необходим некий класс, который должен:

  • Иметь как минимум один экземпляр и не более одного в простое

  • Выполнять параллельные запросы к сети

  • Обладать обработчиком успешного завершения запросов

  • Обладать обработчиком провальных запросов

  • Быть глобальным

  • Быть мультизадачным т.е. допускать возможность многократного, одновременного вызова из разных участков кода/приложения

  • Быть потокобезопасным т.е. гарантировать отсутствия доступа к данным с другого места программы

Давайте порассуждаем, что нам может помочь в такой задаче. Синглтон в виде shared? Безусловно, но этого будет не достаточно т.к. мы не можем генерировать динамические свойства и как следствие хранить данные, к которым не будет доступа при одновременном выполнении. Но мы можем делать сколько угодно экземпляров. И сделать их изолированными. Собственно давайте начнем, а в процессе разберемся.

final class Http {
    typealias This = Http
}

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

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

static private var instances : [UUID : This] = [:]

Обратите внимание, что мы используем псевдоним типа. Запись эквивалентна записи [UUID : Http]

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

//Обертка для хороших ответов
struct RequestAnswer {
	let data: Data
	let response: URLResponse
}

//Обертка для плохих ответов
struct RequestError {
	let error: Error?
	let goodData: [String: RequestAnswer]
}

private let id: UUID //Идентификатор экземпляра
private var requestCount: Int8 = 0 //Кол-во запросов
private var answerCount: Int8 = 0 //Кол-во ответов
private var goodAnswerCount: Int8 = 0 //Кол-во хороших ответов
private var badAnswerCount: Int8 = 0 //Кол-во плохих ответов

//Хранилище данных из хороших ответов
private var goodAnswers: [String : RequestAnswer] = [:]

//Хранилище данных из плохих ответов
private var badAnswers: [String : RequestError] = [:]

//Хранилище экшенов для хороших запросов
private var generalClosuresForGoodAnswers : [(_ answer: [String : RequestAnswer]) -> Void] = []
//Хранилище экшенов для плохих запросов
private var generalClosuresForBadAnswers : [(_ answer: [String : RequestError]) -> Void] = []

//Будет вызвано при успехе
private func done(){}
//Будет вызвано в случае провала
private func notDone(){}

//Метод добавления экшена (замыкания) к хорошим обработчикам
@discardableResult public func addGeneralClosureForGoodAnswer(_ closure: @escaping (_ answer: [String : RequestAnswer]) -> Void) -> Http {
    self.generalClosuresForGoodAnswers.append { answer in
        closure(answer)
    }
    return self
}

//Метод добавления к плохим соответственно
@discardableResult public func addGeneralClosureForBadAnswer(_ closure: @escaping (_ answer: [String : RequestError]) -> Void) -> Http {
    self.generalClosuresForBadAnswers.append { answer in
        closure(answer)
    }
    return self
}

//Метод запроса
request(url urlString: String, key: String = UUID().uuidString) -> This {}

Обратитите внимание. @discardableResult обозначение отсутствие необходимости принимать возвращаемое значение т.е. у нас нет нужды создавать место для возвращаемого значения.

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

//Метод типа для отображения информации в консоль
class private func console(_ cell: String){
    if isShowedLog { print("http: " + cell) }
}

//Флаг для вывода информации в консоль
public static var isShowedLog: Bool = true

//Инициализатор
private init(_ id: UUID){
    self.id = id
    This.console("экземпляр \(self.id) создан")
}

Далее нам надо определить логику запроса и настроить наблюдателей

//Запрос
@discardableResult public func request(url urlString: String, key: String = UUID().uuidString) -> This {
    guard let url = URL.init(string: urlString) else {
        This.console("запрос \(key) не создан, ссылка ошибочна")
        return self
    }
    
    //Прибавляем кол-во запросов
    DispatchQueue.main.async {
        self.requestCount += 1
    }
    
    //Получаем ссылку на синглтон сетевого класса 
    let session = URLSession.shared
    //Создаем задачу
    let task = session.dataTask(with: url){ (data, response, error) in
    		//Грубая обработка ошибок
        if (error != nil || response == nil || data == nil) {
            DispatchQueue.main.async {
                self.badAnswers[key] = RequestError(error: error, goodData: self.goodAnswers)
                self.badAnswerCount += 1
                self.answerCount += 1
            }
            This.console("запрос \(key) провалился")
            return
        }
        
        //Успешный запрос
        DispatchQueue.main.async {
            self.goodAnswerCount += 1
            self.answerCount += 1
            self.goodAnswers[key] = RequestAnswer(data: data!, response: response!)
        }
        This.console("запрос \(key) успешно выполнен")
    }
    task.resume() //Выполняем задачу
    return self
}


//Добавили наблюдателя
private var goodAnswers: [String : RequestAnswer] = [:] {
    didSet {
        if  goodAnswers.count == self.goodAnswerCount &&
            goodAnswers.count == self.requestCount
        {
            self.done()
        }
    }
}

//Добавили наблюдателя
private var answerCount: Int8 = 0 {
    didSet {
        if badAnswerCount != 0 && requestCount == (goodAnswerCount + badAnswerCount){
            self.notDone()
        }
    }
}

Обратите внимение. DispatchQueue.main.async перемещает процессы на главный поток.

Определим деинициализатор и функции выполнения добавленных экшенов

deinit {
		This.console("экземпляр \(self.id) выгружен")
}

//Вызов всех добавленных экшенов при их наличии
private func done(){
    for closure in generalClosuresForGoodAnswers{
        closure(self.goodAnswers)
    }
    self.generalClosuresForGoodAnswers.removeAll()
    This.complition(id: self.id)
}

//Вызов всех добавленных экшенов при их наличии
private func notDone(){
    for closure in generalClosuresForBadAnswers{
        closure(self.badAnswers)
    }
    self.generalClosuresForBadAnswers.removeAll()
    This.complition(id: self.id)
}

С логикой экземпляра разобрались, теперь добавим логику типу

//Вызывая метод мы получим ссылку на экземпляр в хранилище
class func openRequest() -> This {
    let id = UUID()
    self.instances[id] = This(id)
    return self.instances[id]!
}

//Удаляем ссылку на экземпляр
class private func complition(id: UUID) {
		self.instances[id] = nil
}
Код целиком
final class Http {
    
    typealias This = Http
    
    private let id: UUID
    private var requestCount: Int8 = 0
    private var goodAnswerCount: Int8 = 0
    private var badAnswerCount: Int8 = 0
    private var badAnswers: [String : RequestError] = [:]
    
    private var goodAnswers: [String : RequestAnswer] = [:] {
        didSet {
            if  goodAnswers.count == self.goodAnswerCount &&
                goodAnswers.count == self.requestCount
            {
                self.done()
            }
        }
    }
    
    private var answerCount: Int8 = 0 {
        didSet {
            if badAnswerCount != 0 && requestCount == (goodAnswerCount + badAnswerCount){
                self.notDone()
            }
        }
    }
    
    private var generalClosuresForGoodAnswers : [(_ answer: [String : RequestAnswer]) -> Void] = []
    private var generalClosuresForBadAnswers : [(_ answer: [String : RequestError]) -> Void] = []
    
    private func done(){
        for closure in generalClosuresForGoodAnswers{
            closure(self.goodAnswers)
        }
        self.generalClosuresForGoodAnswers.removeAll()
        This.complition(id: self.id)
    }
    
    private func notDone(){
        for closure in generalClosuresForBadAnswers{
            closure(self.badAnswers)
        }
        self.generalClosuresForBadAnswers.removeAll()
        This.complition(id: self.id)
    }
    
    @discardableResult public func addGeneralClosureForGoodAnswer(_ closure: @escaping (_ answer: [String : RequestAnswer]) -> Void) -> This {
        self.generalClosuresForGoodAnswers.append { answer in
            closure(answer)
        }
        return self
    }
    
    @discardableResult public func addGeneralClosureForBadAnswer(_ closure: @escaping (_ answer: [String : RequestError]) -> Void) -> This {
        self.generalClosuresForBadAnswers.append { answer in
            closure(answer)
        }
        return self
    }
    
    @discardableResult public func request(url urlString: String, key: String = UUID().uuidString) -> This {
        guard let url = URL.init(string: urlString) else {
            This.console("запрос \(key) не создан, ссылка ошибочна")
            return self
        }
        
        DispatchQueue.main.async {
            self.requestCount += 1
        }
        
        let session = URLSession.shared
        let task = session.dataTask(with: url){ (data, response, error) in
            if (error != nil || response == nil || data == nil) {
                DispatchQueue.main.async {
                    self.badAnswers[key] = RequestError(error: error, goodData: self.goodAnswers)
                    self.badAnswerCount += 1
                    self.answerCount += 1
                }
                This.console("запрос \(key) провалился")
                return
            }
            
            DispatchQueue.main.async {
                self.goodAnswerCount += 1
                self.answerCount += 1
                self.goodAnswers[key] = RequestAnswer(data: data!, response: response!)
            }
            This.console("запрос \(key) успешно")
        }
        task.resume()
        return self
    }
    
    private init(_ id: UUID){
        self.id = id
        This.console("экземпляр \(self.id) создан")
    }
    
    deinit {
        This.console("экземпляр \(self.id) выгружен")
    }
    
    class func openRequest() -> This {
        let id = UUID()
        self.instances[id] = This(id)
        return self.instances[id]!
    }
    
    static private var instances : [UUID : This] = [:]
    static public var isShowedLog: Bool = true
    
    class private func console(_ cell: String){
        if isShowedLog { print("http: " + cell) }
    }
    
    class private func complition(id: UUID) {
        self.instances[id] = nil
    }
    
    struct RequestAnswer {
        let data: Data
        let response: URLResponse
    }
    
    struct RequestError {
        let error: Error?
        let goodData: [String: RequestAnswer]
    }
}

И проверяем

тест запросов без провала
тест запросов без провала
тест запросов с провалом
тест запросов с провалом

Логика достаточно простая, и мы по сути только и делали, что использовали преимущества ссылочного типа значений. Метод открытия инициализирует экземпляр с id и после помещения в словарь возращает ссылку на него (ключем выступает id), у которого по цепочке мы вызываем методы для формирования запроса и добавления обработчиков. Соотвтетственно получить доступ к словарю напрямую нельзя, а по завершению словарь очищает экземпляры. Реализация крайне простая и не очень изящная, содержит несколько упущений, но как пример отлично демонстрирует некоторые возможности которые мы разбирали ранее. При желании можно передавать свой id (нужно модифицировать код ) и по нему получать данные, построить сложную логику запросов и т.д. но это уже не относится к публикации, продвигаемся дальше.

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

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

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

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

вычисляемое свойство
вычисляемое свойство

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

subscript доступный для чтения
subscript доступный для чтения

Здесь мы описываем сабскрипт, доступный только для чтения

subscript(cell: Int) -> String {
	String(Double(cell) * self.num)
}

А тут получаем возвращаемое им значение

example[5]

как в словаре по индексу (собственно словарь сабскрипты и использует).

Теперь немного усложним

более сложный сабскрипт
более сложный сабскрипт

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

К слову нам не обязательно указывать получаемое значение в сеттере, оно всегда по умолчанию newValue и имеет тип возвращаемого значения.

упрощение сеттера
упрощение сеттера

актуально и для вычисляемых свойств, тип будет соответствовать свойству.

пример простого сеттера в вычисляемых свойствах
пример простого сеттера в вычисляемых свойствах

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

пример значений по умолчанию в сабскриптах и обращение к пустому индексу
пример значений по умолчанию в сабскриптах и обращение к пустому индексу

Обратите внимание, что мы записали параметр с использованием _

subscript(_ cell: Int = 5) -> Int { ...///

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

subscript(cell: Int = 5) -> Int { ...///

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

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

Double в первом сабскрипте указано для компилятора, чтобы он понял к какому самскрипту я обращаюсь по типу значения
Double в первом сабскрипте указано для компилятора, чтобы он понял к какому самскрипту я обращаюсь по типу значения

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

сабскрипт с пустым геттером
сабскрипт с пустым геттером

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

Расширения

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

Расширение начинается с extension, после указывается тип.

Инициализаторы

В расширении можно указывать вспомогательные(удобные) инициализаторы.

Методы

Можно определять методы типа и методы экземпляра

определение метода типа и экземпляра в расширении
определение метода типа и экземпляра в расширении

Если необходимо дать возможность переопределять методы в подклассах, то нужно указать @objc (доступно в Foundation)

описание методов в расширении, которые можно переопределять
описание методов в расширении, которые можно переопределять

Вычисляемые свойства

Доступны как get так и set

вычисляемые свойства в расширениях
вычисляемые свойства в расширениях

Протоколы

Можно подписываться на протоколы (о протоколах можно прочитать здесь - Тыц)

соответствие протоколу в расширениях
соответствие протоколу в расширениях

Конечно же в расширениях можно указывать и сабскрипты

сабскрипты в расширениях
сабскрипты в расширениях

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

Дополнительно

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

демонстрация вложенных типов
демонстрация вложенных типов

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

демонстрация множественной вложенности
демонстрация множественной вложенности
доступы типов
доступы типов
вложенные псевдонимы
вложенные псевдонимы

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

захват приватного свойства во вложенном типе
захват приватного свойства во вложенном типе

Конечно же классами это все не оганичивается и дает широкие возможности с другими типами.

Итого по классам (class)

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

Подсказка по классам и публикации

Class:

  • является ссылочным типом (на экземпляр ссылаются), следовательно экземпляры можно хранить в константах и все равно изменять свойства

  • может настраивать свой доступ

  • как тип существует глобально (если не определен иной доступ)

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

  • обладает дефолтным инициализатором (если свойства определены)

  • может реализовать инициализаторы

  • перетирает дефолтный инициализатор при реализации иного назначеного инициализатора

  • имеет деинициализатор

  • может быть определен в другом типе

  • может содержать в себе типы

  • описывает (имеет) свойства, методы, сабскрипты - все вместе описание

  • может описывать себя как объект (экземпляр) так и как тип

  • описание типа и описание объекта существуют обособленно

  • может использоватся как тип и как экземпляр одновременно

  • описание обладает уровнем доступа

  • может иметь в описании себя в виде экземпляра

  • может наследовать другой класс

  • может быть супер классом

  • может ограничить наследование

  • может переопределять наследие супер класса

  • может ограничивать переопределение

  • может быть расширен

  • может соответствовать протоколу

  • может прерывать инициализации и возвращать nil

  • может инициализироватся на лету с последующей деинициализацией

Прочее

  • didset - наблюдать, выполняемый после получения нового значения в свойство, можно использовать при наличии значения

  • get / set (геттер и сеттер) - get возвращает некое значение, set выполняет логику и имеет доступ к получаемому значению, которое по умолчанию newValue

  • @discardableResult - указывает об отсутствии необходимости принимать возвращаемое значение

  • @objc - бонус из objective-c, позволяющий переопределять методы из расширений

  • typealias - псевдоним типов

а теперь, наконец, переходим к структурам.

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

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

Тип значений

В отличии от класса, структура копирует свою каждую вариацию

пример различных разницы в типе значений и ссылочном типе
пример различных разницы в типе значений и ссылочном типе

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

Почленный инициализатор

У структур есть почленный инициализатор, у классов его нет

компилятор ругается на отсутствие инициализатора в классе
компилятор ругается на отсутствие инициализатора в классе
демонстрация почленного инициализатора
демонстрация почленного инициализатора

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

Необходимость указания мутаций

Если метод структуры будет менять состояние, его необходимо определить как мутирующий метод.

пример мутирующего метода в структуре
пример мутирующего метода в структуре

Можно определять назначеный инициализатор в расширениях

назначенный инициализатор в расширениях структуры
назначенный инициализатор в расширениях структуры
более сложный назначенный инициализатор в расширениях структуры
более сложный назначенный инициализатор в расширениях структуры

конечно если определяемый в расширении инициализатор не перекрывает дефолтный или не дублирует существующий.

Нет удобных инициализаторов, делегирование доступно из назначенных

делегирование инициализаторов в стуктуре
делегирование инициализаторов в стуктуре

Иной способ обозначать методы типа

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

использование методов типа в рамках стуктуры
использование методов типа в рамках стуктуры

Отсутствует деинициализатор

Пояснять особо нечего.

Не наследуются

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

Не могут соответствовать классовым протоколам

И тут пояснять нечего.

Много мелких мелочей

Я бы хотел закончить публикацию фразой «В остальном классы и структуры одинаковые» , но это не так. Различия в типах предполагает и различия в определенных моментах. Например, String это структура, чтобы присваивать значение через « = » существует протокол ExpressibleByStringLiteral (как по мне, чистой воды костыль от Apple), мы можем использовать этот протокол на своих структурах и классах, но реализация будет немного различаться

соответствие протоколы ExpressibleByStringLiteral для класса и структуры
соответствие протоколы ExpressibleByStringLiteral для класса и структуры

Класс обязан указывать обязательный инициализатор (required) и не может использовать Self, как это делает структура, соответственно необходимо указывать именно тип.

Чтобы сравнить структуры, достаточно ==, а вот классам подобное не подходит в полной мере т.к. иногда бывает необходимо узнать используются ли в сравнении две ссылки на один и тот же экземпляр (используется ===/!==, проверка на идентичность). В общем я думаю мыль ясна и понятна. Классы и структуры похожи и 99% того, что я описал про классы, применимо и к структурам (конечно за исключением прямых отличий), но все же, это разные типы и отличия иногда присутствуют там, где вы их не ждали. За сим по этой публикации все.

Если есть что добавить, добро пожаловать в комментарии.

Пояснения про Reduce

Вообще к публикации никак не относится, но коль упомянул, вот пример использования

Собственно это универсальный метод, который на вход принимает два параметра. Начальное значение и замыкание, в замыкание приходит буферное значение и текущий элемент (пробежка по массиву например).

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

Метод reduce является базовым к ознакомлению, так же как map, filter и sorted.

See you later...

Комментарии (6)


  1. Gargo
    06.01.2022 18:22

    читаю уже вторую вашу статью, и у меня складывается впечатление, что на swift вы не программировали от слова совсем:
    1) Из прошлой статьи:

    func getPriceRuble(
        _ length: Double,
        _ purchaseСost: Double,
        _ coefficient: Double,
        _ euroRate: Double,
        _ markupOnGoods: Double) -> Double {

    Никто не пишет у метода кучу параметров без названий. Не нужно превращать Swift в php или javascript. Даже если он это позволяет.

    2) Зачем у каждой-каждой переменной при объявлении писать тип? Например, здесь и так понятно, что это строка:

    var name = "name"

    3)

    Небольшое отступление т.к. хочу обратить внимание на switch в Swift т.к. он более мощен чем кажется на первый взгляд (не проваливается подобно switch в C, работает с диапазонами и т.д.), а так же с лихвой заменит громоздкую конструкцию if else if...

    Никто не пишет "switch true" и уж тем более не ставит этот костыль в пример по сравнению с "if-else". И у switch в swift есть конструкция "fallthrough", которая как раз "проваливается".

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

    self.sum = (cells?.reduce(0) { x, y in x + y } ?? 0) + (cell ?? 0)

    и т.д.


    1. cbepxbeo Автор
      06.01.2022 18:32
      -2

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

      P/s. К чему этот комментарий, я не понял. Если есть что добавить по материалу, здорово, пишите, спасибо Вам скажу. Сейчас же это выглядит как хейт.


  1. KEKSman
    06.01.2022 20:21

    "Данный материал, рассчитан на новичков в разработке" чет слишком сложно для новичков. Две статьи надо было назвать не "Классы и структуры", а "Как инициализировать классы". На моменте с сетевым кодом я уже потерял нить повествования. Какой великий смысл был в typealias? Зачем столько танцев с инициализаторами? К чему было предисловие с функциями в первой статье?


    1. cbepxbeo Автор
      06.01.2022 20:41

      чет слишком сложно для новичков

      Да вроде нет, итак максимально простым сделал, проще мне кажется некуда, но конечно это субъективно. Если Вам не сложно, был бы благодарен, если Вы укажете более детально, что мною изложено не достаточно понятно. Я учту это в будущем.

      надо было назвать не "Классы и структуры", а "Как инициализировать классы"

      Не согласен с Вами. Инициализация, расширения, классы и структуры конечно же являются разными темами, но т.к. инициализатор является неотъемлимой частью класса/структуры, я был обязан осветить этот крайне важный момент.

      На моменте с сетевым кодом я уже потерял нить повествования

      Его можно пропустить т.к. это пример, но так же спасибо за замечание, в будущем постораюсь развивать повествование и подачу информации.

      Какой великий смысл был в typealias?

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

      К чему было предисловие с функциями в первой статье

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

      Зачем столько танцев с инициализаторами

      Наверное при ответе на этот вопрос, мне стоит начать с другого вопроса - «какую цель я себе поставил в момент создания публикации?». Собственно цель иначально проста, доступно объяснить, что такое класс и структура, а так же чем они отличаются. Попутно прицепить какие-либо моменты, чтобы они были понятны и смотрелись уместно. Исходя из этой цели, я не могу ей удовлетворить, не описав инициализаторы т.к. если просто напишу «У структуры есть почленный инициализатор, а у класса нет» это будет не объяснением, а ерундой. Грубо говоря, чтобы понять отличия, надо знать возможности, поэтому я так все и расписывал.

      P/s. Я вообще считаю публикацию очень бедной, столько еще всего можно рассказать, но я ограничился текущим вариантом, возможно немного переборщил. Ну и конечно спасибо за вопросы и замечания.

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


      1. KEKSman
        07.01.2022 07:34
        +1

        Не согласен с Вами. Инициализация, расширения, классы и структуры конечно же являются разными темами, но т.к. инициализатор является неотъемлимой частью класса/структуры, я был обязан осветить этот крайне важный момент.

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

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

        Вы сами определитесь для кого изначально статья. В предисловии написнано что читатель должен "иметь общее представление о разработке и ОПП", но тут же в ответе пишете что "расчет на ознакомление. Человеку, который небыл знаком с ОПП" . Так для кого эта статья? В этой статье есть очень хорошая выжимка о том что такое класс в свифте. почему нет такого для структуры? Ну и напоследок хотелось бы примеры попроще, а не сразу в сетевой слой уходить, и если его можно пропустить, то спрятать дабы не отвлекал.

        P.S. Все вышенаписанное идет под знаком ИМХО, а за статьи спасибо


        1. cbepxbeo Автор
          07.01.2022 07:45

          Спасибо, все по факту. Обязательно учту замечания в будущем.