Что такое kotlin и с чем его едят: обучающее руководство и сравнение нового языка android-разработки с java

История

Остров расположен на легендарных путях «из варяг в греки» и «из варяг в арабы», и упомянут в договоре 1269 года Новгорода с Ганзой «Я, князь Ярослав Ярославич, … со всем Новгородом, и с немецким послом Генриком Вулленпунде из Любека, …, готами, подтвердил мир… А не возьмут они новгородского посла, и учинится, что между Новгородом и Котлингом, князю и новгородцам до того дела нет…» — по тексту напрашивается вывод о существовании города Котлинга на одноимённом острове, в котором осуществлялась перегрузка с речных судов на морские во времена, считающимися уже в 1269 году былинными («как исстари было»). В последующее время прибывавшие купцы дожидались на острове лоцманов из Новгорода, которые проводили торговые караваны через Неву и Волхов до Ильменя.

Ореховский мир в 1323 году установил совместное владение островом Котлин Новгородом и Швецией, по Тявзинскому мирному договору 1595 года граница со Швецией проходила через остров по линии Малая Ижора — Сестрорецк. По Столбовскому мирному договору 1617 года остров отошёл к Швеции.

Существует легенда, согласно которой шведы при высадке на остров русских поспешно бежали, оставив на костре котелок. Этот легендарный котелок изображён на гербе Кронштадта. От слова «котёл» якобы и происходит название Котлин. По другой легенде, Котлин назвали так потому, что на старых картах горловина Финского залива восточнее острова напоминала котёл.

На островке, отсыпанном на отмели к югу от острова Котлин, Петром I в 1703 году был заложен форт Кроншлот, который перекрыл для потенциального противника главный фарватер, ведущий к устью Невы, где строилась новая столица империи — Санкт-Петербург. 7 мая 1704 года укрепления, включавшие в себя и две батареи на острове Котлин, вступили в строй (дата основания Кронштадта).

В 1723 году на Котлине заложили крепость и дали ей имя Кронштадт. Пётр I считал Кронштадт частью столицы.

К началу XX века неприятеля встречали мощные крепости и форты Кронштадта, построенные и постоянно модернизируемые с учётом последних достижений военной науки; 17 искусственных островов с укреплениями, а также батареи на северном и южном берегу Финского залива. Многие сооружения не имели аналогов в мировой военной архитектуре. В Кронштадте находился целый ряд высших военных учреждений и торговый порт, который приносил казне огромный доход.

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

В честь острова назван язык программирования Kotlin, разрабатываемый компанией JetBrains.

Требования

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

  • Знания

    • на уровне представлений:
      • процесс подготовки и решения задач на ПЭВМ;
      • основные приемы программирования на языке Java и Kotlin;
      • принципы разработки программ;
  • Умения

    • теоретические:
      • оперировать понятийным аппаратом в сфере программирования; практические:
      • использовать основные приемы и методы программирования для построения алгоритмов решения конкретных учебных задач;
  • Навыки

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

В процессе обучения используется бесплатное программное обеспечение: IntelliJ IDEA, Android Studio.

Что требуется для начала

Самый простой способ начать программировать на Котлине — зайти на сайт http://try.kotlinlang.org. Имеющаяся там «песочница» позволяет писать программы прямо в браузере, с возможностью выполнять и сохранять свои программы и проходить обучающие курсы.

Масштабы песочницы, однако, достаточны только для небольших программ, а более-менее серьёзные программы, как правило, разрабатываются в интегрированной среде (IDE). Разработка под платформу Java в любом случае требует установки пакета JDK, который необходимо скачать с сайта компании Oracle. Первое время вам потребуется Java Platform, Standard Edition, рекомендуется 8-я её редакция, на сентябрь 2018 года последняя её версия — Java SE 8u181.

Формируемые компетенции

09.03.02 Информационные системы и технологии

  • способность использовать архитектурные и детализированные решения при проектировании систем; проводить выбор исходных данных дляпроектирования информационных систем, проводить сборкуинформационной системы из готовых компонентов, адаптироватьприложения к изменяющимся условиям функционирования (ПК-2)
  • способность использовать​ архитектурные и детализированные решения припроектировании систем; проводить предпроектное обследование(инжиниринг) объекта проектирования, системный анализ предметнойобласти, их взаимосвязей, проводить выбор исходных данных дляпроектирования информационных систем (ПК-4)
  • способность проводить​ выбор исходных данных для проектированияинформационных систем (ПК-12)
  • способность применять​ математические методы для решения практическихзадач (ОК-10)

09.04.02 Информационные системы и технолог

  • способность осуществлять сбор, анализ научно-технической информации, отечественного и зарубежного опыта по тематике исследования (ПК-7)
  • умение проводить разработку и исследование методик анализа, синтеза, оптимизации и прогнозирования качества процессов функционирования информационных систем и технологий (ПК-9)
  • способность воспринимать математические, естественнонаучные, социально-экономические и профессиональные знания, умением самостоятельно приобретать, развивать и применять их для решения нестандартных задач, в том числе в новой или незнакомой среде и в междисциплинарном контексте (ОПК-1)
  • способность анализировать и оценивать уровни своих компетенций в сочетании со способностью и готовностью к саморегулированию дальнейшего образования и профессиональной мобильности (ОПК-3)

Встроенная разметка

Для встроенной разметки KDoc использует Markdown синтаксис, расширенный для поддержки сокращенного синтаксиса с возможностью привязки к другим элементам кода.

Привязка элементов

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

Если вы хотите ввести свое обозначение для ссылки, используйте следующий Markdown синтаксис:

Вы также можете использовать подходящее имя в ссылках. Заметьте, в отличие от JavaDoc, подходящее имя всегда использует точку для разделения компонентов, даже до имени метода:

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

История

Язык разрабатывается с 2010 года, представлен общественности в июле 2011. Исходный код реализации языка открыт в феврале 2012. В феврале выпущен milestone 1, включающий плагин для IDEA. В июне — milestone 2 с поддержкой Android. В декабре 2012 года вышел milestone 4, включающий, в частности, поддержку Java 7.

В феврале 2016 года вышел официальный релиз-кандидат версии 1.0, а 15 февраля 2016 года — релиз 1.0. 1 марта 2017 вышел релиз 1.1.

В мае 2017 года компания сообщила, что инструменты языка Kotlin, основанные на JetBrains IDE, будут по стандарту включены в Android Studio 3.0 — официальный инструмент разработки для ОС Android.

На Google I/O 2019 было объявлено, что язык программирования Kotlin стал приоритетным в разработке под Android.

Операторы и специальные символы (Operators and Special Symbols)

Котлин поддерживает следующие операторы и специальные символы:

  • , , , , — математические операторы
    • оператор присваивания
    • используется для
  • , , , , —
  • , —
  • , , — логические операторы ‘и’, ‘или’, ‘не’ (для побитовых операций используют соответствующие )
  • , — (переведенные на вызовы для не-примитивных типов)
  • , —
  • , , , — (переведенные на вызовы для не-примитивных типов)
  • , — (переведенный на вызовы и )
  • выполняет (вызывает метод или обращается к свойству, если получатель не имеет значения null)
  • принимает правое значение, если левое значение равно нулю ()
  • создает или
  • создает диапазон
  • отделяет имя от типа в объявлениях
  • отмечает тип с
    • разделяет параметры и тело
    • разделяет параметры и тип возвращаемого значения
    • разделяет условие и тело ветви
    • вводит
    • вводит или ссылается на
    • вводит или ссылается на
    • ссылается на
    • ссылается на
  • разделяет несколько операторов на одной строке
  • ссылается на переменную или выражение в
    • заменяет неиспользуемый параметр в
    • заменяет неиспользуемый параметр в

Циклы

Последнее обновление: 05.12.2017

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

For

Цикл for пробегается по всем элементам коллекции. В этом плане цикл for в Kotlin эквивалентен циклу for-each в ряде других языков программирования.
Его формальная форма выглядит следующим образом:

for(переменная in поледовательность){
	выполняемые инструкции
}

Например, выведем все квадраты чисел от 1 до 9, используя цикл for:

for(n in 1..9){
	print("${n * n} \t")
}

В данном случае перебирается последовательность чисел от 1 до 9. При каждом проходе цикла (итерации цикла) из этой последовательности будет извлекаться элемент и
помещаться в переменную n. И через переменную n можно манипулировать значением элемента. То есть в данном случае мы получим следующий консольный вывод:

1 	4 	9 	16 	25 	36 	49 	64 	81

Циклы могут быть вложенными. Например, выведем таблицу умножения:

for(i in 1..9){
	for(j in 1..9){
		print("${i * j} \t")
	}
	println()
}

В итоге на консоль будет выведена следующая таблица умножения:

1 	2 	3 	4 	5 	6 	7 	8 	9 	
2 	4 	6 	8 	10 	12 	14 	16 	18 	
3 	6 	9 	12 	15 	18 	21 	24 	27 	
4 	8 	12 	16 	20 	24 	28 	32 	36 	
5 	10 	15 	20 	25 	30 	35 	40 	45 	
6 	12 	18 	24 	30 	36 	42 	48 	54 	
7 	14 	21 	28 	35 	42 	49 	56 	63 	
8 	16 	24 	32 	40 	48 	56 	64 	72 	
9 	18 	27 	36 	45 	54 	63 	72 	81

Цикл while

Цикл while повторяет определенные действия пока истинно некоторое условие:

var i = 10
while(i > 0){
	println(i*i)
	i--;
}

Здесь пока переменная i больше 0, будет выполняться цикл, в котором на консоль будет выводиться квадрат значения i.

В данном случае вначале проверяется условие (i > 0) и если оно истинно (то есть возвращает true), то выполняется цикл.
И вполне может быть ситуация, когда к началу выполнения цикла условие не будет выполняться. Например, переменная i
изначально меньше 0, тогда цикл вообще не будет выполняться.

Но есть и другая форма цикла while — do..while:

var i = -1
do{
	println(i*i)
	i--;
}
while(i > 0)

В данном случае вначале выполняется блок кода после ключевого слова do, а потом оценивается условие после
while. Если условие истинно, то повторяется выполнение блока после do. То есть несмотря на то, что в данном случае
переменная i меньше 0 и она не соответствует условию, тем не менее блок do выполнится хотя бы один раз.

Операторы continue и break

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

for(n in 1..8){
	if(n == 5) continue;
	println(n * n)
}

В данном случае когда n будет равно 5, сработает оператор continue. И последующая инструкция, которая выводит на консоль квадрат числа,
не будет выполняться. Цикл перейдет к обработке следующего элемента в массиве

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

for(n in 1..5){
	if(n == 5) break;
	println(n * n)
}

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

НазадВперед

Последние изменения

17.04.2018

Новая госзакупка в роли поставщика, контракт № 4405266992850300187,
контрагент:
ОГБУЗ «Боханская РБ»

01.06.2017

Новая госзакупка в роли поставщика, контракт № 3960051586285030018,
контрагент:
ОГБУЗ «Боханская РБ»

26.01.2017

Танганова Мария Александровна: ИНН руководителя изменен с 765780350850 на 135249400785

19.11.2016

Адрес организации исключен из реестра ФНС Адреса, указанные при государственной регистрации в качестве места нахождения несколькими юридическими лицами

23.10.2016

Адрес организации включен в реестр ФНС Адреса, указанные при государственной регистрации в качестве места нахождения несколькими юридическими лицами

31.08.2016

Удалены сведения о дополнительном виде деятельности: Прочие виды полиграфической деятельности (14033)

11.05.2016

Новая госзакупка в роли поставщика, контракт № 1249811510285030018,
контрагент:
ОГБУЗ «Боханская РБ»

03.03.2016

Юридический адрес изменен с 669310, Иркутская область, поселок Бохан, Типографский переулок, 1 на 669310, Иркутская область, Боханский район, поселок Бохан, Типографский переулок, 1

Рельеф

Западная оконечность Котлина является наиболее низменной частью острова, с высотами, не превышающими 2 метра над уровнем моря, береговыми валами и переувлажнённым понижением в центре. Искусственные формы рельефа территории — дороги и земляные валы фортов высотой до 12 метров — в несколько раз выше естественных форм рельефа. При частых наводнениях вся западная оконечность острова, кроме искусственных сооружений и насыпей, оказывается залитой водой.

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

Null Safety

Вот правда, когда кто-то говорит про Kotlin, то сразу думает про Null Safety, а тот, кто знает, что такое Null Safety, вспоминает про Kotlin. Для начала разберемся, что это вообще такое. Те, кто программирует на Java, очень не любят получать (NPE) где-нибудь на сервере, просто потому, что stack trace часто недостаточно информативен для выявления ошибки. Это в первую очередь связано с императивной природой языка Java: место, где определена (или объявлена, но не определена) переменная, и то место, где она используется, могут находиться очень далеко друг от друга как в пространственном (в коде), так и во временном (по времени выполнения) отношении.

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

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

Выражение в return — это специальный вариант оператора «элвис» (elvis — он так называется, потому что ?: похоже на emoji в стиле Элвиса Пресли), который представляет собой не что иное, как синтаксический сахар для конструкции

Последнее условное выражение — это пример так называемого smart cast в Kotlin, и если в условии была проверка на null, то вызов s.indexOf() считается абсолютно легитимным. Этот же механизм работает, если мы будем передавать аргументы, которые могут быть null, в качестве ненулевого формального параметра в функции.

А что, если мы в каком-то месте точно знаем, что переменная будет проинициализирована, но вначале она равна null (к этому вопросу в более общем случае мы вернемся чуть позднее)?

Здесь мы явным образом берем на себя ответственность за небезопасное преобразование. Если все-таки переменная canBeNull не будет определена, как предполагается, то мы получим . И в чем, собственно, плюсы такого подхода по сравнению с обычной Java? Такой подход позволяет локализовать ошибку. Даже если мы будем писать , то сам синтаксис помогает нам понять, где именно проблема. Строго говоря, возможна сложная цепочка, к примеру, и в этом случае также понятно, где источник потенциальных проблем. Правильное использование такого подхода позволяет изолировать крупные участки кода от потенциального NPE! Можно использовать конструкцию , но в отличие от предыдущего случая тип этого выражения будет , а не . Это означает, что если равен , то и само значение будет равно . Таким образом, будет , если любое из значений в цепочке равно null. Тем, кто знаком с функциональным программированием, это напоминает аппликативные функторы и монаду Maybe в Haskell.

Еще одна интересная особенность Kotlin в работе с — это использование . Если мы хотим, чтобы для ненулевых значений была выполнена какая-то операция, то можно сделать так:

Для приведения типов в Kotlin используется оператор , однако в случае преобразования из типа, который не допускает null, можно получить (если мы преобразуем в Int). Есть и более «умный» вариант:

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», увеличит личную накопительную скидку и позволит накапливать профессиональный рейтинг Xakep Score!
Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя!
Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.

Я уже участник «Xakep.ru»

Применение функций

При вызове функции используется традиционный подход

Для вызова вложенной функции используется знак точки

Инфиксная запись

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

  • Они являются членом другой функции или расширения
  • В них используется один параметр
  • Когда они помечены ключевым словом

Параметры

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

Аргументы по умолчанию

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

Значения по умолчанию указываются после типа знаком =.

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

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

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

Именованные аргументы

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

Рассмотрим следующую функцию:

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

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

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

Или, если нам не нужны все эти аргументы

При вызове функции как с позиционными, так и с именованными аргументами все позиционные аргументы должны располагаться перед первым именованным аргументом. Например, вызов разрешен, а — нет.

можно передать в именованной форме с помощью оператора spread:

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

Функции с возвращаемым типом

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

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

Функции с одним выражением

Когда функция возвращает одно-единственное выражение, фигурные скобки могут быть опущены, и тело функции может быть описано после знака

Компилятор способен сам определить типа возвращаемого значения.

Явные типы возвращаемых значений

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

Нефиксированное число аргументов (Varargs)

Параметр функции (обычно для этого используется последний) может быть помечен модификатором :

это позволит указать множество значений в качестве аргументов функции:

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

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

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

Функции

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

Контекстный объект доступен в качестве аргумента (). Возвращаемое значение — результат выполнения лямбды.

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

С функцией это код может быть переписан следующим образом:

Если блок кода содержит одну функцию, где является аргументом, то лямбда-выражение может быть заменено ссылкой на метод ():

часто используется для выполнения блока кода только с non-null значениями. Чтобы выполнить действия с non-null объектом, используйте оператор безопасного вызова совместно с функцией .

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

Не является функцией-расширением. Контекстный объект передается в качестве аргумента, а внутри лямбда-выражения он доступен как получатель (). Возвращаемое значение — результат выполнения лямбды.

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

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

Контекстный объект доступен в качестве получателя (). Возвращаемое значение — результат выполнения лямбды.

делает то же самое, что и , но вызывается как — как функция расширения контекстного объекта.

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

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

Контекстный объект доступен в качестве получателя (). Возвращаемое значение — контекстный объект.

Используйте для такого блока кода, который не возвращает значение и в основном работает с членами объекта-получателя. Типичный способ использования функции — настройка объекта-получателя. Это всеравно что мы скажем “примени перечисленные настройки к объекту.”

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

Контекстный объект доступен в качестве аргумента (). Возвращаемое значение — контекстный объект.

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

Когда вы видите в коде , то это можно прочитать как «а также с объектом нужно сделать следующее.»

«Сейчас никто не ищет разработчика со знание только Java или только Kotlin. Нужно быть универсальным специалистом»

Дмитрий Гордин

Android developer в RoadAR

О противостоянии. Чтобы не разводить демагогию и не основываться на личных ощущениях, давайте посмотрим на крупных игроков рынка. Во-первых, Google признает Kotlin основным языком разработки под Android. Во-вторых, вот так выглядят вакансии на HH.ru от топовых компаний:

  • Tinkoff — «Существенный плюс: опыт разработки приложений на Kotlin».
  • Яндекс — «Приветствуется: опыт разработки на Kotlin».
  • Redmadrobot — «Желательно: опыт разработки на Kotlin».
  • Даже «Почта России» — «Ожидаем от вас следующие знания и компетенции: знание технологического стека Android SDK, Kotlin/Java».

О выборе языка. Нужно знать оба языка. Невозможно знать Kotlin и не знать Java. Лига зануд может придраться и заметить, что есть Kotlin Native, который не связан с Java. Но мы говорим об Android разработке, а значит, о среде выполнения Dalvik/ART. Поэтому все работодатели, которые пишут, что ищут разработчика на Kotlin, на самом деле ищут оба языка.

По моему мнению, оптимальный путь — хорошенько освоиться с Java, а затем начать плавно переходить на Kotlin. Kotlin не переворачивает с ног на голову все, что вы знали из Java. Вы с минимальными изменениями сможете перейти на новый язык и писать в «Java-стиле», постепенно котлинизируя ваш код, добавляя в него сахар и специфичные для Kotlin фишки.

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Adblock
detector