Что такое частные и общие сборки

Что такое частные и общие сборки

Какой бы язык .NET не выбирался для программирования, важно понимать, что хотя двоичные .NET-единицы имеют такое же файловое расширение, как и двоичные единицы СОМ-серверов и неуправляемых программ Win32 (* . dll или * . ехе), внутренне они устроены абсолютно по-другому. Например, двоичные .NET-единицы * .dll не экспортируют методы для упрощения взаимодействия с исполняющей средой СОМ (поскольку .NET — это не СОМ). Более того, они не описываются с помощью библиотек СОМ-типов и не регистрируются в системном реестре. Пожалуй, самым важным является то, что они содержат не специфические, а наоборот, не зависящие от платформы инструкции на промежуточном языке (Intermediate Language — IL), а также метаданные типов. На следующей схеме показано, как все это выглядит:

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

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

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

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

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

Сборки бывают двух видов: разделяемые и приватные.

Приватные сборки

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

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

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

Поскольку приватная сборка полностью самодостаточна, процесс ее развертывания весьма прост. Вы просто помещаете соответствующий файл (или файлы) в соответствующую папку системы (никаких записей вносить в реестр не потребуется). Этот процесс известен как установка с нулевым воздействием или установка с помощью хсору (zero impact (хсору) installation).

Разделяемые сборки

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

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

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

Решение этих проблем включает размещение разделенных сборок в специальном поддереве каталогов файловой системы, известном под названием глобальный кэш сборок (global assembly cache — GAC). В отличие от приватных сборок, это не может быть сделано простым копированием сборки в определенную папку — сборку понадобится специальным образом установить в кэше GAC. Данный процесс может быть реализован с помощью множества утилит .NET и включает в себя выполнение необходимых проверок устанавливаемой сборки, а также создание небольшой иерархии папок в пределах GAC, используемых для обеспечения целостности сборок.

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

Читайте также:  Мойка воздуха вента отзывы польза и вред

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

Однофайловые и многофайловые сборки

В большом количестве случаев между сборками .NET и файлами двоичного кода (*.dll или *.ехе) соблюдается простое соответствие "один к одному". Следовательно, получается, что при построении *.dll-библиотеки .NET, можно спокойно полагать, что файл двоичного кода и сборка представляют собой одно и то же, и что, аналогичным образом, при построении исполняемого приложения для настольной системы на файл *.ехе можно ссылаться как на саму сборку. Однако это не совсем так. С технической точки зрения, сборка, состоящая из одного единственного модуля *.dll или *.ехе, называется . В однофайловых сборках все необходимые CIL-инструкции, метаданные и манифесты содержатся в одном автономном четко определенном пакете.

, в свою очередь, состоят из множества файлов двоичного кода .NET, каждый из которых называется модулем (module). При построении многофайловой сборки в одном из ее модулей (называемом первичным или главным (primary) модулем) содержится манифест всей самой сборки (и, возможно, СIL-инструкции и метаданные по различным типам), а во всех остальных — манифест, CIL-инструкции и метаданные типов, охватывающие уровень только соответствующего модуля. Как нетрудно догадаться, в главном модуле содержится описание набора требуемых дополнительных модулей внутри манифеста сборки.

В завершение мы решили остановиться на проблемах установки и размещения программ, а также на использовании готовых программ, использующих модель COM (Component Object Model). Вообще-то на эту тему можно написать целую книгу, но мы надеемся, что это короткое вступление поможет вам перейти к самостоятельному изучению этой темы.

Установка большинства приложений .NET сводится к простому копированию каталога, содержащего необходимые файлы, на любой компьютер с установленной исполнительной средой .NET. Программа запускается двойным щелчком на имени ЕХЕ-файла в окне Проводника (Windows Explorer).

Выбирая значок Setup and Deployment Project в диалоговом окне New Project, вы получите доступ к весьма нетривиальным возможностям установки. Мастер Setup Wizard чрезвычайно прост в использовании, но для большинства стандартных ситуаций его возможностей оказывается вполне достаточно.

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

Во многих устанавливаемых приложениях хотя бы часть работы выполняется традиционными объектами СОМ, поэтому в этой главе будут кратко затронуты вопросы использования объектов СОМ в .NET [И наоборот — объекты .NET могут использоваться в СОМ, однако эта возможность выглядит несколько экзотически.]. А поскольку одной из целей разработки .NET было исправление недостатков СОМ, мы начнем с краткого обзора СОМ и основных проблем, связанных с этой технологией.

Принципы работы СОМ

Технология СОМ упрощает создание программ, сохраняющих совместимость в разных версиях платформы Windows и более или менее независимых от языка программирования. Компоненты СОМ могут создаваться на разных языках, включая классический С (вариант для мазохистов), C++, Delphi, VB5 и 6. Технология СОМ с большим успехом применялась для создания объектов, предназначенных для решения специализированных задач, таких как элементы VB OCX.

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

Тем не менее у СОМ были свои недостатки. Во-первых, реализация СОМ для Windows требовала, чтобы в системном реестре хранилась вся информация обо всех компонентах в системе. Пользователю приходилось регистрировать компоненты при установке программ и стирать соответствующую информацию при удалении программ. При попытке удаления программ возникала опасность того, что изменения, внесенные в реестр, повлияют на работу других программ. Стоило серьезно повредить реестр, и система вообще переставала работать. Более того, установка новой версии компонента нередко нарушала работу программ, рассчитанных на более раннюю версию компонента.

В Windows 98 была впервые представлена концепция параллельного выполнения (side-by-side execution); это означало, что приложение могло использовать локальный экземпляр компонента СОМ, находящийся в каталоге приложения, вместо экземпляра, зарегистрированного в системе. Справедливости ради следует сказать, что параллельное выполнение так и не решило проблемы с «кошмаром DLL», вдобавок оно работает только в Windows 98, 2000 и ХР — и то если об этом специально позаботится разработчик программы.

Давайте посмотрим, что происходит на уровне реестра при регистрации компонентов СОМ.

  1. Разработчик создает для компонента глобально-уникальный идентификатор (GUID).
  2. Разработчик создает для компонента программный идентификатор (ProgID).
  3. Утилита регистрации связывает ProgID компонента с GUID, создавая соответствующую запись в реестре.
  4. Утилита регистрации заносит полный путь к двоичному файлу компонента в реестр и связывает его с GUID компонента.
  5. Утилита регистрации также может сохранить в реестре дополнительные сведения о компоненте — например, тип потоковой модели.

При попытке использования компонента происходит следующее:

  1. Разработчик приложения создает экземпляр компонента, используя ProgID.
  2. СОМ ищет в реестре GUID компонента.
  3. СОМ находит двоичный файл компонента.
  4. СОМ создает экземпляр компонента.

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

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

Читайте также:  Apple шнур для зарядки

С технической точки зрения сборка (assembly) в .NET представляет собой минимальную устанавливаемую единицу программного кода. Сборка оформляется в виде автономного ЕХЕ-файла или в виде библиотеки DLL, на которую можно ссылаться из других приложений. Однако сборка содержит нечто большее, чем обычный IL-код, компилируемый и выполняемый исполнительной средой .NET. Как минимум, сборка состоит из одного или нескольких модулей и классов, откомпилированных в IL-код, и метаданных (данных, описывающих данные [Префикс «мета» для подобных абстракций второго порядка позаимствован из метаматематики — области математики, посвященной описанию самих математических объектов.]), которые описывают сборку и функциональность входящих в нее классов. Метаданные являются частью сборки, поэтому в документации сборки названы самодокументируемыми. Во многих ситуациях сборка состоит из одного файла, но встречаются и многофайловые сборки. Например, в сборку могут входить ресурсные файлы, графические изображения и даже дополнительные EXE/DLL-файлы. В любом случае сборка является минимальным объектом .NET, для которого производится контроль версии или задаются привилегии.

В большинстве случаев создаются однофайловые сборки, состоящие из одного ЕХЕ-или DLL-файла.

Сборки бывают закрытыми (private) и общими (shared). Закрытые сборки всегда находятся в каталоге приложения или в одном из его подкаталогов. Общие сборки хранятся в глобальном кэше сборок (GAC, global assembly cache). Начнем с закрытых сборок, поскольку именно они используются по умолчанию для решений, построенных в VS .NET IDE. С общими сборками дело обстоит сложнее, и мы займемся ими позже.

Обычно у закрытых сборок не бывает проблем с несовместимостью версий, однако они требуют дополнительных затрат дискового пространства, если в системе приходится хранить несколько копий одного файла в разных каталогах [В наше время дисковое пространство обходится так дешево, что эти затраты с избытком компенсируются удобствами, связанными с использованием закрытых сборок.]. При создании ссылок на сборку командой Project > Add Reference по умолчанию в каталоге приложения создается новый экземпляр закрытой сборки. Мы рекомендуем по возможности ограничиваться использованием закрытых сборок.

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

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

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

Именно благодаря наличию манифеста появляется возможность создания сборок, состоящих из нескольких файлов. Кроме того, данные манифеста заменяют сложную систему регистрации компонентов в реестре. Первое представление о сборке и ее манифесте дает файл Assemblylnfo.vb; чтобы просмотреть содержимое этого файла, дважды щелкните на соответствующей строке окна решения VS .NET. Как видно из приведенного ниже примера, этот текстовый файл содержит многочисленные атрибуты сборки. Большинство атрибутов (например, название организации) можно редактировать вручную, хотя чаще значения задаются в IDE при помощи диалоговых окон свойств проекта.

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

‘Review the values of the assembly attributes

№ 2 Основы CLR и .NET. Типы. Массивы, кортежи и строки

a. Определите переменные всех возможных примитивных типов С# и проинициализируйте их.

b. Выполните 5 операций явного и 5 неявного приведения.

c. Выполните упаковку и распаковку значимых типов.

d. Продемонстрируйте работу с неявно типизированной переменной.

e. Продемонстрируйте пример работы с Nullable переменной.

a. Объявите строковые литералы. Сравните их.

b. Создайте три строки на основе String. Выполните: сцепление, копирование, выделение подстроки, разделение строки на слова, вставки подстроки в заданную позицию, удаление заданной подстроки.

c. Создайте пустую и null строку. Продемонстрируйте что можно выполнить с такими строками

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

a. Создайте целый двумерный массив и выведите его на консоль в отформатированном виде (матрица).

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

c. Создайте ступечатый (не выровненный) массив вещественных чисел с 3-мя строками, в каждой из которых 2, 3 и 4 столбцов соответственно. Значения массива введите с консоли.

d. Создайте неявно типизированные переменные для хранения массива и строки.

a. Задайте кортеж из 5 элементов с типами int, string, char, string, ulong.

b. Сделайте именование его элементов.

c. Выведите кортеж на консоль целиком и выборочно (1, 3, 4 элементы)

d. Выполните распаковку кортежа в переменные.

e. Сравните два кортежа.

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

6) Загрузите проект в свой репозиторий на GitHub.

7) Подготовить ответы на все вопросы.

1. Что такое .Net Framework и из чего он состоит?

2. Поясните, что такое CLR-среда.

3. Что такое FCL?

4. Какая наименьшая исполнимая единица в .NET?

6. Пояснить работу JIT-компилятора?

7. Что такое CTS (Common Type System)?

8. Какие аспекты поведения определяет тип System.Object?

9. Что находится в MSCorLib dll?

10. Что такое частные и общие сборки?

11. Что такое assembly manifest?

12. Что такое GAC?

13. Чем managed code отличается от unmanaged code

Читайте также:  Мультик добрый волшебник ээх

14. Как и для чего определен метод Main?

15. Варианты использования директивы using( using Directive ) в C#.

16. Как связаны между собой сборки и пространства имен?

17. Что такое примитивные типы данных? Перечислите их.

18. Что такое ссылочные типы? Какие типы относятся к ним?

19. Какие типы относятся к типам-значениям?

20. В чем отличие между ссылочными и значимыми типами данных?

21. Что такое упаковка и распаковка значимых типов?

22. Для чего используется тип dynamic?

23. Что такое неявно типизированная переменная?

24. Для чего используют Nullable тип?

25. Как объявить строковый литерал? Какие операции можно выполнять со строкой?

26. Какие есть способы для задания и инициализации строк?

27. Какие методы есть у типа String?

28. В чем отличие пустой и null строки?

29. Как можно выполнить сравнение строк?

30. В чем отличие типов String и StringBuilder?

31. Поясните явные преобразования переменных с помощью команд

32. Как выполнить консольный ввод/вывод?

33. Приведите примеры определения и инициализации одномерных и двумерных массивов.

34. Что такое ступенчатый массив? Как его задать?

35. Какие типы можно использовать в foreach? Приведите пример.

36. Что такое кортеж? Для чего и как он используется?

37. Что такое локальная функция?

Краткие теоретические сведения

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

Язык программирования C# является прямым наследником языка С++. Он унаследовал многие синтаксические конструкции языка С и объектноориентированную модель С++. В отличие от С++ С# является чисто объектноориентированным языком. В объектно-ориентированном программировании ход выполнения программы определяется объектами. Объекты это экземпляры класса. Класс это абстрактный тип данных, определяемый пользователем ( программистом). Класс включает в себя данные и функции для обработки этих данных. В С# запрещены глобальные функции. Все функции должны быть обязательно определены внутри класса. Не является исключением и главная функция языка С# Main( ) (в отличии от языка С пишется с прописной буквы).

Объявление класса синтаксически имеет следующий вид:

Члены класса это данные и функции для работы с этими данными. Рассмотрим шаблон приложения, подготовленный для нас мастером:

/// Summary description for Class1.

/// The main entry point for the application.

static void Main( string [] args)

// TODO: Add code to start application here

Первая строчка проекта using System; , включает в себя директиву using , которая сообщает компилятору, где он должен искать классы (типы), не определенные в данном пространстве имен. Мастер, по умолчанию, указывает стандартное пространство имен System , где определена большая часть типов среды .NET.

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

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

В шаблоне приложения имеется множество строк, которые являются комментариями.

В C# определены три вида комментариев:

— XML (///) – комментарий для поддержки возможности создания

Строчка [STAThread] является атрибутом. Атрибуты задаются в квадратных скобках. С помощью атрибута в программу добавляется дополнительная описательная информация, связанная с элементом кода, непосредственно перед которым задается атрибут. В нашем случае указывается однопоточная модель выполнения функции Main. Заголовок функции:

static void Main( string [] args)

Функция Main определена как статическая (static) с типом возвращаемого значения void. Функция Main( ) C# как и функция main( ) языка С может принимать аргументы. Аргумент — это строковый массив, содержащий элементы командной строки. Тело функции пустое и в нем содержится, в виде комментария, предложение добавить туда код для запуска приложения:

// TODO: Add code to start application here

Воспользуемся этим предложением и добавим в тело функции одну строчку:

static void Main( string [] args)

// TODO: Add code to start application here

Функции консольного ввода-вывода являются методами класса Console библиотеки классов среды .NET.

Для ввода строки с клавиатуры используется метод Console.ReadLine(), а для ввода одного символа метод Console.Read().

Для консольного вывода также имеются две метода

— метод Console.Write(), который выводит параметр, указанный в качестве аргумента этой функции, и

— метод Console.WriteLine(),который работает так же, как и Console.Write(), но добавляет символ новой строки в конец выходного текста.

Для анализа работы этих методов модифицируйте функцию Main( ) так, как показано ниже :

static void Main( string [] args)

// TODO: Add code to start application here

Console.WriteLine("Введите ваше имя"); string str=Console.ReadLine(); Console.WriteLine("Привет "+str+". ");

Console.WriteLine("Введите один символ с клавитуры"); int kod=Console.Read();

char sim=( char )kod;

Console.WriteLine("Код символа "+sim+" = "+kod);

Console.WriteLine("Код символа <0>= <1>",sim,kod);

Первым параметром списка является строка, содержащая маркеры в фигурных скобках. Маркер это номер параметра в списке. При выводе текста вместо маркеров будут подставлены соответствующие параметры из остального списка. После маркера через запятую можно указать, сколько позиций отводится для вывода значений. Например, запись <1,3>означает, что для печати первого элемента списка отводится поле шириной в три символа. Причем, если значение ширины положительно, то производится выравнивание по правому краю поля, если отрицательно то по левому.

Добавим 4 новые строчки в конец кода функции Main():

int s1=255; int s2=32;

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

Существует 8 различных форматов вывода:

С – формат национальной валюты,

D – десятичный формат,

E – научный (экспоненциальный) формат,

F – формат с фиксированной точкой,

G – общий формат,

N – числовой формат,

P – процентный формат,

X – шестнадцатеричный формат

Например, запись <2,9:C2>– означает, что для вывода второго элемента из списка, отводится поле шириной в 9 символов. Элемент выводится в формате денежной единицы с количеством знаков после запятой равной двум. При выводе результата происходит округление до заданной точности.

Ссылка на основную публикацию
Что написать о себе в инстаграмме девушке
Вроде как и всё ясно, но в самом деле, как только доходит до дела, написать о себе в Инстаграм, у...
Чем открыть cab файл на компьютере
Файл формата CAB открывается специальными программами. Чтобы открыть данный формат, скачайте одну из предложенных программ. Чем открыть файл в формате...
Чем открыть fb2 на телефоне
Формат электронных публикаций FB2, наряду с EPUB и MOBI, является одним из самых популярных для книг, публикуемых в интернете. Мы...
Что нового в айос 12 1
Apple выпустила iOS 12.1.1 − скорее всего, последнюю публичную сборку iOS 12 в этом году. Хотя это обновление по большей...
Adblock detector