Язык программирования Swift и его архитектура

С самого начала изучения языка программирования Swift будет полезно будет получить общее представление о том, как устроен Swift и как выглядит iOS-программа на Swift.

Основы Swift

Команда на Swift – это утверждение. Текстовый файл Swift состоит из строк текста. Разрывы строк имеют значение. Типичный пример программы – одно утверждение на одной строке:

print("hello")
print("world")

Команда print обеспечивает вывод сообщений в консоль Xcode.

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

print("hello"); print("world")

Вы можете ставить точку с запятой в конце каждого оператора, который является последним или единственным в своей строке, но никто обычно этого не делает (кроме как по привычке, потому C и Objective-C требуют точки с запятой):

print("hello");
print("world");

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

 print(
        "world")

Комментарии – это все, что идет после двух косых черт в строке:

print ("world") // это комментарий, поэтому Swift его игнорирует

Вы также можете также использовать многострочные комментарии, которые заключаются между /* и */:

/*
...
...
...
*/

Многие конструкции в Swift используют фигурные скобки в качестве разделителей:

   class Dog {
        func bark() {
            print("woof")
        }
}

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

    class Dog { func bark() { print("woof") }}

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

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

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

Все – это объект?

В Swift «все является объектом». Это общее утвержение для различных объектно-ориентированных языков программирования, но что это означает? Прежде всего это зависит от того, что мы подразумеваем под «объектом», а также, что мы подразумеваем под «всем»?

Условно говоря, объект – это то, чему вы можете отправить определенную команду. Например, вы можете дать команду собаке: “Лай!” или “Сидеть!”

В Swift команды отправляются с помощью точки:

fido.bark() 
rover.sit()

В языке программирования Swift даже «примитивные» объекты могут отправлять сообщения. Взять, к примеру, число 1. Кажется, что это всего лишь цифра и не более. Но в Swift за числом 1 также может следовать точка и команда:

let s = 1.description

Тип объекта может быть расширен в Swift. Это означает, что вы можете определять свои собственные команды для каждого типа данных. Например, вы обычно не можете отправить команду «Скажи привет» типу Int, но вы можете изменить тип Int, чтобы у вас появилась данная возможность:

    extension Int {
        func sayHello() {
            print("Привет, Я \(self)")
        }
}
sayHello() // выводит: "Привет, Я 1"

В Swift число 1 – это объект. В некоторых языках, таких как Objective-C это «примитивный» или скалярный тип данных. В Swift нет скаляров, и все типы являются типами объектов. Вот, что на самом деле означает «все есть объект».

Три типа объектов

Если вы знаете Objective-C или какой-либо другой объектно-ориентированный язык, вы можете быть удивлены тем, какой тип объекта представляет собой число 1. Во многих языках, таких как Objective-C, тип объекта является классом. В Swift есть классы, но 1 в Swift не является классом. Тип 1, а именно Int, является структурой, а 1 – экземпляром данной структуры. И у Swift есть еще одна тип объектов – это enum.

Таким образом, язык программирования Swift имеет три типа объектов:

  • Классы.
  • Структуры.
  • Перечисления.

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

Переменные

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

В Swift ни одна переменная не появляется из ниоткуда. Все переменные должны быть объявлены. Если вам нужно имя для чего-то, вы должны заявить: «Я создаю имя». Вы делаете это с помощью одного из двух ключевых слов: let или var. В Swift объявление обычно сопровождается инициализацией, когда вы используете знак равенства для присвоения переменной значения:

let one = 1
var two = 2

Как только имя создано, вы можете использовать его. Мы можем изменить значение two, чтобы оно совпадало со значением one:

let one = 1
var two = 2
two = one

Знак равенства является оператором присваивания. Это команда, которая означает: «Получить значение того, что находится справа от меня, и использовать его, чтобы заменить значение того, что находится слева от меня».

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

let one = 1
var two = 2
one = two // ошибка компиляции

Если вы знаете, что никогда не замените начальное значение переменной, всегда лучше использовать let, поскольку это позволяет Swift комплировать код быть более эффективно. На самом деле, Swift будет обращать ваше внимание на любой случай использования var, где вы могли бы использовать let, предлагая изменить var на let.

Каждая переменная имеет свой тип. Этот тип устанавливается при объявлении переменной и не может изменяться.

var two = 2
two = "Привет" // ошибка компиляции

Как только two объявляются и инициализируются как 2, эта переменная является числом (тип Int), и это всегда должно быть так. Вы можете заменить значение на 1, потому что это тоже Int, но вы не можете заменить значение на «Привет», потому что это строка (String), а String не является Int.

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

Функции

Исполняемые части кода, такие как:

fido.bark()
one = two
print ("hello")

не могут находиться где угодно в коде вашей программе. Пренебрежение этим фактом является распространенной ошибкой начинающих программистов и может привести к сообщению об ошибке компиляции – “Expected declaration”.

Любой исполняемый код должен находиться внутри тела функции. Функция – это пакет кода, который можно запустить. Ее тело ограничено фигурными скобками. Как правило, у функции есть имя, и она получает это имя через объявление функции.

func go() {
    let one = 1
    var two = 2
    two = one 
}

Здесь функция описывает последовательность действий:

  • Объявить переменную one.
  • Объявить переменную two.
  • Изменить значение переменной two на one.

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

go()

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

func doGo() {
    go()
}

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

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

Структура Swift файла

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

Модуль импорта

Это модуль даже более высокого уровня, чем сам файл. Модуль может состоять из нескольких файлов. Файлы вашего приложения принадлежат одному модулю и могут видеть друг друга. Но модуль не может видеть другой модуль без оператора импорта. К примеру, в первой строке вашего файла должно быть написано import UIKit.

Объявления переменных

Переменная, объявленная на верхнем уровне файла, является глобальной переменной: весь код в любом файле сможет ее видеть и получать к ней доступ.

Объявление функций

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

Объявления типов объектов

Объявление для класса, структуры или перечисления.

Рассмотрим файл Swift, содержащий на своем верхнем уровне оператор импорта, объявление переменной, объявление функции, объявление класса, объявление структуры и объявление enum:

 import UIKit
    var one = 1
    func changeOne() {
    }
    class Manny {
    }
    struct Moe {
    }
    enum Jack {
    }

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

Исполняемый код не может находиться на верхнем уровне. Только тело функции может содержать исполняемый код. К примеру, оператор типа one = two или print (“hello”) является исполняемым кодом и не может находиться на верхнем уровне файла. В нашем примере func changeOne() является объявлением функции, поэтому исполняемый код должен находиться внутри фигурных скобок в самом теле функции:

 var one = 1
    // исполняемый код не может здесь находиться
    func changeOne() {
        let two = 2 // место для исполняемого кода
        one = two   // место для исполняемого кода
    }

Точно так же исполняемый код не может находиться внутри фигурных скобок, которые сопровождают объявление класса Manny. Это верхний уровень объявления класса, а не тела функции. Но объявление класса может содержать объявление функции, и это объявление функции может содержать исполняемый код:

 class Manny {
        let name = "manny"
        // исполняемый код не может здесь находиться
        func sayName() {
            print(name) // место для исполняемого кода
        }
}

Область видимости и время жизни объектов

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

  • Модуль – это область видимости.
  • Файл – это область видимости.
  • Фигурные скобки – это область видимости.

Когда что-то объявляется, это объявляется на каком-то уровне в иерархии видимости объектов.

import UIKit
 
var one = 1
 
func changeOne() {
    let two = 2
    func sayTwo() {
       print(two) 
    }
    class Klass {}
    struct Struct {}
    enum Enum {}
    one = two
}
 
class Manny {
    let name = "manny"
    func sayName() {
        print(name)
    }
    class Klass {}
    struct Struct {}
    enum Enum {}
}
 
struct Moe {
    let name = "moe"
    func sayName() {
        print(name)
    }
    class Klass {}
    struct Struct {}
    enum Enum {}
}
 
enum Jack {
    var name : String {
        return "jack"
    }
 
    func sayName() {
        print(name)
    }
 
    class Klass {}
    struct Struct {}
    enum Enum {}
}

Внутри объявления класса Manny находится объявление переменной name и объявление функции sayName. Код внутри фигурных скобок sayName может видеть объекты вне этих фигурных скобок на более высоком уровне и поэтому может видеть переменную name. Аналогично, код внутри тела функции changeOne может видеть одну переменную, объявленную на верхнем уровне файла.

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

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

Объекты, заявленные на более глубоком уровне, живут еще короче. Рассмотрим следующий код:

func silly() {
        if true {
            class Cat {}
            var one = 1
            one = one + 1
} }

Этот код очень простой, но он вполне рабочий. Класс Cat и переменная one даже не появятся, пока кто-то не вызовет функцию silly(), и даже тогда они будут существовать только в течение короткого момента, когда исполнение кода происходит через конструкцию if.

Предположим, что вызывается функция silly(). Затем срабатывает конструкция if. Далее объявляется класс Cat и исполнятся строка one = one + 1. На протяжении всей своей короткой жизни класс Cat и переменная one были совершенно невидимы для остальной части программы.

Свойства и методы

Внутри трех типов объектов (класса, структуры и перечисления) объекты имеют специальные имена. Возьмем класс Manny в качестве примера:

 class Manny {
        let name = "manny"
        func sayName() {
            print(name)
        }
}

В этом коде:

  • Переменная name называется свойством этого класса.
  • Функция sayname() называется методом этого класса.

Пространство имен

Пространство имен – это именованные области программы. Имена объектов внутри пространства имен не могут быть достигнуты другими объектами, если они каким-то образом не пройдут сквозь барьер названия. Это позволяет использовать одно и то же имя в разных местах программы без каких-либо конфликтов.

 class Manny {
        class Klass {}
}

Способ объявления Klass делает Klass вложенным типом. Он эффективно «скрывает» Klass внутри класса Manny. Manny – это пространство имен. Код внутри Manny может видеть Klass напрямую. Но код за пределами Manny не может этого сделать. Он должен явно указать пространство имен, чтобы пройти через барьер, который представляет пространство имен. Для этого необходимо сначала указать имя класса Manny, а затем использовать точку

Manny.Klass

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

Модули

Пространства имен верхнего уровня называются модулями. Ваше приложение является модулем и, следовательно, пространством имен. Имя этого пространства имен по умолчанию является именем вашего приложения. Если ваше приложение называется MyApp, то когда вы объявите класс Manny на верхнем уровне, настоящее имя этого класса будет MyApp.Manny. Но вам обычно не нужно использовать это имя, потому что ваш код уже и так находится в том же пространстве имен и может видеть имя класса Manny напрямую.

Когда вы импортируете модуль, все объявления верхнего уровня этого модуля становятся видимыми для вашего кода. Например, фреймворк Foundation Cocoa, где находится NSString, является модулем. Когда вы программируете на iOS, вы используете import Foundation (или import UIKit, который импортирует Foundation), что позволяет вам использовать NSString, не используя Foundation.NSString.

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

Этот факт важен, потому что он решает главную загадку: откуда берутся такие вещи, как функция print(), и почему их можно использовать в любом месте вашего кода? На самом деле print – это функция, объявленная на верхнем уровне модуля Swift, и весь ваш код может видеть объявления верхнего уровня модуля Swift, потому что он импортирует Swift. Поэтому функция print() является глобальной для вашего кода.

Экземпляры

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

Например, я могу объявить класс Dog и присвоить этому классу метод:

class Dog {
        func bark() {
            print("Гав!")
        }
}

На самом деле в моей программе еще нет объектов Dog. Я просто описал тип объекта. Чтобы получить настоящего пса, я должен создать его. Процесс создания фактического объекта Dog, типом которого является класс Dog – это процесс инициализации экземпляра Dog. Результатом этого является появление нового объекта – экземпляра Dog.

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

язык программирования Swift и инициализация экземпляра

Создадим экземпляр класса Dog:

    let fido = Dog()

В этом коде происходит две вещи. Мы создаем экземпляр класса Dog, а также объявляем новую константу fido и присваиваем ей новый экземпляр Dog.

Теперь, когда у меня есть экземпляр Dog, я могу отправлять ему сообщения. То есть свойства и методы собаки. Например:

fido.bark() // Гав!

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

Dog.bark () // ошибка компиляции

То же самое относится и к свойствам.

    класс Dog {
        var name = ""
}

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

let fido = Dog()
fido.name = "Fido"

Можно объявить имя свойства таким образом, чтобы выражение Dog.name было допустимым, но тогда это будет другой тип свойства – свойство класса или статическое свойство.

Чем удобны экземпляры?

Даже если бы не было такой вещи как экземпляр, тип объекта сам по себе является объектом. Мы знаем это, потому что возможно отправить сообщение типу объекта (например, через Manny.Klass). Почему же тогда существуют экземпляры?

Ответ связан с природой свойств экземпляра. Значение свойства экземпляра определяется через его отношение к конкретному экземпляру. Вот где экземпляры показывают свою реальную пользу.

Рассмотрим снова наш класс с собаками. Я дам ему свойство name и метод bark(), то есть свойство экземпляра и метод экземпляра:

    класс Dog {
        var name = ""
        func bark() {
            print("Гав!")
          }
}

Экземпляр Dog изначально создается с пустым именем (пустой строкой). Но его свойство name является переменной var, поэтому, когда у нас есть экземпляр Dog, мы можем присвоить его имени новое значение типа String:

let dog1 = Dog ()
dog1.name = "Фидо"

Мы также можем запросить имя экземпляра Dog:

print (dog1.name) // "Фидо"

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

язык программирования Swift и объект Dog

let dog1 = Dog()
dog1.name = "Фидо"
let dog2 = Dog()
dog2.name = "Ровер"
print(dog1.name) // "Фидо"
print(dog2.name) // "Ровер"

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

let dog1 = Dog()
dog1.name = "Фидо"
var dog2 = Dog()
dog2.name = "Rover"
print (dog1.name) // "Фибо"
print (dog2.name) // "Ровер"
dog2 = dog1
print (dog2.name) // "Фибо"

Здесь проявляется суть объектно-ориентированного программирования. Существует тип Dog, который определяет, что значит быть Dog. В нашем объявлении Dog говорится, что любой экземпляр Dog имеет свойство name и метод bark. Но каждый экземпляр собаки может иметь собственное значение свойства name. Таким образом, несколько экземпляров одного и того же типа объекта ведут себя одинаково – как Фидо, так и Ровер могут лаять и будут делать это, когда им отправляется сообщение о лае, но при этом это разные экземпляры и они могут иметь разные значения свойств.

Экземпляр отвечает не только за значения, но и за время жизни его свойств. Предположим, мы создаем экземпляр Dog и присваиваем его свойству name значение «Фидо». Экземпляр Dog сохраняет значение свойства «Фидо» только в том случае, если мы не заменяем значение его имени каким-либо другим значением.

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

Ключевое слово self

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

class Dog {
        var name = ""
        var whatADogSays = "Гав!"
        func bark() {
            print(self.whatADogSays) 
  }
    func speak() { 
       self.bark()
     }
}

Однако использование ключевого self здесь не является обязательным. Вы можете пропустить его, и код все так же будет работать:

class Dog {
        var name = ""
        var whatADogSays = "woof"
        func bark() {
            print(whatADogSays)
        }
        func speak() {
            bark()
     } 
}

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

Контроль доступа

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

Рассмотрим пример:

class Dog {
        var name = ""
        var whatADogSays = "Гав!"
        func bark() {
          print(self.whatADogSays) 
      }
         func speak() { 
          print(self.whatADogSays)
    }
}

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

let dog1 = Dog()
dog1.whatADogSays = "Мяу!"
dog1.bark () // Мяу!

Почему мы не объявили whatADogSays с помощью помощью константы let?

     class Dog {
        var name = ""
        let whatADogSays = "Гав!"
        func bark() {
           print(self.whatADogSays) 
          }
        func speak() { 
         print(self.whatADogSays)
          } 
}

Здесь есть две проблемы. Предположим, я хочу, чтобы сам экземпляр Dog мог изменять свой собственный метод whatADogSays через self.whatADogSays. Тогда whatADogSays должен быть объявлен через var. Кроме того, предположим, что я не хочу, чтобы какой-либо другой объект знал, что именно делает класс Dog. Даже если свойство объявлено с помощью let, другие объекты все равно могут получить доступ к свойству whatADogSays.

Чтобы решить эту проблему, Swift использует ключевое слово private.

 class Dog {
        var name = ""
        private var whatADogSays = "woof"
        func bark() {
          print(self.whatADogSays) 
          }
        func speak() { 
          print(self.whatADogSays)
          } 
}

Теперь whatADogSays является private свойством – его не видят другие объекты. Внутри Dog мы можем использовать конструкцию self.whatADogSays, но при этом экземпляры класса Dog не смогут использовать функцию whatADogSays.

Язык программирования Swift и архитектура приложений

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

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

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

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

Объектно-ориентированное программирование – это искусство. И позволить вашей программе (и вашему мышлению) развиваться по мере того, как вы пишете код, обнаруживая новые потребности и проблемы, – это развитие программы и вас как программиста.

Читайте также:
Добавить комментарий

Ваш адрес email не будет опубликован.