Русский
Русский
English
Статистика
Реклама

Блог компании издательский дом «питер»

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

24.02.2021 12:05:30 | Автор: admin
image Привет, Хаброжители! Глубокие нейронные сети (DNN) становятся неотъемлемой частью IT-продуктов, провоцируя появление нового направления кибератак. Хакеры пытаются обмануть нейросети с помощью данных, которые не смогли бы обмануть человека.

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

Для кого предназначена книга


Целевая аудитория этой книги:

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

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

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


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

Схемы атак


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

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

Существующие схемы атак можно разбить на такие основные категории, как:

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

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

image

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

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

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

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

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

Прямая атака


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

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

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

Проведение прямой атаки весьма непростая задача. Чтобы найти идеально подходящий вредоносный входной сигнал, используя один из методов черного ящика, например граничную атаку, нужно выполнить очень много итераций (десятки тысяч). Каждая итерация, в свою очередь, может включать в себя несколько запросов к целевой ГНС. Таким образом, в итоге мы имеем громадное количество запросов, выполнение которых вряд ли останется незамеченным защищающейся организацией! Более того, ограничения по пропускной способности и времени задержки, характерные для коммерческого развертывания, будут замедлять скорость обработки этих запросов. На самом деле целевая система может даже специально ограничить количество запросов или ввести временную задержку перед выдачей ответов, чтобы защитить себя от такой атаки. Если злоумышленнику настолько повезет, что у него будет доступ к показателям, возвращаемым целевой системой, он может сократить объем запросов за счет использования таких интеллектуальных стратегий, как генетический алгоритм, рассмотренный в разделе Методы черного ящика с оценкой на с. 163. Однако, как уже говорилось ранее, в большинстве случаев есть лишь ограниченный доступ к этим оценкам.

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

Атака с копированием


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

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

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

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

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

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

Атака с переносом


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

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

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

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

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

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

  • Отображение входа на выход. Включает в себя формат и точность входных данных и допустимых выходных данных. В случае ГНС для классификации данных мы имеем набор категорий и, возможно, некоторую иерархическую структуру этих категорий (например, категория собака является подмножеством категории животное).
  • Внутренняя архитектура. Тип сети (например, LSTM или СНС), тип и количество слоев, количество узлов в каждом слое. Включает в себя особенности используемых функций активации фактически те аспекты модели, которые определяются до проведения обучения.
  • Параметры. Веса и пороги, определяемые в ходе обучения.

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

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

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

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

image

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

image

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

Универсальная атака с переносом


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

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

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

Гипотетический пример: обход классификации видеоданных

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

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

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

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

Об авторе


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

Более подробно с книгой можно ознакомиться на сайте издательства
Оглавление
Отрывок

Для Хаброжителей скидка 25% по купону Сети

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Подробнее..

Книга JavaScript с нуля

15.06.2021 18:13:51 | Автор: admin
imageПривет, Хаброжители! JavaScript еще никогда не был так прост! Вы узнаете все возможности языка программирования без общих фраз и неясных терминов. Подробные примеры, иллюстрации и схемы будут понятны даже новичку. Легкая подача информации и живой юмор автора превратят нудное заучивание в занимательную практику по написанию кода. Дойдя до последней главы, вы настолько прокачаете свои навыки, что сможете решить практически любую задачу, будь то простое перемещение элементов на странице или даже собственная браузерная игра.

Вот небольшой список того, что вы узнаете:

Как организовать код с помощью переменных.
Как функции делают ваш код повторно используемым.
Как работать с циклами и условиями.
Что такое глобальная и локальная области видимости.
Что такое замыкания.
Как правильно писать комментарии.
Основные типы объектов, с которыми вы столкнетесь в JavaScript.
Как работать с текстом и выполнять стандартные операции со строками.
Как использовать массивы для обработки списков.
Как создавать собственные объекты.

О ПИЦЦЕ, ТИПАХ, ПРИМИТИВАХ И ОБЪЕКТАХ


В ЭТОЙ ГЛАВЕ:

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

Пора заняться серьезными делами. Суперсерьезными! В последних нескольких главах мы изучили разные значения, в том числе: строки (текст), числа, логические значения (true и false), функции и другие встроенные элементы JavaScript.

Вот некоторые примеры, чтобы освежить память:

let someText = "hello, world!";let count = 50;let isActive = true;

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

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

Поехали!

Сначала поговорим о пицце


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

Если вы давненько ее не ели, то напомню, как она выглядит:

image

Конечно же, пицца не появляется в таком виде из ниоткуда. Она создается из простых и сложных ингредиентов:

image

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

image

Они не изготавливаются и не составляются из других компонентов.

К сложным же ингредиентам относятся сыр, соус, основа из теста и пеперони. Сложными их делает то, что они сделаны из других ингредиентов:

image

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

От пиццы к JavaScript


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

image

Подобно сыру, соусу, пеперони, грибам и бекону в нашей пицце, типами в JavaScript являются string (строка), number (число), boolean (логическое значение), null (пустой), undefined (не определен), bigint (целочисленные значения), symbol (символы) и Object (объект). С некоторыми из этих типов вы уже можете быть знакомы, с некоторыми нет. Подробнее мы будем рассматривать их в дальнейшем, сейчас же в табл. 12.1 вы можете посмотреть краткое описание их назначения.

image

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

К примитивным типам относятся string, number, boolean, null, bigint, symbol и undefined. Любые значения, попадающие в их юрисдикцию, не подлежат делению на части. Они являются халапеньо и грибами в мире JavaScript. Примитивы достаточно легко определять и оформлять в понятные элементы. В них нет глубины, и при встрече с ними мы, как правило, получаем то, что видим изначально.

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

Что такое объект?


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

Некоторые объекты вроде пресс-папье малофункциональны и могут долго бездействовать.

image

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

image

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

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

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

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

Предопределенные объекты в JavaScript


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

image

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

// массивlet names = ["Jerry", "Elaine", "George", "Kramer"];let alsoNames = new Array("Dennis", "Frank", "Dee", "Mac");// округленное числоlet roundNumber = Math.round("3.14");// текущая датаlet today = new Date();// объект booleanlet booleanObject = new Boolean(true);// бесконечностьlet unquantifiablyBigNumber = Number.POSITIVE_INFINITY;// объект stringlet hello = new String("Hello!");

Вас может несколько озадачить то, что примитивы string, boolean, symbol, bigint и number могут существовать и в форме объектов. Внешне эта объектная форма выглядит очень похожей на примитивную. Вот пример:

let movie = "Pulp Fiction";let movieObj = new String("Pulp Fiction");console.log(movie);console.log(movieObj);

При выводе обоих вариантах вы увидите одинаковый результат. Тем не менее внутренне movie и movieObj весьма различны. Первый буквально является примитивом типа string, а второй имеет тип Object. Это ведет к интересному (а иногда и непонятному) поведению, о котором я постепенно расскажу в процессе изучения встроенных типов.

КОРОТКО О ГЛАВНОМ

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

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

Более подробно с книгой можно ознакомиться на сайте издательства
Оглавление
Отрывок
Электронная версия книги цветная

Для Хаброжителей скидка 25% по купону JavaScript

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Подробнее..

Книга TypeScript быстро

22.04.2021 12:11:11 | Автор: admin
image Привет, Хаброжители! TypeScript быстро научит вас секретам продуктивной разработки веб- или самостоятельных приложений. Она написана практиками для практиков. В книге разбираются актуальные для каждого программиста задачи, объясняется синтаксис языка и описывается разработка нескольких приложений, в том числе нетривиальных так вы сможете понять, как использовать TypeScript с популярными библиотеками и фреймворками. Вы разберетесь с превосходным инструментарием TypeScript и узнаете, как объединить в одном проекте TypeScript и JavaScript. Среди продвинутых тем, рассмотренных авторами, декораторы, асинхронная обработка и динамические импорты. Прочитав эту книгу, вы поймете, что именно делает TypeScript особенным.


ДЛЯ КОГО ЭТА КНИГА
Эта книга написана для инженеров ПО, которые хотят повысить продуктивность разработки веб- или самостоятельных приложений. Мы, ее авторы, являемся практиками и книгу писали для таких же практиков. В ней не только объясняется синтаксис языка на простых примерах, но также описывается разработка нескольких приложений так вы можете понять, как использовать TypeScript с популярными библиотеками и фреймворками.

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

Этот материал, в свою очередь, предполагает, что у читателей уже есть практические познания в HTML, CSS и JavaScript, использующем последние нововведения из спецификации ECMAScript. Если вам знаком только синтаксис ECMAScript 5, рекомендуем просмотреть приложение, чтобы лучше понимать примеры кода, используемые в книге. Приложение выполняет роль введения в современный JavaScript.

СТРУКТУРА КНИГИ
Эта книга состоит из двух частей. В части 1 мы рассматриваем различные элементы синтаксиса TypeScript, приводя для наглядности небольшие образцы кода. В части 2 мы используем TypeScript в нескольких версиях блокчейн-приложения. Если ваша цель как можно быстрее освоить синтаксис TypeScript и сопутствующие инструменты, тогда части 1 будет достаточно.

Глава 1 поможет начать разработку с помощью TypeScript. Вы скомпилируете и запустите самые простые программы, чтобы понять суть рабочего процесса от написания программ в TypeScript и до их компиляции в выполняемый JavaScript. Мы также рассмотрим преимущества программирования в TypeScript в сравнении с JavaScript и представим вам редактор Visual Studio Code.

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

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

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

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

Глава 6 посвящена сопутствующим инструментам. В ней мы рассказываем об использовании карт кода и TSLinter (несмотря на то, что TSLinter считается устаревшим, многие разработчики по-прежнему им пользуются). Затем мы покажем вам, как компилировать и обвязывать (bundle) приложения TypeScript с помощью Webpack. Вы также узнаете, как и зачем компилировать TypeScript с помощью Babel.

Глава 7 научит вас использовать библиотеки JavaScript в вашем TypeScript-приложении. Мы начнем с объяснения роли файлов определений типов, а затем представим небольшое приложение, использующее библиотеку JavaScript в приложении TypeScript. В завершение мы пройдем через весь процесс постепенного перевода имеющегося проекта JavaScript в TypeScript.

В части 2 мы используем TypeScript в блокчейн-приложении. Здесь вы можете подумать: Ни одна из компаний, где я работал, не использует блокчейн, так зачем мне его изучать, если моя цель освоение TypeScript? Мы не хотели, чтобы наш образец приложения был очередным стандартным примером, поэтому озадачились поиском горячей технологии, в которой можно было бы применить различные элементы TypeScript, а также техники, представленные в части 1. Знакомство с тем, как TypeScript может быть использован в нетривиальном приложении, придаст содержанию больше практичности, даже если вы не собираетесь использовать блокчейн в ближайшем будущем.

В этой части книги вы разработаете несколько блокчейн-приложений: самостоятельное приложение, браузерное приложение, а также приложения на Angular, React.js и Vue.js. При этом можете выбрать к прочтению только интересующие вас главы, но обязательно прочитайте главы 8 и 10, где объясняются основные принципы.

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

В главе 9 описывается создание веб-клиента для блокчейна. Это приложение не будет использовать никакие фреймворки; в нем мы задействуем только HTML, CSS и TypeScript. Мы также создадим небольшую библиотеку для генерации хеша, которую можно будет использовать как в веб-, так и в самостоятельных клиентах. Помимо этого, вы увидите, как производить отладку кода TypeScript в своем браузере.

Глава 10 рассматривает код блокчейн-приложения, использующего сервер обмена сообщениями для связи между членами блокчейна. Мы создадим Node.js и WebSocket сервер в TypeScript и покажем вам, как для достижения консенсуса блокчейн использует правило длиннейшей цепочки. Вы найдете практические примеры использования TypeScript интерфейсов, абстрактных классов, квалификаторов доступа, перечислений и обобщенных типов.

В главе 11 дается краткий обзор разработки веб-приложений в Angular при помощи TypeScript, а глава 12 рассматривает код блокчейн-веб-клиента, разработанного с помощью этого фреймворка.

Глава 13 дает краткое введение в разработку веб-приложений в React.js с помощью TypeScript, а глава 14 рассматривает код блокчейн-веб-клиента, разработанного с React.

Глава 15 аналогичным образом представляет разработку веб-приложений в Vue.js с помощью TypeScript, а глава 16 рассматривает блокчейн-веб-клиент, разработанный с использованием Vue.

ИСПОЛЬЗОВАНИЕ КОМПИЛЯТОРА BABEL


Babel это популярный JS-компилятор, предлагающий решение для широко известной проблемы: не все браузеры поддерживают весь набор возможностей, объявленных в ECMAScript. Мы даже не говорим о полной реализации конкретной версии ECMAScript. В любой момент времени один из браузеров может реализовать конкретную выборку возможностей из ECMAScript 2019, в то время как другой по-прежнему будет понимать только ECMAScript 5. Посетите сайт caniuse.com и поищите arrow functions (стрелочные функции). Вы увидите, что Internet Explorer 11, OperaMini и некоторые другие браузеры их не поддерживают.

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

На рис. 6.9 показан фрагмент таблицы совместимости с браузерами (из mng.bz/O9qw). Сверху вы видите названия браузеров и компиляторов, а слева расположен список возможностей. Браузер, компилятор или среда выполнения сервера могут полностью либо частично поддерживать некоторые из перечисленных возможностей, а плагины Babel позволяют указывать только конкретные из них, которые требуется трансформировать в более старый код. Полный список плагинов можно посмотреть в документации Babel: babeljs.io/docs/en/plugins.

image


В целях этого рассмотрения мы выбрали функцию Обрезка строк из ES2019 (см. черную стрелку слева от рис. 6.9). Давайте предположим, что наше приложение должно работать в браузере Edge. Проследуйте по вертикальной стрелке, и вы увидите, что Edge 18 на данный момент реализует обрезку строк только частично (2/4).

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

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

В разделе A.12 приложения мы включили скриншот из babeljs.io, иллюстрирующий Babel-инструмент REPL. Взгляните на меню Babel Try it now, показанное на рис. 6.10, где вы увидите навигационную панель, позволяющую конфигурировать предустановки.

Каждая предустановка просто является группой плагинов, и если вы хотите скомпилировать код в синтаксис ES2015, достаточно отметить галочкой es2015. Вместо указания имен спецификаций ECMAScript вы можете настраивать конкретные версии браузеров или других сред выполнения, используя опцию ENV PRESET. Белая стрелка на рис. 6.10 показывает редактируемое окошко с предполагаемыми значениями для предустановки ENV: >2%, ie11, safari>9. Это означает, что вы хотите, чтобы Babel скомпилировал код для запуска во всех браузерах, имеющих рыночный охват не менее 2%, а также в Internet Explorer 11 и Safari.

Ни IE11, ни Safari 9 не поддерживают стрелочные функции, и если вы введете (a, b) ?a+b;, Babel преобразует это в JS, который перечисленные браузеры понимают, как показано в правой части рис. 6.11.

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

Теперь давайте изменим предустановку на last 2 chrome versions (две последние версии Chrome), как показано на рис. 6.12. Babel достаточно сообразителен, чтобы понять, что последние две версии Chrome поддерживают стрелочные функции и нет нужды производить преобразование.

image

Предустановка ENV идет со списком браузеров, и вам нужно использовать верные имена и фразы, чтобы указать ограничения (например, last2majorversions, Firefox>=20 или >5% in US). Эти фразы перечислены в проекте browserslist, который доступен здесь: github.com/browserslist/browserslist.

Мы использовали предустановку ENV в Babel REPL, чтобы поиграть с целевыми средами, но эти настройки могут быть также сконфигурированы и использованы из командной строки. В листинге 6.15 мы добавим в файл конфигурации .babelrc следующее: babel/preset-env. В листинге 6.17 вы увидите файл .browserlistrc, в котором вы можете настроить конкретные браузеры и версии, как мы делали в Babel REPL. Подробнее о preset-env вы можете прочитать в документации Babel здесь: babeljs.io/docs/en/next/babel-preset-env.html.

image


Babel может использоваться для компиляции таких языков, как JavaScript, TypeScript, CoffeeScript, Flow и др. Например, фреймворк React использует синтаксис JSX, который не относится к стандарту JS, и Babel это понимает. В главе 12 мы используем Babel с приложением React.

Когда Babel компилирует TS, он не выполняет проверку типов в отличие от tsc. Создатели Babel не реализовали полноценный компилятор TS. Babel просто считывает TS-код и генерирует соответствующий синтаксис JS.

Должно быть, вы подумали: Я вполне доволен компилятором TS. Зачем вообще в эту книгу о TypeScript включать раздел о компиляторе JS-в-JS? Причина в том, что вы можете подключиться к проекту, где часть модулей написаны в JS, а часть в TS. В таких проектах Babel может уже быть частью потока разработки-развертывания. Например, Babel популярен среди разработчиков, использующих фреймворк React, который лишь недавно начал поддерживать TS.

Подобно любому npm-пакету, вы можете установить Babel локально или глобально (с опцией -g). Локальная установка внутри директории проекта делает этот проект самодостаточным, так как после запуска npm install вы можете использовать Babel, не ожидая, что он установлен где-то в другом месте (кто-нибудь может работать над вашим проектом с другого компьютера).

npm install babel/core babel/cli babel/preset-env

Здесь babel/core является компилятором Babel, babel/cli интерфейсом командной строки, а babel/preset-env это предустановка ENV, которую мы недавно рассматривали.

В реестре npmjs.org пакеты JavaScript могут быть организованы как ветки. Например, babel это ветка для пакетов, относящихся к Babel. angular это ветка для пакетов, принадлежащих фреймворку Angular. @types это место для файлов определений типов TS для различных популярных JS-библиотек.


В последующих разделах мы представим вам три небольших проекта. Первый использует Babel с JS, второй Babel с TS, а третий Babel, TS и Webpack.

Использование Babel с JavaScript

В этом разделе мы рассмотрим простой проект, использующий Babel с JavaScript и расположенный в директории babel-javascript. Мы продолжим работать с трехстрочным скриптом index.js, представленным в листинге 6.7 и использующем JS-библиотеку Chalk. Единственное изменение в следующем листинге заключается в том, что сообщение теперь гласит Compiled with Babel (Скомпилировано с помощью Babel).

Листинг 6.15. index.js: исходный код приложения babel-javascript

const chalk = require('chalk');const message = 'Compiled with Babel';console.log(chalk.black.bgGreenBright(message));


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

image


Babel настраивается в файле .babelrc, и наш файл конфигурации будет очень прост. Нам нужно только использовать preset-env для компиляции.

Листинг 6.17. Файл .babelrc

{    "presets": [       "@babel/preset-env"    ]}


Мы не настраивали здесь никакие конкретные версии браузеров, и без каких бы то ни было опций конфигурации babel/preset-env ведет себя в точности так же, как babel/preset-es2015, babel/preset-es2016 и babel/preset-es2017. Другими словами, все возможности языка, представленные в ECMAScript2015, 2016 и 2017, будут скомпилированы в ES5.

СОВЕТ Мы сконфигурировали Babel в файле .babelrc, который отлично подходит для статических конфигураций вроде нашей. Если ваш проект требует программного создания конфигураций Babel, вам понадобится использовать файл babel.config.js (подробнее в документации Babel здесь: babeljs.io/docs/en/config-files#project- wide-configuration). Если вы хотите увидеть, как Babel компилирует наш файл src/index.js, установите зависимости этого проекта, выполнив npm install, а затем запустите npm-скрипт из package.json: npm run babel.

Следующий листинг показывает скомпилированную версию index.js, созданную в директории dist. Она будет иметь следующее содержимое (сравните с листингом 6.15):

image


Скомпилированный файл по-прежнему вызывает require('chalk'), и эта библиотека расположена в отдельном файле. Помните, что Babel это не бандлер. Мы используем Webpack с Babel в разделе 6.4.3.


Можете запустить скомпилированную версию так:

node dist/index.js

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

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

image


Листинг 6.19. Пример файла .browserslistrc

last 2 chrome versionslast 2 firefox versions


Теперь при запуске Babel не будет преобразовывать const в var, как в листинге 6.18, потому что и Firefox, и Chrome уже поддерживают ключевое слово const. Попробуйте сами, чтобы убедиться.

Использование Babel с TypeScript

В этом разделе мы рассмотрим простой проект, использующий Babel с TypeScript; он размещен в директории babel-typescript. Мы продолжим работать с трехстрочным скриптом, представленным в листинге 6.11 и использующим JS-библиотеку Chalk. Единственное изменение будет в том, что теперь сообщение гласит Compiled with Babel (Скомпилировано с помощью Babel).

Листинг 6.20. index.ts: исходный код приложения babel-typescript

import chalk from 'chalk';const message: string = 'Compiled with Babel';console.log(chalk.black.bgGreenBright(message));


В сравнении с package.json из чистого JS-проекта (см. листинг 6.16) наш TS-проект добавляет dev-зависимость preset-typescript, отделяющую типы TS от кода, чтобы Babel мог воспринимать его как чистый JS. Мы также добавим опцию --extensions '.ts' в npm-скрипт, запускающий Babel, как в листинге 6.21. Теперь Babel будет считывать файлы .ts.

image


Как правило, предустановки включают набор плагинов, но preset-typescript содержит только один, babel/plugin-transform-typescript. Этот плагин внутренне использует babel/plugin-syntax-typescript, чтобы считывать TypeScript, и babel/helper-plugin-utils для основных утилит плагинов.

Несмотря на то что babel/plugin-transform-typescript преобразует TS-код в синтаксис ES.Next, это не компилятор TS. Как бы странно это ни звучало, Babel просто стирает TypeScript. Например, он преобразует const x: number = 0 в const x = 0. babel/plugin-transform-typescript намного быстрее, чем компилятор TS, так как не производит проверку типов для вводных файлов.

babel/plugin-transform-typescript имеет несколько небольших ограничений, перечисленных в документации на babeljs.io/docs/en/babel-plugin-transform-typescript (например, он не поддерживает const enum). Для лучшей поддержки TS рассмотрите использование плагинов babel/plugin-proposal-class-properties и babel/plugin-proposal-object-rest-spread.

Прочитав первые пять глав этой книги, вы наверняка уже начали ценить проверку типов и обнаружение ошибок во время компиляции, осуществляемое реальным компилятором TS. Неужели теперь мы действительно предлагаем вам использовать Babel, чтобы стереть связанный с TS синтаксис? Не совсем так. В процессе разработки вы можете продолжать использовать tsc (с помощью tsconfig.json) и IDE с полной поддержкой TypeScript. Тем не менее на стадии развертывания вы можете все же ввести Babel- и ENV-предустановки. (Скорее всего, вы уже оценили гибкость, предлагаемую ENV-предустановками, при конфигурировании целевых браузеров, не так ли?)

В вашем процессе сборки вы можете даже добавить npm-скрипт (в package.json), запускающий tsc:

check_types: tsc --noEmit src/index.ts

Теперь вы можете последовательно запустить check_types и Babel при наличии локально установленного tsc:

npm run check_types && npm run babel

Опция --noEmit гарантирует, что tsc не сгенерирует никаких файлов (вроде index.js), так как это будет сделано командой babel, выполняемой сразу после check_types. Если в index.js присутствуют ошибки компиляции, процесс сборки провалится и команда babel даже не запустится.

СОВЕТ Если вы используете && (двойной амперсанд) между двумя npm-скриптами, они будут выполняться последовательно. Для параллельного выполнения используйте & (одинарный амперсанд). Подробности вы можете найти во врезке Использование амперсандов в npm-скриптах в Windows в главе 10.

В этом проекте файл конфигурации .babelrc включает babel/preset-typescript.

Листинг 6.22. Файл .babelrc

{"presets": ["@babel/preset-env","@babel/preset-typescript"]}


В сравнении с проектом babel-javascript мы сделали следующие относящиеся к TypeScript изменения:

Добавили опцию --extensions '.ts' в команду, запускающую Babel.
Добавили в package.json связанные с TypeScript dev-зависимости.
Добавили babel/preset-typescript в файл конфигурации .babelrc.

Чтобы скомпилировать наш простой скрипт index.ts, запустите следующий npm-скрипт из package.json:

npm run babel

Вы найдете скомпилированную версию index.js в директории dist. Вы можете запустить скомпилированный код так же, как мы это делали в предыдущем разделе:

node dist/index.js

Теперь давайте добавим в наш рабочий поток Webpack, чтобы связать скрипт index.js и JS библиотеку Chalk.

Использование Babel с TypeScript и Webpack

Babel это компилятор, но не бандлер, который необходим для любого реального приложения. Вы вольны выбирать из ряда доступных бандлеров (вроде Webpack, Rollup и Browserify), но мы будем придерживаться Webpack. В этом разделе мы рассмотрим простой проект, использующий Babel с TypeScript и Webpack. Расположен он в директории webpack-babel-typescript.

В разделе 6.3.2 мы рассмотрели настройку TypeScript-Webpack, и далее мы продолжим использовать наш трехстрочный исходный код из того проекта.

Листинг 6.23. index.ts: исходный код приложения webpack-babel-typescript

import chalk from 'chalk';const message: string = 'Built with Babel bundled with Webpack';console.log(chalk.black.bgGreenBright(message));


В следующем листинге показан раздел devDependency из package.json.

image


Сравните зависимости Babel в листингах 6.24 и 6.21. В листинге 6.24 присутствуют три изменения:

Мы добавили babel-loader, являющийся Webpack-загрузчиком для Babel.
Мы удалили babel-cli, потому что не будем запускать Babel из командной строки.
Вместо babel-cli Webpack будет использовать babel-loader как часть процесса связывания.

Как вы помните из раздела 6.3, Webpack использует файл конфигурации webpack.config.js. Для настройки TS с помощью Webpack мы использовали ts-loader (см. листинг 6.14). В данном же случае мы хотим, чтобы файлы с расширением .ts обрабатывал babel-loader. Следующий листинг показывает раздел Babel из файла webpack.config.js.

image


Файл .babelrc будет выглядеть в точности так же, как и в предыдущем разделе (см. листинг 6.20).

После установки зависимостей командой npm install мы готовы к созданию связки через выполнение команды bundleup из package.json:

npm run bundleup

Эта команда создаст index.bundle.js в директории dist. Этот файл будет содержать скомпилированную (при помощи Babel) версию файла index.js, а также код из JS-библиотеки Chalk. Запустить эту связку вы можете как обычно:

node dist/index.bundle.js

Вывод, показанный на рис. 6.14, будет знакомым.

Для генерации JavaScript не нужно выбирать между Babel и tsc. Они могут успешно сосуществовать в одном проекте.

image


Противники TypeScript зачастую используют такой аргумент: Если я буду писать на чистом JS, мне не потребуется использовать компилятор. Я смогу запустить JS-программу сразу после ее написания. Это абсолютно ошибочно, так как если вы не хотите игнорировать новейший синтаксис JS, представленный, начиная с версии 2015, потребуется процесс, который сможет компилировать код, написанный в современном JS, в код, который смогут понять все браузеры. Скорее всего, вы так или иначе будете использовать в своем проекте компилятор, будь то Babel, TypeScript или какой-либо другой.

ОБ АВТОРАХ

Яков Файн является сооснователем двух IT-компаний: Farata Systems и SuranceBay. Он автор и соавтор таких книг, как Java Programming: 24-Hour Trainer, Angular Development with TypeScript1, Java Programming for Kids и др. Являясь чемпионом Java, провел множество классов и семинаров, посвященных веб- и Java-технологиям. Помимо этого, также был докладчиком на международных конференциях. Файн опубликовал более тысячи статей в своем блоге на yakovfain.com. В твиттере и инстаграме его можно найти по адресу @yfain. Он также публикует видео на YouTube.

Антон Моисеев является ведущим разработчиком в SuranceBay. Провел за разработкой корпоративных приложений более десяти лет, работая с Java и .NET. Имеет обширный опыт и фокусируется на развитии веб-технологий, реализующих лучшие методики слаженной работы фронтенда и бэкенда. Некоторое время проводил обучение по фреймворкам AngularJS и Angular. Периодически Антон делает посты в блоге antonmoissev.com. В твиттере вы можете найти его по адресу @antonmoiseev.

Более подробно с книгой можно ознакомиться на сайте издательства
Оглавление
Отрывок

Для Хаброжителей скидка 25% по купону TypeScript

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Подробнее..

Перевод Проектирование API почему для представления отношений в API лучше использовать ссылки, а не ключи

03.02.2021 10:04:43 | Автор: admin
Привет, Хабр!

У нас выходит долгожданное второе издание книги "Веб-разработка с применением Node и Express".



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


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

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

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

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

Согласно стандарту Internet Engineering Task Force (IETF), веб-ссылку можно представить как инструмент для описания отношений между страницами в вебе. Наиболее известные веб-ссылки те, что фигурируют на HTML-страницах и заключаются в элементы link или anchor, либо в заголовки HTTP. Но ссылки также могут фигурировать и в ресурсах API, а при использовании их вместо внешних ключей существенно сокращается объем информации, которую поставщику API приходится дополнительно документировать, а пользователю изучать.

Ссылка это элемент в одном веб-ресурсе, содержащий указание на другой веб-ресурс, а также название взаимоотношения между двумя ресурсами. Ссылка на другую сущность записывается в особом формате, именуемом уникальный идентификатор ресурса (URI), для которого существует стандарт IETF. В этом стандарте словом ресурс называется любая сущность, на которую указывает URI. Название типа отношения в ссылке можно считать аналогичным имени того столбца базы данных, в котором содержатся внешние ключи, а URI в ссылке аналогичен значению внешнего ключа. Наиболее полезны из всех URI те, по которым можно получить информацию о ресурсе, на который проставлена ссылка, при помощи стандартного веб-протокола. Такие URI называются унифицированный указатель ресурса (URL) и наиболее важной разновидностью URL для API являются HTTP URL.

В то время как ссылки не находят широкого применения в API, некоторые очень известные веб-API все-таки основаны на HTTP URL, используемых в качестве средства представления взаимоотношений. Таковы, например, Google Drive API и GitHub API. Почему так складывается? В этой статье я покажу, как на практике строится использование внешних ключей API, объясню их недостатки по сравнению с использованием ссылок, и расскажу, как преобразовать дизайн, использующий внешние ключи, в такой, где применяются ссылки.

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


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

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



Взаимоотношение между Лесси и Джо выражается так: в представлении Лесси Джо обозначен как обладатель имени и значения, соответствующих владельцу. Обратное взаимоотношение не выражено. Значение владельца равно 98765, это внешний ключ. Вероятно, это самый настоящий внешний ключ базы данных то есть, перед нами значение первичного ключа из какой-то строки какой-то таблицы базы данных. Но, даже если реализация API немного изменит значения ключей, она все равно сближается по основным характеристикам с внешним ключом.
Значение 98765 не слишком годится для непосредственного использования клиентом. В наиболее распространенных случаях клиенту, воспользовавшись этим значением, нужно составить URL, а в документации к API необходимо описать формулу для выполнения такого преобразования. Как правило, это делается путем определения шаблона URI, вот так:

/people/{person_id}

Обратное взаимоотношение любимцы принадлежат владельцу также можно предоставить в API, реализовав и документировав один из следующих шаблонов URI (различия между ними только стилистические, а не по существу):

/pets?owner={person_id}
/people/{person_id}/pets


В API, спроектированных по такому принципу, обычно требуется определять и документировать много шаблонов URI. Наиболее популярным языком для определения таких шаблонов является не тот, что задан в спецификации IETF, а язык OpenAPI (ранее известный под названием Swagger). До версии 3.0 в OpenAPI не существовало способа указать, какие значения полей могут быть вставлены в какие шаблоны, поэтому часть документации требовалось составлять на естественном языке, а что-то приходилось угадывать клиенту. В версии 3.0 OpenAPI появился новый синтаксис под названием links, призванный решить эту проблему, но для того, чтобы пользоваться этой возможностью последовательно, надо потрудиться.

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

Представление взаимоотношений при помощи ссылок


Что, если бы ресурсы, показанные выше, были видоизменены следующим образом:



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

Обратите внимание: обратное взаимоотношение, то есть, от питомца к владельцу, теперь тоже реализовано явно, поскольку к представлению Joel добавлено поле "pets".

Изменение "id" на "self", в сущности, не является необходимым или важным, но существует соглашение, что при помощи "self" идентифицируется ресурс, чьи атрибуты и взаимоотношения указаны другими парами имя/значение в том же объекте JSON. "self" это имя, зарегистрированное в IANA для этой цели.

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

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

В предыдущем примере я использовал в ссылках относительную форму записи URI, например, /people/98765. Возможно, клиенту было бы немного удобнее (хотя, автору при форматировании этого поста было не слишком сподручно), если бы я выразил URI в абсолютной форме, напр. pets.org/people/98765. Клиентам необходимо знать лишь стандартные правила URI, определенные в спецификациях IETF, чтобы преобразовывать такие URI из одной формы в другую, поэтому выбор конкретной формы URI не так важен, как могло бы показаться на первый взгляд. Сравните эту ситуацию с описанным выше преобразованием из внешнего ключа в URL, для чего требовались конкретные знания об API зоомагазина. Относительные URL несколько удобнее для тех, кто занимается реализацией сервера, о чем рассказано ниже, но абсолютные URL, пожалуй, удобнее для большинства клиентов. Возможно, именно поэтому в API Google Drive и GitHub используются абсолютные URL.

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

Подводные камни


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

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

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

  1. Не переписывайте URL в прокси. Я стараюсь избегать переписывания URL, но в вашей среде может не быть такой возможности.
  2. В прокси аккуратно найдите и переназначьте им формат везде, где они фигурируют в запросе или в отклике. Я так никогда не делал, поскольку мне это кажется сложным, чреватым ошибками и неэффективным, но кто-то, возможно, так поступает.
  3. Записывайте все ссылки в относительном виде. Можно не только встроить во все прокси некоторые возможности по перезаписи URL; более того, относительные URL могут упростить использование одного и того же кода в тестировании и в продакшене, так как код не придется конфигурировать и знать для этого его хост-имя. Если писать ссылки с использованием относительных URL, то есть, с единственным ведущим слэшем, как я показал в примере выше, то возникают некоторые минусы как для сервера, так и для клиента. Но в таком случае в прокси появляется лишь возможность сменить хост-имя (точнее, те части URL, которые называются схемой и источником), но не путь. В зависимости от того, как построены ваши URL, вы можете реализовать в прокси некоторую возможность переписывать пути, если готовы писать ссылки с использованием относительных URL без ведущих слэшей, но я так никогда не делал, поскольку полагаю, что серверам будет сложно записывать такие URL как следует.


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

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

/v1/pets/12345
/v2/pets/12345
/v1/people/98765
/v2/people/98765


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

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

Возможно, формат 2 для описания владельцев даже не будет предусмотрен. Также нет концептуального смысла в том, чтобы использовать в ссылках конкретную версию URL ведь Лесси принадлежит не конкретной версии Джо, а Джо как таковому. Поэтому, даже если вы предоставляете URL в формате /v1/people/98765 и идентифицируете таким образом конкретную версию Джо, то также должны предоставлять URL /people/98765 для идентификации самого Джо, и именно второй вариант использовать в ссылках. Другой вариант определить только URL /people/98765 и позволить клиентам выбирать конкретную версию, включая для этого заголовок запроса. Для этого заголовка нет никакого стандарта, но, если называть его Accept-Version, то такой вариант хорошо сочетается с именованием стандартных заголовков. Лично я предпочитаю использовать для версионирования заголовок и избегаю ставить в URL номера версий. но URL с номерами версий популярны, и я часто реализую и заголовок. и версионные URL, так как легче реализовать оба варианта, чем спорить, какой лучше. Подробнее о версионировании API можете почитать в этой статье.

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


В большинстве веб-API URL нового ресурса выделяется сервером, когда новый ресурс создается при помощи метода POST. Если вы пользуетесь этим методом для создания ресурсов и указываете взаимоотношения при помощи ссылок, то вам не требуется публиковать шаблон для URI этих ресурсов. Однако, некоторые API позволяют клиенту контролировать URL нового ресурса. Позволяя клиентам контролировать URL новых ресурсов, мы значительно упрощаем многие паттерны написания скриптов API разработчикам клиентской части, а также поддерживаем сценарии, в которых API используется для синхронизации информационной модели с внешним источником информации. В HTTP для этой цели предусмотрен специальный метод: PUT. PUT означает создай ресурс по этому URL, если он еще не существует, а если он существует обнови его. Если ваш API позволяет клиентам создавать новые сущности при помощи метода PUT, то вы должны документировать правила составления новых URL, возможно, включив для этого шаблон URI в спецификацию API. Также можно предоставить клиентам частичный контроль над URL, включив первичное ключ-подобное значение в тело или заголовки POST. В таком случае не требуется шаблон URI для POST как такового, но клиенту все равно придется выучить шаблон URI, чтобы полноценно пользоваться достигаемой в результате предсказуемостью URI.

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

В вышеприведенном примере мы включили следующую пару имя/значение в представление Джо:

"pets": "/pets?owner=/people/98765"

Клиенту, чтобы пользоваться этим URL, не требуется что-либо знать о его структуре кроме того, что он был записан в соответствии со стандартными спецификациями. Таким образом, клиент может получить по этой ссылке список питомцев Джо, не изучая для этого никакой язык запросов. Также отсутствует необходимость документировать в API форматы его URL но только в случае, если клиент сначала сделает запрос GET к /people/98765. Если же, кроме того, в API зоомагазина документирована возможность выполнения запросов, то клиент может составить такой же или эквивалентный URL запроса, чтобы извлечь питомцев интересующего его владельца, не извлекая перед этим самого владельца достаточно будет знать URI владельца. Возможно, даже важнее, что клиент может формировать и запросы, подобные следующим, что в ином случае было бы невозможно:

/pets?owner=/people/98765&species=Dog
/pets?species=Dog&breed=Collie


Спецификация URI описывает для этой цели часть HTTP URL, называемую "компонент запроса" это участок URL после первого ? и до первого #. Стиль запрашивания URI, который я предпочитаю использовать всегда ставить клиент-специфичные запросы в компонент запроса URI. Но при этом допустимо выражать клиентские запросы и в той части URL, которая называется путь. Так или иначе, необходимо описать клиентам, как составляются эти URL вы фактически проектируете и документируете язык запросов, специфичный для вашего API. Разумеется, также можно разрешить клиентам ставить запросы в теле сообщения, а не в URL, и пользоваться методом POST, а не GET. Поскольку существует практический лимит по размеру URL превышая 4k байт, вы всякий раз испытываете судьбу рекомендуется поддерживать POST для запросов, даже если вы уже поддерживаете GET.

Поскольку запросы такая полезная возможность в API, и поскольку проектировать и реализовывать языки запросов непросто, появились такие технологии, как GraphQL. Я никогда не пользовался GraphQL, поэтому не могу рекомендовать его, но вы можете рассмотреть его в качестве альтернативы для реализации возможности запросов в вашем API. Инструменты для реализации запросов в API, в том числе, GraphQL, лучше всего использовать в качестве дополнения к стандартному HTTP API для считывания и записи ресурсов, а не как альтернативу HTTP.

И кстати Как лучше всего писать ссылки в JSON?


В JSON, в отличие от HTML, нет встроенного механизма для выражения ссылок. Многие по-своему понимают, как ссылки должны выражаться в JSON, и некоторые подобные мнения публиковались в более или менее официальных документах, но в настоящее время нет стандартов, ратифицированных авторитетными организациями, которые бы это регламентировали. В вышеприведенном примере я выражал ссылки при помощи обычных пар имя/значение, написанных на JSON предпочитаю такой стиль и, кстати, этот же стиль используется в Google Drive и GitHub. Другой стиль, который вам, вероятно, встретится, таков:
  {"self": "/pets/12345", "name": "Lassie", "links": [   {"rel": "owner" ,    "href": "/people/98765"   } ]}

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

Есть и другой стиль написания ссылок на JSON, который мне нравится, и он выглядит так:
 {"self": "/pets/12345", "name": "Lassie", "owner": {"self": "/people/98765"}}


Польза этого стиля в том, что он явно дает: "/people/98765" это URL, а не просто строка. Я изучил этот паттерн по RDF/JSON. Одна из причин освоить этот паттерн вам так или иначе придется им пользоваться, всякий раз, когда вы захотите отобразить информацию об одном ресурсе, вложенную в другом ресурсе, как показано в следующем примере. Если использовать этот паттерн повсюду, код приобретает красивое единообразие:

{"self": "/pets?owner=/people/98765", "type": "Collection",  "contents": [   {"self": "/pets/12345",    "name": "Lassie",    "owner": {"self": "/people/98765"}   } ]}


Более подробно о том, как лучше всего использовать JSON для представления данных, рассказано в статье Terrifically Simple JSON.

Наконец, в чем же разница между атрибутом и взаимоотношением?


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

{"self": "/people/98765", "shoeSize": 10}

Принято считать, что shoeSize это атрибут, а не взаимоотношение, а 10 это значение, а не сущность. Правда, не менее логично утверждать, что строка '10 фактически является ссылкой, записанной специальной нотацией, предназначенной для ссылок на числа, до 11-го целого числа, которое само по себе является сущностью. Если 11-е целое число совершенно полноценная сущность, а строка '10' лишь указывает на нее, то пара имя/значение '"shoeSize": 10' концептуально является ссылкой, хотя, здесь и не используются URI.

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

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

Ссылки попросту лучше


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

Перевод Rust теперь и на платформе Android

10.04.2021 14:17:23 | Автор: admin

Корректность кода на платформе Android является наиважнейшим аспектом в контексте безопасности, стабильности и качества каждого релиза Android. По-прежнему сложнее всего вытравливаются ошибки, связанные с безопасностью памяти и попадающиеся в коде на С и C++. Google вкладывает огромные усилия и ресурсы в обнаружение, устранение багов такого рода, а также в уменьшение вреда от них, старается, чтобы багов в релизы Android проникало как можно меньше. Тем не менее, несмотря на все эти меры, ошибки, связанные с безопасностью памяти, остаются основным источником проблем со стабильностью. На их долю неизменно приходится ~70% наиболее серьезных уязвимостей Android.

Наряду с текущимиипланируемымимероприятиями по улучшению выявления багов, связанных с памятью, Google также наращивает усилия по их предотвращению. Языки, обеспечивающие безопасность памяти наиболее эффективные и выгодные средства для решения этой задачи. Теперь в рамках проекта Android Open Source Project (AOSP) наряду с языками Java и Kotlin, отличающимися безопасностью памяти, поддерживается и язык Rust, предназначенный для разработки операционной системы как таковой.

Системное программирование

Управляемые языки, в частности, Java и Kotlin, лучше всего подходят для разработки приложений под Android. Эти языки проектировались в расчете на удобство использования, портируемость и безопасность. Среда исполнения Android (ART)управляет памятью так, как указал разработчик. В операционной системе Android широко используется Java, что фактически защищает большие участки платформы Android от багов, связанных с памятью. К сожалению, на низких уровнях ОС Android Java и Kotlin бессильны.

На низких уровнях ОС нужны языки для системного программирования, такие как C, C++ и Rust. При проектировании этих языков приоритет отдавался контролируемости и предсказуемости. Они обеспечивают доступ к низкоуровневым ресурсам системы и железу. Они обходятся минимальными ресурсами, прогнозировать их производительность также сравнительно просто.

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

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

Пределы работы в песочнице

Языки C и C++ не дают таких же гарантий безопасности, как Rust, и требуют надежной изоляции. Все процессы Android укладываются в песочницу, и мы придерживаемся правила двух, принимая решение, требуется ли для конкретной функциональности дополнительная изоляция и снижение привилегий. Правило двух формулируется просто: при наличии следующих трех вариантов действий, разработчики вправе выбрать лишь два из них.

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

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

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

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

Что же насчет всего имеющегося C++?

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

Вышеприведенный анализ возраста багов, связанных с безопасностью памяти (отсчитывается с момента их появления) позволяет судить, почему команда Android делает акцент на новых разработках, а не на переписывании зрелого кода на C/C++. Большинство багов возникает в новом или недавно измененном коде, причем, возраст около 50% багов составляет менее года.

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

Ограничения находимости багов

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

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

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

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

Предотвращение прежде всего

В Rust модернизируется ряд языковых аспектов, что позволяет улучшить корректность кода:

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

  • Конкурентность данных предотвращает гонку данных. Учитывая, насколько легко при этом становится писать эффективный потокобезопасный код, Rust обрел слоганБезбоязненная Конкурентность.

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

  • Ссылки и переменные по умолчанию являются неизменяемыми что помогает разработчику следовать безопасному принципу наименьших привилегий. Программист помечает ссылку или переменную как изменяемые, только если в самом деле намерен сделать их таковыми. Притом, что в C++ есть const, эта возможность обычно используется нечасто и несогласованно. Напротив, компилятор Rust помогает избегать случайных аннотаций об изменяемости, так как выдает предупреждения об изменяемых значениях, которые никогда не меняются.

  • Улучшенная обработка ошибок в стандартных библиотеках потенциально провальные вызовы обертываются в Result, и поэтому компилятор требует от пользователя проверять возможность провала даже для функций, не возвращающих необходимого значения. Это позволяет защититься от таких багов как уязвимостьRage Against the Cage, возникающая из-за необработанной ошибки. Обеспечивая легкое просачивание ошибок при помощи оператора ? и оптимизируя Result с расчетом на низкие издержки, Rust стимулирует пользователей писать все потенциально провальные функции в одном и том же стиле, благодаря чему все они получают ту же защиту.

  • Инициализация требует, чтобы все переменные инициализировались перед началом использования. Исторически сложилось, что неинициализированные уязвимости памяти были в Android причиной 3-5% уязвимостей, связанных с безопасностью. В Android 11, чтобы сгладить эту проблему, стала применятьсяавтоматическая инициализация памяти на C/C++. Однако, инициализация в ноль не всегда безопасна, особенно для таких штук, как возвращаемые значения, и в этой области может стать новым источником неправильной обработки ошибок. Rust требует, чтобы любая переменная перед использованием инициализировалась в полноценный член своего типа. Тем самым избегается проблема непреднамеренной инициализации небезопасного значения. Подобно Clang в C/C++, компилятор Rust знает о требовании инициализации и позволяет избежать потенциальных проблем производительности, связанных с двойной инициализацией.

  • Более безопасная обработка целых чисел Очистка во избежание включена в отладочных сборках Rust по умолчанию, что стимулирует программистов указывать wrapping_add, если действительно предполагается допустить переполнение при расчетах, или saturating_add если не предполагается. Очистка при переполнении в дальнейшем должна быть включена для всех сборок Android. Кроме того, при всех преобразованиях целочисленных типов применяются явные приведения: разработчик не может сделать случайного приведения в процессе вызова функции при присваивании значения переменной или при попытке выполнять арифметические операции с другими типами.

Что дальше

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

Подробнее..

Книга PowerShell для сисадминов

27.05.2021 16:21:56 | Автор: admin
image Привет, Хаброжители! PowerShell это одновременно язык сценариев и командная оболочка, которая позволяет управлять системой и автоматизировать практически любую задачу. В книге PowerShell для сисадминов обладатель Microsoft MVP Адам Бертрам aka the Automator покажет, как использовать PowerShell так, чтобы у читателя наконец-то появилось время на игрушки, йогу и котиков. Вы научитесь: -Комбинировать команды, управлять потоком выполнения, обрабатывать ошибки, писать сценарии, запускать их удаленно и тестировать их с помощью фреймворка тестирования Pester. -Анализировать структурированные данные, такие как XML и JSON, работать с популярными сервисами (например Active Directory, Azure и Amazon Web Services), создавать системы мониторинга серверов. -Создавать и проектировать модули PowerShell. -Использовать PowerShell для удобной, полностью автоматизированной установки Windows. -Создавать лес Active Directory, имея лишь узел Hyper-V и несколько ISO-файлов. -Создавать бесчисленные веб- и SQL-серверы с помощью всего нескольких строк кода! Реальные примеры помогают преодолеть разрыв между теорией и работой в настоящей системе, а легкий авторский юмор упрощает чтение. Перестаньте полагаться на дорогое ПО и невнятные советы из сети!

Для кого эта книга
Эта книга предназначена для ИТ-специалистов и системных администраторов, которым надоело постоянно использовать один и тот же интерфейс и выполнять одну и ту же задачу в пятисотый раз за этот год. Также она будет полезна для инженеров DevOps, которые испытывают затруднения с автоматизацией новых серверных сред, выполнением автоматических тестов или автоматизацией конвейера непрерывной интеграции / непрерывной доставки (CI/CD).

Не получится назвать отрасль, для которой PowerShell был бы полезен больше всего. Традиционная должность пользователя PowerShell в магазине Windows системный администратор Microsoft, однако PowerShell хорошо вписывается в набор инструментов любого сотрудника в сфере ИТ. Если вы работаете в ИТ, но не считаете себя разработчиком, эта книга для вас.

Поток управления


Немного повторим. В главе 3 мы узнали, как можно комбинировать команды с помощью конвейера и внешних сценариев. В главе 2 рассмотрели переменные и как их использовать для хранения значений. Одним из основных преимуществ работы с переменными является возможность писать с их помощью код, который работает не со значением, а со смыслом. Вместо того чтобы работать, например, с числом 3, вы будете работать с общим понятием $serverCount. За счет этого вы можете писать код, который работает одинаково, будь у вас один, два или тысяча серверов. Совместите эту способность с возможностью сохранять код в сценариях, которые можно запускать на разных компьютерах, и вы сможете начать решать задачи гораздо большего масштаба.

Однако в жизни порой имеет значение, работаете ли вы с одним сервером, с двумя или с тысячей. Пока что у вас нет подходящего способа учитывать это, и ваши сценарии работают просто сверху вниз, не имея возможности адаптироваться в зависимости от определенных значений. В этой главе мы будем использовать поток управления и условную логику для написания сценариев, которые будут выполнять различные команды в зависимости от значений, с которыми они работают. К концу главы вы узнаете, как использовать операторы if/then и switch, а также различные циклы, чтобы придать вашему коду столь необходимую гибкость.

Немного о потоке управления

Мы напишем сценарий, который считывает содержимое файла, хранящегося на нескольких удаленных компьютерах. Чтобы продолжить работу, загрузите файл под названием App_configuration.txt из прилагаемых к книге материалов по ссылке github.com/adbertram/PowerShellForSysadmins/ и поместите его в корень диска C:\ на нескольких удаленных компьютерах. Если у вас нет удаленных компьютеров, пока просто продолжайте читать. В этом примере я буду использовать серверы с именами SRV1, SRV2, SRV3, SRV4 и SRV5.

Чтобы получить доступ к содержимому файла, воспользуемся командой Get-Content и укажем путь к файлу в значении аргумента параметра Path, как показано ниже:

Get-Content -Path "\\servername\c$\App_configuration.txt"

Для начала сохраним все имена наших серверов в массиве и запустим эту команду для каждого сервера. Откройте новый файл .ps1 и введите в него код из листинга 4.1.

Листинг 4.1. Извлечение содержимого файла с нескольких серверов

$servers = @('SRV1','SRV2','SRV3','SRV4','SRV5')Get-Content -Path "\\$($servers[0])\c$\App_configuration.txt"Get-Content -Path "\\$($servers[1])\c$\App_configuration.txt"Get-Content -Path "\\$($servers[2])\c$\App_configuration.txt"Get-Content -Path "\\$($servers[3])\c$\App_configuration.txt"Get-Content -Path "\\$($servers[4])\c$\App_configuration.txt"


Теоретически, этот код должен работать без проблем. Но в этом примере предполагается, что у вас что-то идет не так. Что делать, если сервер SRV2 не работает? А если кто-то забыл положить App_configuration.txt на SRV4? А может, кто-то изменил путь к файлу? Вы можете написать отдельный сценарий для каждого сервера, но это решение не будет масштабироваться, особенно когда вы начнете добавлять все больше и больше серверов. Вам нужен код, который будет работать в зависимости от ситуации.

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

Мы начнем с рассмотрения самого простого типа потока управления условного оператора.

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

В главе 2 мы узнали, что существуют логические значения: истина и ложь. Логические значения позволяют создавать условные операторы, которые ставят задачу PowerShell выполнить определенный блок кода в зависимости от того, имеет ли выражение (называемое условием) значение True или False. Условие это вопрос с вариантами ответов да/нет. У вас больше пяти серверов? Работает ли сервер 3? Существует ли путь к файлу? Чтобы начать использовать условные операторы, давайте посмотрим, как преобразовать такие вопросы в выражения.

Построение выражений с помощью операторов

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

PS> 1 eq 1
True

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

-eq сравнивает два значения и возвращает True, если они равны.

-ne сравнивает два значения и возвращает True, если они не равны.

-gt сравнивает два значения и возвращает True, если первое больше второго.

-ge сравнивает два значения и возвращает True, если первое больше или равно второму.

-lt сравнивает два значения и возвращает True, если первое меньше второго.

-le сравнивает два значения и возвращает True, если первое меньше или равно второму.

-contains возвращает True, если второе значение является частью первого. Например, этот оператор позволяет определить, находится ли значение внутри массива.

В PowerShell есть и более продвинутые операторы сравнения. Здесь мы не будем на них останавливаться, но я рекомендую вам почитать о них в документации Microsoft по ссылке docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_comparison_operators или в разделе справки PowerShell (см. главу 1).

Вы можете использовать приведенные выше операторы для сравнения переменных и значений. Но выражение не обязательно должно быть сравнением. Иногда команды PowerShell можно использовать как условия. В предыдущем примере мы хотели узнать доступность сервера. С помощью командлета Test-Connection можно проверить наличие связи с сервером. Обычно в выходных данных командлета Test-Connection содержится много разной информации, но с помощью параметра Quiet вы можете заставить команду вернуть True или False, а с помощью параметра Count можно ограничить тест одной попыткой.

PS> Test-Connection -ComputerName offlineserver -Quiet -Count 1
False

PS> Test-Connection -ComputerName onlineserver -Quiet -Count 1
True

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

PS> -not (Test-Connection -ComputerName offlineserver -Quiet -Count 1)
True

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

Оператор if

Оператор if работает просто: если выражение X истинно, то сделайте Y. Вот и все!

Чтобы использовать оператор в выражении, пишется ключевое слово if, за которым следуют круглые скобки, содержащие условие. После выражения следует блок кода, выделенный фигурными скобками. PowerShell выполнит этот блок кода только в том случае, если это выражение будет иметь значение True. Если выражение if имеет значение False либо вообще ничего не возвращает, блок кода не будет выполнен. Синтаксис оператора if/then показан в листинге 4.2.

Листинг 4.2. Синтаксис оператора if

if (условие) {   # выполняемый код, если условие истинно}


В этом примере есть немного нового синтаксиса: символ решетки (#) обозначает комментарий это текст, который PowerShell игнорирует. Вы можете использовать комментарии, чтобы оставить полезные примечания и описания для себя или кого-нибудь, кто позже будет читать ваш код.

Теперь давайте еще раз посмотрим на код, показанный в листинге 4.1. Я расскажу вам о том, как использовать оператор if, чтобы не пытаться достучаться до неработающего сервера. В предыдущем разделе мы уже видели, что команду Test-Connection можно использовать в качестве выражения, которое возвращает True или False, поэтому сейчас давайте упакуем Test-Connection в оператор if, а затем воспользуемся командой Get-Content, чтобы не пытаться обращаться к неработающему серверу. Сейчас мы поменяем код только для первого сервера, как показано в листинге 4.3.

Листинг 4.3. Использование оператора if для выборочного обращения

$servers = @('SRV1','SRV2','SRV3','SRV4','SRV5')if (Test-Connection -ComputerName $servers[0] -Quiet -Count 1) {   Get-Content -Path "\\$($servers[0])\c$\App_configuration.txt"}Get-Content -Path "\\$($servers[1])\c$\App_configuration.txt"--пропуск--


Поскольку у вас есть Get-Content в операторе if, вы не столкнетесь с какими-либо ошибками, если попытаетесь получить доступ к неработающему серверу; если тест завершится неудачно, ваш сценарий будет знать, что не следует пытаться считать файл. Код попытается получить доступ к серверу, только если он уже знает, что тот включен. Но обратите внимание, что этот код срабатывает только в том случае, если условие истинно. Достаточно часто вам нужно будет задать одно поведение сценария для истинного условия и другое для ложного. В следующем разделе вы увидите, как определить поведение для ложного условия с помощью оператора else.

Оператор else

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

Листинг 4.4. Использование оператора else для запуска кода, если условие
не истинно

if (Test-Connection -ComputerName $servers[0] -Quiet -Count 1) {   Get-Content -Path "\\$($servers[0])\c$\App_configuration.txt"} else {   Write-Error -Message "The server $($servers[0]) is not responding!"}


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

Оператор elseif

Оператор else охватывает противоположную ситуацию: если if не срабатывает, значит, выполните это в любом случае. Такой подход работает для двоичных условий, то есть когда сервер либо работает, либо нет. Но иногда приходится иметь дело с большим числом вариантов. Например, предположим, что у вас есть сервер, на котором нет нужного вам файла, и вы сохранили имя этого сервера в переменной $problemServer (добавьте в свой сценарий эту строку кода!). Это означает, что вам нужна дополнительная проверка, позволяющая узнать, является ли сервер, который вы опрашиваете в данный момент, проблемным. Это можно реализовать с помощью вложенных операторов if, как показано в коде ниже:

if (Test-Connection -ComputerName $servers[0] -Quiet -Count 1) {   if ($servers[0] eq $problemServer) {      Write-Error -Message "The server $servers[0] does not have the right         file!"   } else {      Get-Content -Path "\\$servers[0]\c$\App_configuration.txt"   }} else {   Write-Error -Message "The server $servers[0] is not responding!"}--пропуск--


Но есть и более аккуратный способ реализовать ту же логику с помощью оператора elseif, который позволяет вам проверить дополнительное условие, перед тем как вернуться к коду в блоке else. Синтаксис блока elseif идентичен синтаксису блока if. Итак, чтобы проверить проблемный сервер с помощью оператора elseif, запустите код из листинга 4.5.

Листинг 4.5. Использование блока elseif

if (-not (Test-Connection -ComputerName $servers[0] -Quiet -Count 1)) {    Write-Error -Message "The server $servers[0] is not responding!"} elseif ($servers[0] eq $problemServer)    Write-Error -Message "The server $servers[0] does not have the right file!"} else {   Get-Content -Path "\\$servers[0]\c$\App_configuration.txt" }--пропуск--


Обратите внимание, что мы не просто добавили оператор elseif, а заодно изменили логику кода. Теперь мы можем проверить, не находится ли сервер в автономном режиме, с помощью оператора not. Затем, как только мы определили сетевой статус сервера, мы проверяем, является ли он проблемным. Если это не так, мы используем оператор else для запуска поведения по умолчанию извлечения файла. Как видите, существует несколько способов структурировать код описанным образом. Важно то, что код работает и что он читабелен для человека со стороны, будь то ваш коллега, видящий его впервые, или вы сами спустя некоторое время после написания.

Вы можете объединить в цепочку сколько угодно операторов elseif, что позволяет учитывать самые разные сочетания обстоятельств. Однако операторы elseif являются взаимоисключающими: когда один из elseif принимает значение True, PowerShell запускает только его код и не проверяет остальные случаи. В листинге 4.5 это не вызвало никаких проблем, так как вам нужно было проверить сервер на предмет проблемности только после проверки работоспособности, но в дальнейшем я советую вам держать в уме эту особенность.

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

Оператор switch

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

Обратите внимание, что теперь мы будем работать с другим типом условия. Если раньше нам нужны были ответы на вопросы типа да/нет, то теперь мы хотим получить конкретное значение одной вещи. Это сервер SRV1? SRV2? И так далее. Если бы вы работали только с одним или двумя конкретными значениями, оператор if подошел бы, но в данном случае оператор switch сработает гораздо лучше.

Оператор switch позволяет выполнять различные фрагменты кода в зависимости от некоторого значения. Он состоит из ключевого слова switch, за которым следует выражение в скобках. Внутри блока switch находится серия операторов, построенных по следующему принципу: сначала указывается значение, за ним следует набор фигурных скобок, содержащих блок кода, и наконец ставится блок default, как указано в листинге 4.6.

Листинг 4.6. Шаблон для оператора switch

switch (выражение) {   значениевыражения {      # Код   }   значениевыражения {   }   default {     # Код, который выполняется при отсутствии совпадений   }}


Оператор switch может содержать практически неограниченное количество значений. Если выражение оценивается как значение, выполняется соответствующий код внутри блока. Важно то, что, в отличие от elseif, после выполнения одного блока кода PowerShell продолжит проверять и остальные условия, если не указано иное. Если ни одно из значений не подойдет, PowerShell выполнит код, указанный в блоке default. Чтобы прекратить перебор условий в операторе switch, используйте ключевое слово break в конце блока кода, как показано в листинге 4.7.

Листинг 4.7. Использование ключевого слова break в операторе switch

switch (выражение) {   значениевыражения {      # Код      break   }--пропуск--


Ключевое слово break позволяет сделать условия в операторе switch взаимоисключающими. Вернемся к нашему примеру с пятью серверами и одним и тем же файлом, имеющим разные пути. Вы знаете, что сервер, с которым вы работаете, может иметь только одно значение (то есть он не может одновременно называться и SRV1, и SRV2), поэтому вам нужно использовать операторы break. Ваш сценарий должен выглядеть примерно так, как показано в листинге 4.8.

Листинг 4.8. Проверка различных серверов с помощью оператора switch

$currentServer = $servers[0]switch ($currentServer) {   $servers[0] {      # Check if server is online and get content at SRV1 path.      break   }   $servers[1] {      ## Check if server is online and get content at SRV2 path.      break   }   $servers[2] {      ## Check if server is online and get content at SRV3 path.      break   }--пропуск--


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

Использование циклов

Существует хорошее практическое правило для работы за компьютером: не повторяйся (dont repeat yourself, DRY). Если вы обнаружите, что выполняете одну и ту же работу, то, скорее всего, существует способ ее автоматизировать. То же самое и с написанием кода: если вы используете одни и те же строки снова и снова, вероятно, существует решение получше.

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

PowerShell предлагает пять типов циклов: foreach, for, do/while, do/until и while. В этом разделе мы обсудим каждый тип цикла, отметим их уникальные черты и выделим лучшие ситуации для их использования.

Об авторе

Адам Бертрам (Adam Bertram) опытный ИТ-специалист и эксперт в области интернет-бизнеса с 20-летним стажем. Предприниматель, ИТ-инфлюенсер, специалист Microsoft MVP, блогер, тренинг-менеджер и автор материалов по контент-маркетингу, сотрудничающий со многими ИТ-компаниями. Также Адам основал популярную платформу TechSnips для развития навыков ИТ-специалистов (techsnips.io).

Более подробно с книгой можно ознакомиться на сайте издательства
Оглавление
Отрывок

Для Хаброжителей скидка 25% по купону PowerShell

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Подробнее..

Книга C для профи

19.04.2021 16:09:25 | Автор: admin
image Привет, Хаброжители! С++ популярный язык для создания ПО. В руках увлеченного программиста С++ становится прекрасным инструментом для создания лаконичного, эффективного и читаемого кода, которым можно гордиться.

C++ для профи адресован программистам среднего и продвинутого уровней, вы продеретесь сквозь тернии к самому ядру С++. Часть 1 охватывает основы языка С++ от типов и функций до жизненного цикла объектов и выражений. В части II представлена стандартная библиотека C ++ и библиотеки Boost. Вы узнаете о специальных вспомогательных классах, структурах данных и алгоритмах, а также о том, как управлять файловыми системами и создавать высокопроизводительные программы, которые обмениваются данными по сети.


Об этой книге

Современные программисты на C++ имеют доступ к ряду очень качественных книг, например Эффективный современный C++ Скотта Мейерса1 и Язык программирования C++ Бьёрна Страуструпа, 4-е издание2. Однако эти книги написаны для достаточно продвинутых программистов. Доступны также некоторые вводные тексты о C++, но они часто пропускают важные детали, потому что ориентированы на абсолютных новичков в программировании. Опытному программисту непонятно, где можно погрузиться в язык C++.

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

Кому будет интересна эта книга?

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

В ПОЗНАКОМИТЕСЬ С ОСНОВНМИ ФИШКАМИ СОВРЕМЕННОГО С++:

Базовые типы, ссылочные и пользовательские типы.
Полиморфизм во время компиляции и полиморфизм во время выполнения.
Жизненный цикл объекта, включая длительность хранения, стек вызовов, управление памятью, исключения и парадигму RAII.
Продвинутые выражения, операторы и функции.
Умные указатели, структуры данных, дата и время, числовые данные и др.
Контейнеры, итераторы, строки и алгоритмы.
Потоки и файлы, многозадачность, сетевое программирование и разработка приложений.

Отслеживание жизненного цикла объекта


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

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

Листинг 4.5. Класс Tracer и его конструктор с деструктором

#include <cstdio>struct Tracer {    Tracer(const char* name1) : name{ name }2 {       printf("%s constructed.\n", name); 3    }    ~Tracer() {       printf("%s destructed.\n", name); 4    }private:    const char* const name;};

Конструктор принимает один параметр 1 и сохраняет его в члене name 2. Затем он печатает сообщение, содержащее name 3. Деструктор 4 также выводит сообщение с name.

Рассмотрим программу в листинге 4.6. Четыре различных объекта Tracer имеют разную длительность хранения. Просматривая порядок вывода программы Tracer, вы можете проверить полученные знания о длительности хранения.

Листинг 4.6. Программа, использующая класс Tracer в листинге 4.5 для иллюстрации длительности хранения

#include <cstdio>struct Tracer {    --пропуск--};static Tracer t1{ "Static variable" }; 1thread_local Tracer t2{ "Thread-local variable" }; 2int main() {  const auto t2_ptr = &t2;  printf("A\n"); 3  Tracer t3{ "Automatic variable" }; 4  printf("B\n");  const auto* t4 = new Tracer{ "Dynamic variable" }; 5  printf("C\n");}

Листинг 4.6 содержит Tracer со статической 1, локальной поточной 2, автоматической 4 и динамической 5 длительностью хранения. Между каждой строкой в main выводится символ A, B или C для ссылки 3.

Запуск программы приводит к результату в листинге 4.7.

Листинг 4.7. Пример вывода из листинга 4.6

Static variable constructed.Thread-local variable constructed.A 3Automatic variable constructed.BDynamic variable constructed.CAutomatic variable destructed.Thread-local variable destructed.Static variable destructed.

Перед первой строкой main 3 статические и потоковые локальные переменные t1 и t2 были инициализированы 1 2. Это можно увидеть в листинге 4.7: обе переменные напечатали свои сообщения инициализации до A. Как и для любой автоматической переменной, область видимости t3 ограничена включающей функцией main. Соответственно t3 создается в месте инициализации сразу после A.

После B вы можете видеть сообщение, соответствующее инициализации t4 5. Обратите внимание, что соответствующее сообщение, генерируемое динамическим деструктором Tracer, отсутствует. Причина в том, что вы (намеренно) потеряли память для объекта, на который указывает t4. Поскольку команды delete t4 не было, деструктор никогда не будет вызван.

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

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

Исключения


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

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

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

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

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

Большинство объектов являются бросаемыми. Однако рекомендуется использовать одно из исключений, доступных в stdlib, например std::runtime_error в заголовке <stdеxcept>. Конструктор runtime_error принимает const char* с нулевым символом в конце, описывающий природу состояния ошибки. Это сообщение можно получить с помощью метода what, который не принимает параметров.

Класс Groucho в листинге 4.8 создает исключение всякий раз при вызове метода forget с аргументом, равным 0xFACE.

Листинг 4.8. Класс Groucho

#include <stdexcept>#include <cstdio>struct Groucho {   void forget(int x) {      if (x == 0xFACE) {         throw1 std::runtime_error2{ "I'd be glad to make an exception." };      }      printf("Forgot 0x%x\n", x);    }};

Чтобы вызвать исключение, в листинге 4.8 используется ключевое слово throw 1, за которым следует объект std::runtime_error 2.

Использование блоков try-catch

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

Листинг 4.9 показывает использование блока try-catch для обработки исключений, генерируемых объектом Groucho.

В методе main создается объект Groucho, а затем устанавливается блок try-catch 1. В части try вызывается метод forget класса groucho с несколькими различными параметрами: 0xC0DE 2, 0xFACE 3 и 0xC0FFEE 4. Внутри части catch обрабатываются любые исключения std::runtime_error 5, выводя сообщение в консоли 6.

Листинг 4.9. Использование try-catch для обработки исключений класса Groucho

#include <stdexcept>#include <cstdio>struct Groucho {      --пропуск--};int main() {   Groucho groucho;   try { 1       groucho.forget(0xC0DE); 2       groucho.forget(0xFACE); 3       groucho.forget(0xC0FFEE); 4    } catch (const std::runtime_error& e5) {       printf("exception caught with message: %s\n", e.what()); 6    }}

При запуске программы в листинге 4.9 вы получите следующий вывод:

Forgot 0xc0deexception caught with message: I'd be glad to make an exception.

При вызове forget с параметром 0xC0DE 2 groucho выводит Forgot0xc0de и завершает выполнение. При вызове forget с параметром 0xFACE 3 groucho выдает исключение. Это исключение остановило нормальное выполнение программы, поэтому forget никогда больше не вызывается 4. Вместо этого исключение в полете перехватывается 5, а его сообщение выводится в консоль 6.

Классы исключений stdlib

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

Стандартные классы исключений

stdlib предоставляет стандартные классы исключений в заголовке <stdеxcept>. Они должны стать вашим первым причалом при программировании исключений. Суперклассом для всех стандартных классов исключений является класс std::exception. Все подклассы в std::exception могут быть разделены на три группы: логические ошибки (logic_error), ошибки выполнения (runtime_error) и ошибки языковой поддержки. Ошибки языковой поддержки обычно не относятся к вам как к программисту, но вы наверняка столкнетесь с логическими ошибками и ошибками выполнения. Рисунок 4.1 обобщает их отношения.

image


КРАТКИЙ КУРС ПО НАСЛЕДОВАНИЮ

Прежде чем вводить исключения stdlib, нужно понять простое наследование классов C++ на очень высоком уровне. Классы могут иметь подклассы, которые наследуют функциональность своих суперклассов. Синтаксис в листинге 4.10 определяет это отношение.

Листинг 4.10. Определение суперклассов и подклассов

struct Superclass {    int x;};struct Subclass : Superclass { 1    int y;    int foo() {      return x + y; 2    }};

В Superclass нет ничего особенного. Но вот объявление Subclass 1 является особенным. Оно определяет отношения наследования с использованием синтаксиса: Superclass. Subclass наследует члены от Superclass, которые не помечены как private. Это можно увидеть в действии, когда Subclass использует поле x 2. Это поле принадлежит Superclass, но поскольку Subclass наследует от Superclass, x доступно.

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

Логические ошибки

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

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

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

  • domain_error сообщает об ошибках, связанных с допустимым диапазоном ввода, особенно для математических функций. Например, квадратный корень поддерживает только неотрицательные числа (в реальном случае). Если передается отрицательный аргумент, функция квадратного корня может выдать domain_error.
  • Исключение invalid_argument сообщает, как правило, о неожиданных параметрах.
  • Исключение length_error сообщает, что какое-либо действие нарушит ограничение максимального размера.
  • Исключение out_of_range сообщает, что некоторое значение не находится в ожидаемом диапазоне. Каноническим примером является индексирование с проверкой границ в структуре данных.

Ошибки выполнения

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

  • system_error сообщает, что операционная система обнаружила некоторую ошибку. Такого рода исключения могут тысячи раз встретиться на вашем пути. Внутри заголовка <system_error> находится большое количество кодов ошибок и их состояний. Когда создается system_error, информация об ошибке упаковывается, чтобы можно было определить природу ошибки. Метод .code() возвращает enumclass типа std::errc, который имеет большое количество значений, таких как bad_file_descriptor, timed_out и license_denied,
  • overflow_error и underflow_error сообщают об арифметическом переполнении и потере значимости соответственно.


Другие ошибки наследуются напрямую от exception. Распространенным является исключение bad_alloc, которое сообщает, что new не удалось выделить необходимую память для динамического хранения.

Ошибки языковой поддержки

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

Обработка исключений

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

Например, следующий обработчик перехватывает любое исключение, которое наследуется от std::exception, включая std::logic_error:

try {   throw std::logic_error{ "It's not about who wrong "                          "it's not about who right" };} catch (std::exception& ex) {   // Обрабатывает std::logic_error. Поскольку он наследуется от std::exception}

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

try {  throw 'z'; // Don't do this.} catch (...) {  // Обрабатывает любое исключение, даже 'z'}

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

Можно обрабатывать различные типы исключений, происходящих из одного и того же блока try, объединяя операторы catch, как показано здесь:

try {  // Код, который может вызвать исключение  --пропуск--} catch (const std::logic_error& ex) {  // Запись исключения и завершение работы программы; найдена программная ошибка!  --пропуск--} catch (const std::runtime_error& ex) {  // Делаем все, что можно  --пропуск--} catch (const std::exception& ex) {  // Обработка любого исключения, наследуемого от std:exception,  // которое не является logic_error или runtime_error.  --пропуск--} catch (...) {  // Паника; было сгенерировано непредвиденное исключение  --пропуск--}

Обычно такой код можно увидеть в точке входа в программу.

ПЕРЕБРАСВАНИЕ ИСКЛЮЧЕНИЯ
В блоке catch можно использовать ключевое слово throw, чтобы возобновить поиск подходящего обработчика исключений. Это называется перебрасыванием исключения. Есть несколько необычных, но важных случаев, когда вы, возможно, захотите дополнительно проверить исключение, прежде чем обработать его, как показано в листинге 4.11.

Листинг 4.11. Перебрасывание ошибки

try {  // Код, который может вызвать system_error  --пропуск--} catch(const std::system_error& ex) {   if(ex.code()!= std::errc::permission_denied){   // Ошибка, не связанная с отказом в доступе     throw; 1}  // Восстановление после ошибки   --пропуск--}

В этом примере код, который может выдать system_error, помещается в блок trycatch.
Все системные ошибки обрабатываются, но, если это не ошибка EACCES (в доступе отказано), исключение перебрасывается 1. У этого подхода есть некоторые потери производительности, и полученный код часто оказывается излишне запутанным.

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

Листинг 4.12. Перехват конкретного исключения, но не перебрасывание


try {  // Генерация исключения PermissionDenied  --пропуск--} catch(const PermissionDenied& ex) {  // Восстановление после ошибки EACCES (отказано в доступе) 1  --пропуск--}

Если генерируется std::system_error, обработчик PermissionDenied 1 не поймает его. (Конечно, обработчик std::system_error все равно можно оставить, чтобы перехватывать такие исключения, если это необходимо.)

Пользовательские исключения

Программист может при необходимости определить свои собственные исключения; обычно эти пользовательские исключения наследуются от std::exception. Все классы из stdlib используют исключения, которые происходят от std::exception. Это позволяет легко перехватывать все исключения, будь то из вашего кода или из stdlib, с помощью одного блока catch.

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

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

bool is_odd(int x) noexcept {  return 1 == (x % 2);}

Функции с пометкой noexcept составляют жесткий контракт. При использовании функции, помеченной как noexcept, вы можете быть уверены, что функция не может вызвать исключение. В обмен на это вы должны быть предельно осторожны, когда помечаете собственную функцию как noexcept, так как компилятор не может это проверить. Если код выдает исключение внутри функции, помеченной как noexcept, это плохо. Среда выполнения C++ вызовет функцию std::terminate, которая по умолчанию завершит работу программы через abort. После такого программа не может быть восстановлена:

void hari_kari() noexcept {   throw std::runtime_error{ "Goodbye, cruel world." };}

Пометка функции ключевым словом noexcept позволяет оптимизировать код, полагаясь на то, что функция не может вызвать исключение. По сути, компилятор освобождается для использования семантики переноса, что может быть выполнено быстрее (подробнее об этом в разделе Семантика перемещения, с. 184).
ПРИМЕЧАНИЕ
Ознакомьтесь с правилом 14 Эффективного использования C++ Скотта Мейерса, чтобы подробно обсудить noexcept. Суть в том, что некоторые конструкторы переноса и операторы присваивания переноса могут выдавать исключение, например если им нужно выделить память, а система не работает. Если конструктор переноса или оператор присваивания переноса не указывает иное, компилятор должен предполагать, что перенос может вызвать исключение. Это отключает определенные оптимизации.

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

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

image

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

Стеки вызовов и обработка исключений

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

Выбрасывание исключений из деструктора

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

Допустим, генерируется исключение и во время размотки стека другое исключение генерируется деструктором во время обычной очистки. Теперь у вас есть два исключения в состоянии полета. Как среда выполнения C++ должна справляться с такой ситуацией?

У вас может быть свое мнение на этот счет, но среда выполнения вызовет функцию terminate (завершение). Рассмотрим листинг 4.13, который показывает, что может произойти при выбрасывании исключений из деструктора:

Листинг 4.13. Программа, где показана опасность создания исключения в деструкторе

#include <cstdio>#include <stdexcept>struct CyberdyneSeries800 {  CyberdyneSeries800() {   printf("I'm a friend of Sarah Connor."); 1  }  ~CyberdyneSeries800() {    throw std::runtime_error{ "I'll be back." }; 2}};  int main() {    try {      CyberdyneSeries800 t800; 3      thro std::runtime_error{ "Come with me if you want to live." }; 4    } catch(const std::exception& e) { 5      printf("Caught exception: %s\n", e.what()); 6    }}----------------------------------------------------------------------I'm a friend of Sarah Connor. 

ПРИМЕЧАНИЕ
Листинг 4.13 вызывает std::terminate, поэтому в зависимости от операционной среды может быть показано всплывающее окно с уведомлением.

Во-первых, был объявлен класс CyberdyneSeries800, который имеет простой конструктор, который выводит сообщение 1, и воинственный деструктор, который генерирует необработанное исключение 2. В main определяется блок try, в котором инициализируется CyberdyneSeries800 под именем t800 3, и выбрасывается runtime_error 4. В лучшем случае блок catch 5 обработает это исключение, выведет его сообщение 6 и все выйдет изящно. Поскольку t800 это автоматическая переменная в блоке try, она разрушается во время обычного процесса поиска обработчика для исключения, которое было выброшено 4. А поскольку t800 создает исключение в своем деструкторе 2, программа вызывает std::terminate и внезапно завершается.

Как правило, обращайтесь с деструкторами так, как если бы они были noexcept.

Класс SimpleString


Используя расширенный пример, давайте рассмотрим, как конструкторы, деструкторы, члены и исключения объединяются. Класс SimpleString в листинге 4.14 позволяет добавлять строки в стиле C и выводить результат.

Листинг 4.14. Конструктор и деструктор класса SimpleString

#include <stdexcept>struct SimpleString {  SimpleString(size_t max_size) 1    : max_size{ max_size }, 2      length{} { 3    if (max_size == 0) {      throw std::runtime_error{ "Max size must be at least 1." }; 4    }    buffer = new char[max_size]; 5    buffer[0] = 0; 6    }   ~SimpleString() {     delete[] buffer; 7    }--пропуск--private:    size_t max_size;    char* buffer;    size_t length;};

Конструктор 1 принимает один параметр max_size. Это максимальная длина строки, которая включает символ завершения строки. Инициализатор члена 2 сохраняет эту длину в переменной-члене max_size. Это значение также используется в выражении new массива для выделения буфера для хранения данной строки 5. Полученный указатель сохраняется в buffer. Длина инициализируется нулем 3, и это гарантирует, что по крайней мере буфер будет достаточного размера для хранения нулевого байта 4. Поскольку строка изначально пуста, первый байт буфера заполняется нулем 6.
ПРИМЕЧАНИЕ
Поскольку max_size это size_t, он не имеет знака и не может быть отрицательным, поэтому не нужно проверять это фиктивное условие.

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

Этот шаблон называется получение ресурса есть инициализация (RAII), или получение конструктора освобождение деструктора (CADRe).
ПРИМЕЧАНИЕ
Класс SimpleString все еще имеет неявно определенный конструктор копирования. Несмотря на то что память не может быть потеряна, при копировании класс потенциально освободится вдвое. Вы узнаете о конструкторах копирования в разделеСемантике копирования, с. 176. Просто знайте, что листинг 4.14 это обучающий инструмент, а не рабочий код.

Добавление и вывод

Класс SimpleString пока не очень полезен. В листинг 4.15 добавлена возможность выводить строку и добавлять набор символов в конец строки.

Листинг 4.15. Методы print и append_line для SimpleString

#include <cstdio>#include <cstring>#include <stdexcept>struct SimpleString {  --пропуск--  void print(const char* tag) const { 1    printf("%s: %s", tag, buffer);  }  bool append_line(const char* x) { 2  const auto x_len = strlen3(x);  if (x_len + length + 2 > max_size) return false; 4  std::strncpy(buffer + length, x, max_size - length);  length += x_len;  buffer[length++] = '\n';  buffer[length] = 0;  return true; } --пропуск--};


Первый метод print 1 выводит строку. Для удобства можно предоставить строку tag, чтобы можно было сопоставить вызов print с результатом. Этот метод является постоянным, потому что нет необходимости изменять состояние SimpleString.

Метод append_line 2 принимает строку с нулем в конце и добавляет ее содержимое плюс символ новой строки в buffer. Он возвращает true, если был успешно добавлен, и false, если не было достаточно места. Во-первых, append_line должен определить длину x. Для этого используется функция strlen 3 из заголовка <сstring>, которая принимает строку с нулевым символом в конце и возвращает ее длину:

size_t strlen(const char* str);

strlen используется для вычисления длины x и инициализации x_len с результатом. Этот результат используется для вычисления того, приведет ли добавление x (символов новой строки) и нулевого байта к текущей строке к получению строки с длиной, превышающей max_size 4. Если это так, append_line возвращает false.

Если для добавления x достаточно места, необходимо скопировать его байты в правильное место в buffer. Функция std::strncpy 5 из заголовка <сstring> является одним из подходящих инструментов для этой работы. Она принимает три параметра: адрес назначения, адрес источника и количество символов для копирования:

char* std::strncpy(char* destination, const char* source, std::size_t num);

Функция strncpy будет копировать до num байтов из source в destination. После завершения она вернет значение destination (которое будет отброшено).

После добавления количества байтов x_len, скопированных в buffer, к length работа завершается добавлением символа новой строки \n и нулевого байта в конец buffer. Функция возвращает true, чтобы указать, что введенный х был успешно добавлен в виде строки в конец буфера.
ПРЕДУПРЕЖДЕНИЕ
Используйте strncpy очень осторожно. Слишком легко забыть символ конца строки в исходной строке или не выделить достаточно места в целевой строке. Обе ошибки приведут к неопределенному поведению. Мы рассмотрим более безопасную альтернативу во второй части книги.

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

Листинг 4.16. Методы SimpleString

#include <cstdio>#include <cstring>#include <exception>struct SimpleString {   --пропуск--}int main() {   SimpleString string{ 115 }; 1   string.append_line("Starbuck, whaddya hear?");   string.append_line("Nothin' but the rain."); 2   string.print("A"); 3   string.append_line("Grab your gun and bring the cat in.");   string.append_line("Aye-aye sir, coming home."); 4   string.print("B"); 5   if (!string.append_line("Galactica!")) { 6      printf("String was not big enough to append another message."); 7   }}

Сначала создается SimpleString с max_length=115 1. Метод append_line используется дважды 2, чтобы добавить некоторые данные в строку, а затем вывести содержимое вместе с тегом A 3. Затем добавляется больше текста 4 и снова выводится содержимое, на этот раз с тегом B 5. Когда append_line определяет, что SimpleString исчерпал свободное пространство 6, возвращается false 7. (Вы как пользователь SimpleString несете ответственность за проверку этого условия.)

Листинг 4.17 содержит выходные данные запуска этой программы.

Листинг 4.17. Результат выполнения программы в листинге 4.16

A: Starbuck, whaddya hear? 1Nothin' but the rain.B: Starbuck, whaddya hear? 2Nothin' but the rain.Grab your gun and bring the cat in.Aye-aye sir, coming home.String was not big enough to append another message. 3


Как и ожидалось, строка содержит Starbuck, whaddya hear?\nNothin' but the rain.\nвA 1. (Вспомните из главы 2, что \n это специальный символ новой строки.) После добавления Grab your gun and bring the cat in. и Aye-aye sir, coming home. вы получите ожидаемый результат в B 2.

Когда листинг 4.17 пытается добавить Galactica! в string, append_line возвращает false, поскольку в buffer недостаточно места. Это вызывает вывод сообщения String was not big enough to append another message 3.

Составление SimpleString

Рассмотрим, что происходит при определении класса с членом SimpleString, как показано в листинге 4.18.

Как предполагает инициализатор члена 1, string полностью построена, и ее инварианты класса назначаются после выполнения конструктора SimpleStringOwner. Здесь демонстрируется порядок членов объекта во время создания: члены создаются перед вызовом конструктора окружающего объекта. Смысл есть, а иначе как можно установить инварианты класса без знаний об инвариантах его членов?

Листинг 4.18. Реализация SimpleStringOwner

#include <stdexcept>struct SimpleStringOwner {   SimpleStringOwner(const char* x)     : string{ 10 } { 1     if (!string.append_line(x)) {       throw std::runtime_error{ "Not enough memory!" };    }    string.print("Constructed");  }  ~SimpleStringOwner() {    string.print("About to destroy"); 2  }private:  SimpleString string;};

Деструкторы работают в обратном порядке. Внутри ~SimpleStringOwner() 2 нужно хранить инварианты класса строки, чтобы можно было напечатать ее содержимое. Все члены уничтожаются после вызова деструктора объекта.


В листинге 4.19 используется SimpleStringOwner.

Листинг 4.19. Программа, содержащая SimpleStringOwner

--пропуск--int main() {   SimpleStringOwner x{ "x" };   printf("x is alive\n");}--------------------------------------------------------------------Constructed: х 1x is aliveAbout to destroy: х 2

Как и ожидалось, член string в x 1 создается надлежащим образом, потому что конструкторы членов объекта вызываются перед конструктором объекта, в результате чего появляется сообщение Constructed: x. Как автоматическая переменная x уничтожается непосредственно перед выходом из main, и вы получаете сообщение About to destroy: x 2. Член string все еще доступен в этот момент, потому что деструкторы членов вызываются после деструктора вмещающего объекта.

Размотка стека вызовов

Листинг 4.20 демонстрирует, как обработка исключений и размотка стека работают вместе. Блок try-catch устанавливается в main, после чего выполняется серия вызовов функций. Один из этих вызовов вызывает исключение.

Листинг 4.20. Программа, где используется SimpleStringOwner и размотка стека вызовов

--пропуск--void fn_c() {   SimpleStringOwner c{ "cccccccccc" }; 1}void fn_b() {  SimpleStringOwner b{ "b" };  fn_c(); 2}int main() {  try { 3   SimpleStringOwner a{ "a" };   fn_b(); 4   SimpleStringOwner d{ "d" }; 5 } catch(const std::exception& e) { 6  printf("Exception: %s\n", e.what()); }}

В листинге 4.21 показаны результаты запуска программы из листинга 4.20.

Листинг 4.21. Результат запуска программы из листинга 4.20

Constructed: aConstructed: bAbout to destroy: bAbout to destroy: aException: Not enough memory!

Вы установили блок try-catch 3. Первый экземпляр SimpleStringOwner, a, создается без инцидентов, и в консоль выводится сообщение Constructed: а. Далее вызывается fn_b 4. Обратите внимание, что вы все еще находитесь в блоке try-catch, поэтому любое выброшенное исключение будет обработано. Внутри fn_b другой экземпляр SimpleStringOwner, b, успешно создается, и Constructed: b выводится на консоль. Затем происходит вызов еще одной функции, fn_c 2.

Давайте на минуту остановимся, чтобы разобраться, как выглядит стек вызовов, какие объекты живы и как выглядит ситуация обработки исключений. Сейчас у нас есть два живых и действительных объекта SimpleStringOwner: a и b. Стек вызовов выглядит как main() fn_ () fn_c(), и в main настроен обработчик исключений для обработки любых исключений. Эта ситуация показана на рис. 4.3.

В 1 возникает небольшая проблема. Напомним, что SimpleStringOwner имеет член SimpleString, который всегда инициализируется с max_size 10. При попытке создания c конструктор SimpleStringOwner выдает исключение, потому что вы пытались добавить cccccccccc, который имеет длину 10, что выходит за рамки, потому что нужно еще добавить символы новой строки и завершения строки.

Теперь в полете находится одно исключение. Стек будет раскручиваться до тех пор, пока не будет найден соответствующий обработчик, и все объекты, выпадающие из области видимости в результате этого раскручивания, будут уничтожены. Обработчик доходит до стека 6, поэтому fn_c и fn_b разматываются. Поскольку SimpleStringOwner b это автоматическая переменная в fn_b, она разрушается и в консоль выводится сообщение About to destroy: b. После fn_b автоматические переменные внутри try {} уничтожаются. Это включает в себя SimpleStringOwner a, поэтому в консоль выводится About to destroy: a.

image

Как только исключение происходит в блоке try{}, дальнейшие операторы не выполняются. В результате d никогда не инициализируется 5 и конструктор d не вызывается и не выводится в консоль. После размотки стека вызовов выполнение сразу переходит к блоку catch. В итоге в консоль выводится сообщение Exception: Not enough memory! 6.

Исключения и производительность

Обработка исключений обязательна в программах; ошибки неизбежны. При правильном использовании исключений ошибок не возникает, код работает быстрее, чем код, проверенный вручную. Если ошибка все-таки есть, обработка исключений может иногда выполняться медленнее, но у этого есть огромные преимущества в надежности и удобстве обслуживания по сравнению с альтернативными вариантами. Курт Гантерот, автор Оптимизации программ на C++, хорошо пишет об этом: Использование обработки исключений приводит к программам, которые работают быстрее при нормальном выполнении и ведут себя лучше в случае неудачи. Когда программа на C++ выполняется нормально (без исключений), при проверке исключений не возникает никаких издержек во время выполнения. Вы платите только за исключение.

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

Альтернативы для исключений

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

struct HumptyDumpty {   HumptyDumpty();   bool is_together_again();  --пропуск--};

В идиоматическом C++ вы бы просто сгенерировали исключение в конструкторе, но здесь следует помнить о проверке и обработке ситуации как условии ошибки в вызывающем коде:

bool send_kings_horses_and_men() {  HumptyDumpty hd{};  if (hd.is_together_again()) return false;  // Использование инвариантов класса hd гарантировано.  // HumptyDumpty с треском проваливается.  --пропуск--  return true;}

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

Листинг 4.22. Фрагмент кода с объявлением структурированной привязки

struct Result { 1   HumptyDumpty hd;   bool success;   };  Result make_humpty() { 2    HumptyDumpty hd{};    bool is_valid;    // Проверка правильности hd и установка соответствующего значения is_valid    return { hd, is_valid };   }bool send_kings_horses_and_men() {   auto [hd, success] = make_humpty();    if(!success) return false;   // Установка инвариантов класса   --пропуск--   return true;}

Сначала объявляется POD, который содержит HumptyDumpty и флаг success 1. Затем определяется функция make_humpty 2, которая создает и проверяет HumptyDumpty. Такие методы называются фабричными, поскольку их целью является инициализация объектов. Функция make_humpty оборачивает его и флаг success в Result при возврате. Синтаксис в точке вызова 3 показывает, как можно распаковать Result, получив несколько переменных с определением типа при помощи auto.
ПРИМЕЧАНИЕ
Более подробное описание структурированных привязок приведено в подразделе Структурированные привязки, с. 289.


Об авторе

Джош Лоспинозо (Josh Lospinoso) доктор философии и предприниматель, прослуживший 15 лет в армии США. Джош офицер, занимающийся вопросами кибербезопасности. Написал десятки программ для средств информационной безопасности и преподавал C++ начинающим разработчикам. Выступает на различных конференциях, является автором более 20 рецензируемых статей и стипендиатом Родса, а также имеет патент. В 2012 году стал соучредителем успешной охранной компании. Джош ведет блог и активно участвует в разработке ПО с открытым исходным кодом.

О научном редакторе

Кайл Уиллмон (Kyle Willmon) разработчик информационных систем с 12-летним опытом в C++. В течение 7 лет работал в сообществе по информационной безопасности, используя C++, Python и Go в различных проектах. В настоящее время является разработчиком в команде Sony Global Threat Emulation.

Более подробно с книгой можно ознакомиться на сайте издательства
Оглавление
Отрывок

Для Хаброжителей скидка 25% по купону C++

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Подробнее..

Книга Современный скрапинг веб-сайтов с помощью Python. 2-е межд. издание

12.04.2021 16:18:43 | Автор: admin
image Привет, Хаброжители! Если программирование напоминает волшебство, то веб-скрапинг это очень сильное колдунство. Написав простую автоматизированную программу, можно отправлять запросы на веб-серверы, запрашивать с них данные, а затем анализировать их и извлекать необходимую информацию. Новое расширенное издание книги знакомит не только с веб-скрапингом, но и поможет собрать любого вида данные в современном Интернете. В части I основное внимание уделено механике веб-скрапинга: как с помощью Python запрашивать информацию с веб-сервера, производить базовую обработку серверного отклика и организовать автоматизированное взаимодействие с сайтами. В части II исследованы более специфичные инструменты и приложения, которые пригодятся при любом сценарии веб-скрапинга. Разбирайте сложные HTML-страницы. Разрабатывайте поисковые роботы с помощью фреймворка Scrapy. Изучайте методы хранения данных, полученных с помощью скрапинга. Считывайте и извлекайте данные из документов. Очищайте и нормализуйте плохо отформатированные данные. Читайте и пишите информацию на естественных языках. Освойте поиск по формам и логинам. Изучите скрапинг JavaScript и работу с API. Используйте и пишите программы для преобразования изображений в текст. Учитесь обходить скрапинговые ловушки и блокаторы ботов. Протестируйте собственный сайт с помощью скрапинга.

Веб-краулинг с помощью API


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

По мере распространения методов генерации и загрузки контента с помощью JavaScript и Ajax описанная выше ситуация становится все менее привычной. В главе 11 мы рассмотрели один из способов решения указанной проблемы: использование Selenium для автоматизации браузера и извлечения данных. Это легко сделать. Это работает почти всегда.

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

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

Краткое введение в API

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

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

Веб-API чаще всего используются разработчиками для взаимодействия с широко разрекламированными и хорошо документированными открытыми сервисами. Например, американский кабельный спортивный телевизионный канал ESPN предоставляет API (http://personeltest.ru/away/www.espn.com/apis/devcenter/docs/) для получения информации о спортсменах, счетах в играх и др. У Google в разделе для разработчиков (http://personeltest.ru/aways/console.developers.google.com) есть десятки API для языковых переводов, аналитики и геолокации.

В документации всех этих API обычно описываются маршруты или конечные точки в виде URL, которые можно запрашивать, с изменяемыми параметрами, либо входящими в состав этих URL, либо выступающими в роли GET-параметров.

Например, в следующем URL pathparam является параметром пути:

example.com/the-api-route/pathparam

А здесь pathparam является значением параметра param1:

example.com/the-api-route?param1=pathparam

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

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

Вот пример ответа на API-запрос в формате JSON:

{"user":{"id": 123, "name": "Ryan Mitchell", "city": "Boston"}}


А вот ответ на API-запрос в формате XML:

<user><id>123</id><name>Ryan Mitchell</name><city>Boston</city></user>


Сайт ip-api.com (http://personeltest.ru/away/ip-api.com/) имеет понятный и удобный API, который преобразует IP-адреса в реальные физические адреса. Вы можете попробовать выполнить простой запрос API, введя в браузере следующее

ip-api.com/json/50.78.253.58

В результате вы получите примерно такой ответ:

{"ip":"50.78.253.58","country_code":"US","country_name":"United States","region_code":"MA","region_name":"Massachusetts","city":"Boston","zip_code":"02116","time_zone":"America/New_York","latitude":42.3496,"longitude":-71.0746,"metro_code":506}

Обратите внимание: в запросе есть параметр пути json. Чтобы получить ответ в формате XML или CSV, нужно заменить его на соответствующий формат:

ip-api.com/xml/50.78.253.58
ip-api.com/csv/50.78.253.58


API и HTTP-методы

В предыдущем разделе мы рассмотрели API, отправляющие на сервер GET-запрос для получения информации. Существует четыре основных способа (или метода) запроса информации с веб-сервера через HTTP:

GET;
POST;
PUT;
DELETE.

Технически типов запросов больше четырех (например, еще есть HEAD, OPTIONS и CONNECT), но они редко используются в API и маловероятно, что когда-либо встретятся вам. Подавляющее большинство API ограничиваются этими четырьмя методами, а иногда даже какой-то их частью. Постоянно встречаются API, которые используют только GET или только GET и POST.

GET тот запрос, который вы используете, когда посещаете сайт, введя его адрес в адресной строке браузера. Обращаясь по адресу ip-api.com/json/50.78.253.58, вы применяете именно метод GET. Такой запрос можно представить как команду: Эй, веб-сервер, будь добр, выдай мне эту информацию.

Запрос GET по определению не вносит изменений в содержимое базы данных сервера. Ничего не сохраняется и ничего не изменяется. Информация только считывается.

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

Запрос PUT при взаимодействии с сайтами используется реже, но время от времени встречается в API. Этот запрос применяется для изменения объекта или информации. Например, в API можно задействовать запрос POST для создания пользователя и запрос PUT для изменения его адреса электронной почты.

Запросы DELETE, как нетрудно догадаться, служит для удаления объекта. Например, если отправить запрос DELETE по адресу myapi.com/user/23, то будет удален пользователь с идентификатором 23. Методы DELETE нечасто встречаются в открытых API, поскольку те в основном создаются для распространения информации или чтобы позволить пользователям создавать или публиковать информацию, но не удалять ее из баз данных.

В отличие от GET запросы POST, PUT и DELETE позволяют передавать информацию в теле запроса, в дополнение к URL или маршруту, с которого запрашиваются данные.
Как и ответ, получаемый от веб-сервера, эти данные в теле запроса обычно представляются в формате JSON или реже в формате XML. Конкретный формат данных определяется синтаксисом API. Например, при использовании API, который добавляет комментарии к сообщениям в блоге, можно создать следующий PUT-запрос:

example.com/comments?post=123

с таким телом запроса:

{"title": "Great post about APIs!", "body": "Very informative. Really helped me out with a tricky technical challenge I was facing. Thanks for taking the time to write such a detailed blog post about PUT requests!", "author": {"name": "Ryan Mitchell", "website": "http://pythonscraping.com", "company": "O'Reilly Media"}}


Обратите внимание: идентификатор сообщения в блоге (123) передается в качестве параметра в URL, а контент создаваемого нами комментария в теле запроса. Параметры и данные могут передаваться и в параметре, и в теле запроса. Какие параметры обязательны и где передаются опять-таки определяется синтаксисом API.

Подробнее об ответах на API-запросы

Как мы видели в примере с сайтом ip-api.com в начале данной главы, важной особенностью API является то, что эти интерфейсы возвращают хорошо отформатированные ответы. Наиболее распространенные форматы ответов XML (eXtensible Markup Language расширяемый язык разметки) и JSON (JavaScript Object Notation нотация объектов JavaScript).

В последние годы JSON стал намного популярнее, чем XML, по нескольким основным причинам. Во-первых, файлы JSON обычно меньше, чем хорошо проработанные файлы XML. Сравните, например, следующие данные в формате XML, занимающие 98 символов:

<user><firstname>Ryan</firstname><lastname>Mitchell</lastname><username>Kludgist</username></user>

А теперь посмотрите на те же данные в формате JSON:

{"user":{"firstname":"Ryan","lastname":"Mitchell","username":"Kludgist"}}


Это всего 73 символа, на целых 36 % меньше, чем те же данные в формате XML.
Конечно, вероятен аргумент, что XML можно отформатировать так:

<user firstname="ryan" lastname="mitchell" username="Kludgist"></user>


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

Другая причина, по которой JSON так быстро становится более популярным, чем XML, связана с изменением веб-технологий. Раньше получателями API были по большей части серверные скрипты на PHP или .NET. Сейчас вполне может оказаться, что получать и отправлять вызовы API будет фреймворк наподобие Angular или Backbone. Серверным технологиям до определенной степени безразлично, в какой форме к ним поступают данные. Однако библиотекам JavaScript, таким как Backbone, проще обрабатывать JSON.

Принято считать, что API возвращают ответ либо в формате XML, либо в формате JSON, однако возможен любой другой вариант. Тип ответа API ограничен только воображением программиста, создавшего этот интерфейс. Еще один типичный формат ответа CSV (как видно из примера с ip-api.com). Отдельные API даже позволяют создавать файлы. Можно отправить на сервер запрос, по которому будет сгенерировано изображение с наложенным на него заданным текстом, или же запросить определенный файл XLSX или PDF.

Некоторые API вообще не возвращают ответа. Например, если отправить на сервер запрос для создания комментария к записи в блоге, то он может вернуть только HTTP-код ответа 200, что означает: Я опубликовал комментарий; все в порядке! Другие запросы могут возвращать минимальный ответ наподобие такого:

{"success": true}


В случае ошибки вы можете получить такой ответ:

{"error": {"message": "Something super bad happened"}}


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

Об авторе

Райан Митчелл (Ryan Mitchell) старший инженер-программист в бостонской компании HedgeServ, в которой она разрабатывает API и инструменты для анализа данных. Райан окончила Инженерно-технический колледж им. Франклина В. Олина, имеет степень магистра в области разработки программного обеспечения и сертификат по анализу и обработке данных, полученный на курсах повышения квалификации при Гарвардском университете. До прихода в HedgeServ Райан трудилась в компании Abine, где разрабатывала веб-скраперы и средства автоматизации на Python. Регулярно выступает консультантом проектов по веб-скрапингу для розничной торговли, сферы финансов и фармацевтики. По совместительству работает консультантом и внештатным преподавателем в Северо-Восточном университете и Инженерно-техническом колледже им. Франклина В. Олина.

Более подробно с книгой можно ознакомиться на сайте издательства
Оглавление
Отрывок

Для Хаброжителей скидка 25% по купону Python

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Подробнее..

Книга C 8 и .NET Core. Разработка и оптимизация

15.04.2021 12:10:33 | Автор: admin
image Привет, Хаброжители! В издании рассмотрены все темы, связанные с разработкой на C#. В начале книги вы ознакомитесь с основами C#, в том числе с объектно-ориентированным программированием, а также с новыми возможностями C# 8.0. Несколько глав посвящено .NET Standard API, применяемым для запроса данных и управления ими, отслеживания производительности и ее повышения, работы с файловой системой, асинхронными потоками, сериализацией и шифрованием. Кроме того, на примерах кроссплатформенных приложений вы сможете собрать и развернуть собственные. Например, веб-приложения с использованием ASP.NET Core или мобильные приложения на Xamarin Forms.

Также вы познакомитесь с технологиями, применяемыми при создании приложений Windows для ПК, в частности с Windows Forms, Windows Presentation Foundation (WPF) и Universal Windows Platform (UWP).

Улучшение производительности и масштабируемости с помощью многозадачности


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

В этой главе:

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

Процессы, потоки и задачи


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

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

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

Потоки имеют свойства Priority и ThreadState. Кроме того, существует класс ThreadPool, предназначенный для управления пулом фоновых рабочих потоков, как показано на следующей схеме (рис. 13.1).

image

Если вы как разработчик имеете дело со сложными действиями, которые должны быть выполнены вашим кодом, и хотите получить полный контроль над ними, то можете создавать отдельные экземпляры класса Thread и управлять ими. При наличии одного основного потока и нескольких небольших действий, которые можно выполнять в фоновом режиме, вы можете добавить экземпляры делегатов, указывающие на эти фрагменты, реализованные в виде методов в очередь, и они будут автоматически распределены по потокам с помощью пула потоков.
Дополнительную информацию о пуле потоков можно получить на сайте docs.microsoft.com/ru-ru/dotnet/standard/threading/the-managed-thread-pool.

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

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

image

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

Мониторинг производительности и использования ресурсов


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

Оценка эффективности типов


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

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

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

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

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

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

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

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

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

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

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


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

1. Создайте в папке Code папку Chapter13 с двумя подпапками MonitoringLib и MonitoringApp.

2. В программе Visual Studio Code сохраните рабочую область как Chapter13.code-workspace.

3. Добавьте в рабочую область папку MonitoringLib, откройте для нее новую панель TERMINAL (Терминал) и создайте новый проект библиотеки классов, как показано в следующей команде:

dotnet new classlib

4. Добавьте в рабочую область папку MonitoringApp, откройте для нее новую панель TERMINAL (Терминал) и создайте новый проект консольного приложения, как показано в следующей команде:

dotnet new console

5. В проекте MonitoringLib переименуйте файл Class1.cs на Recorder.cs.

6. В проекте MonitoringApp найдите и откройте файл MonitoringApp.csproj и добавьте ссылку на библиотеку MonitoringLib, как показано ниже (выделено полужирным шрифтом):

<Project Sdk="Microsoft.NET.Sdk">   <PropertyGroup>      <OutputType>Exe</OutputType>      <TargetFramework>netcoreapp3.0</TargetFramework>   </PropertyGroup>   <ItemGroup>      <ProjectReference          Include="..\MonitoringLib\MonitoringLib.csproj" />      </ItemGroup></Project>

7. На панели TERMINAL (Терминал) скомпилируйте проекты, как показано в следующей команде:

dotnet build

Реализация класса Recorder


Тип Stopwatch содержит несколько полезных членов, как показано в табл. 13.1.

image

Тип Process содержит несколько полезных членов, перечисленных в табл. 13.2.

image

Для реализации класса Recorder мы будем использовать классы Stopwatch и Process.

1. Откройте файл Recorder.cs и измените его содержимое, чтобы задействовать экземпляр класса Stopwatch в целях записи времени и текущий экземпляр класса Process для записи использованной памяти, как показано ниже:

using System;using System.Diagnostics;using static System.Console;using static System.Diagnostics.Process;namespace Packt.Shared{   public static class Recorder{   static Stopwatch timer = new Stopwatch();   static long bytesPhysicalBefore = 0;   static long bytesVirtualBefore = 0;   public static void Start()   {      // очистка памяти, на которую больше нет ссылок,      // но которая еще не освобождена     GC.Collect();     GC.WaitForPendingFinalizers();     GC.Collect();     // сохранение текущего использования физической     // и виртуальной памяти     bytesPhysicalBefore = GetCurrentProcess().WorkingSet64;     bytesVirtualBefore = GetCurrentProcess().VirtualMemorySize64;     timer.Restart();   }   public static void Stop()   {     timer.Stop();     long bytesPhysicalAfter = GetCurrentProcess().WorkingSet64;     long bytesVirtualAfter =      GetCurrentProcess().VirtualMemorySize64;    WriteLine("{0:N0} physical bytes used.",       bytesPhysicalAfter - bytesPhysicalBefore);    WriteLine("{0:N0} virtual bytes used.",      bytesVirtualAfter - bytesVirtualBefore);    WriteLine("{0} time span ellapsed.", timer.Elapsed);     WriteLine("{0:N0} total milliseconds ellapsed.",       timer.ElapsedMilliseconds);   } }}

В методе Start класса Recorder используется сборщик мусора (garbage collector, класс GC), позволяющий нам гарантировать, что вся выделенная в настоящий момент память будет собрана до записи количества использованной памяти. Это сложная техника, и ее стоит избегать при разработке прикладной программы.

2. В классе Program в метод Main добавьте операторы для запуска и остановки класса Recorder при генерации массива из 10 000 целых чисел, как показано ниже:

using System.Linq;using Packt.Shared;using static System.Console;namespace MonitoringApp{    class Program    {      static void Main(string[] args)      {        WriteLine("Processing. Please wait...");        Recorder.Start();        // моделирование процесса, требующего ресурсов памяти...        int[] largeArrayOfInts =           Enumerable.Range(1, 10_000).ToArray();        // ...и занимает некоторое время, чтобы завершить        System.Threading.Thread.Sleep(           new Random().Next(5, 10) * 1000);        Recorder.Stop();      }   }}

3. Запустите консольное приложение и проанализируйте результат:

Processing. Please wait...655,360 physical bytes used.536,576 virtual bytes used.00:00:09.0038702 time span ellapsed.9,003 total milliseconds ellapsed

Измерение эффективности обработки строк

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

1. Закомментируйте предыдущий код в методе Main, обернув его символами /* и */.

2. Добавьте в метод Main следующий код. Он создает массив из 50 000 переменных int, а затем конкатенирует их, используя в качестве разделителей запятые, с помощью классов string и StringBuilder:

int[] numbers = Enumerable.Range(1, 50_000).ToArray();Recorder.Start();WriteLine("Using string with +");string s = "";for (int i = 0; i < numbers.Length; i++){   s += numbers[i] + ", ";}Recorder.Stop();Recorder.Start();WriteLine("Using StringBuilder");var builder = new System.Text.StringBuilder();for (int i = 0; i < numbers.Length; i++){   builder.Append(numbers[i]); builder.Append(", ");}Recorder.Stop();

3. Запустите консольное приложение и проанализируйте результат:

Using string with +11,231,232 physical bytes used.29,843,456 virtual bytes used.00:00:02.6908216 time span ellapsed.2,690 total milliseconds ellapsed.Using StringBuilder4,096 physical bytes used.0 virtual bytes used.00:00:00.0023091 time span ellapsed.2 total milliseconds ellapsed.

Исходя из результатов, мы можем сделать следующие выводы:

  • класс string вместе с оператором + использовал около 11 Мбайт физической памяти, 29 Мбайт виртуальной и занял по времени 2,7 с;
  • класс StringBuilder использовал 4 Кбайт физической памяти, 0 виртуальной и занял менее 2 мс.

В нашем случае при конкатенации текста класс StringBuilder выполняется примерно в 1000 раз быстрее и приблизительно в 10 000 раз эффективнее по затратам ресурсов памяти!
Избегайте использования метода String.Concat и оператора + внутри цикла. Вместо этого для конкатенации переменных, особенно в циклах, применяйте класс StringBuilder.

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

Об авторе


imageМарк Дж. Прайс обладатель сертификатов Microsoft Certified Trainer (MCT), Microsoft Specialist: Programming in C# и Microsoft Specialist: Architecting Microsoft Azure Infrastructure Solutions. За его плечами более 20 лет практики в области обучения и программирования.

С 1993 года Марк сдал свыше 80 экзаменов корпорации Microsoft по программированию и специализируется на подготовке других людей к успешному прохождению тестирования. Его студенты как 16-летние новички, так и профессионалы с многолетним опытом. Марк ведет эффективные тренинги, делясь реальным опытом консультирования и разработки систем для корпораций по всему миру.

В период с 2001 по 2003 год Марк посвящал все свое время разработке официального обучающего программного обеспечения в штаб-квартире Microsoft в американском городе Редмонд. В составе команды он написал первый обучающий курс по C#, когда была только выпущена ранняя альфа-версия языка. Во время сотрудничества с Microsoft он преподавал на курсах повышения квалификации сертифицированных корпорацией специалистов, читая лекции по C# и .NET.

В настоящее время Марк разрабатывает и поддерживает обучающие курсы для системы Digital Experience Platform компании Episerver, лучшей .NET CMS в сфере цифрового маркетинга и электронной коммерции.

В 2010 году Марк получил свидетельство об окончании последипломной программы обучения, дающее право на преподавание. Он преподает старшеклассникам математику в двух средних школах в Лондоне. Кроме того, Марк получил сертификат Computer Science BSc. Hons. Degree в Бристольском университете (Англия).

О научном редакторе


Дамир Арх профессионал с многолетним опытом разработки и сопровождения различного программного обеспечения: от сложных корпоративных программных проектов до современных потребительских мобильных приложений. Он работал со многими языками, однако его любимым остается C#. Стремясь к совершенствованию процессов, Дамир предпочитает разработку, основанную на тестировании, непрерывной интеграции и непрерывном развертывании. Он делится своими знаниями, выступая на конференциях, ведет блоги и пишет статьи. Дамир Арх семь раз подряд получал престижную премию Microsoft MVP за разработку технологий. В свободное время он всегда в движении: любит пеший туризм, геокэшинг, бег и скалолазание.

Более подробно с книгой можно ознакомиться на сайте издательства
Оглавление
Отрывок

Для Хаброжителей скидка 25% по купону .NET

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Подробнее..

Программируем на C 8.0. Атрибуты

18.06.2021 16:08:17 | Автор: admin
image Привет, Хабр! Обращаем ваше внимание на одну новинку (сдана в типографию), доступную уже сейчас для покупки в электронном виде.

Язык C# существует уже около двух десятилетий. Он неуклонно развивался и в плане возможностей, и в плане размера, но основные характеристики Microsoft всегда сохраняла без изменений. Каждая новая возможность должна идеально вписываться в состав предыдущих, улучшая язык, а не превращая его в несвязный набор различных функций.

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

Вашему внимаю предлагаю отрывок из книги.

Атрибуты


В .NET можно аннотировать компоненты, типы и их члены с помощью атрибутов. Назначение атрибута регулировать или изменять поведение платформы, инструмента, компилятора или CLR. Например, в главе 1 я демонстрировал класс, аннотированный атрибутом [TestClass]. Он сообщал инфраструктуре юнит-теста, что класс содержит ряд тестов, которые должны быть выполнены как часть набора тестов.

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

Применение атрибутов

Во избежание необходимости вводить дополнительный набор понятий в систему типов .NET работает с ними как с экземплярами типов .NET. Для использования в качестве атрибута тип должен быть производным от класса System.Attribute, и это его единственная особенность. Чтобы применить атрибут, вы помещаете имя типа в квадратные скобки и, как правило, размещаете непосредственно перед целью атрибута. Листинг 14.1 показывает некоторые атрибуты из среды тестирования Microsoft. Один я применил к классу, чтобы указать, что он содержит тесты, которые я хотел бы запустить. Кроме этого, я применил атрибуты к отдельным методам, сообщая среде тестирования, какие из них представляют собой тесты, а какие содержат код инициализации, который должен выполняться перед каждым тестом.

Листинг 14.1. Атрибуты в классе юнит-теста

using Microsoft.VisualStudio.TestTools.UnitTesting;namespace ImageManagement.Tests{   [TestClass]   public class WhenPropertiesRetrieved   {      private ImageMetadataReader _reader;      [TestInitialize]      public void Initialize()      {         _reader = new ImageMetadataReader(TestFiles.GetImage());      }      [TestMethod]      public void ReportsCameraMaker()      {         Assert.AreEqual(_reader.CameraManufacturer, "Fabrikam");      }      [TestMethod]      public void ReportsCameraModel()      {          Assert.AreEqual(_reader.CameraModel, "Fabrikam F450D");      }   }}

Если вы заглянете в документацию по большинству атрибутов, вы обнаружите, что их настоящие имена оканчиваются на Attribute. Если нет класса с именем, указанным в скобках, компилятор C# попытается добавить Attribute, поэтому атрибут [TestClass] в листинге 14.1 ссылается на класс TestClassAttribute. Если хотите, вы можете записывать имя класса полностью, например [TestClassAttribute], но чаще используют более короткую форму.

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

Некоторые типы атрибутов способны принимать аргументы конструктора. Например, среда тестирования Microsoft содержит атрибут TestCategoryAttribute. При запуске можно выбрать выполнение только тех тестов, которые принадлежат определенной категории. Данный атрибут требует, чтобы вы передали имя категории в качестве аргумента конструктора, потому что нет смысла применять этот атрибут без указания имени. Синтаксис для указания аргументов конструктора атрибута вполне ожидаемый (листинг 14.2).

Листинг 14.2. Атрибут с аргументом конструктора

[TestCategory("Property Handling")][TestMethod]public void ReportsCameraMaker(){...

Вы также можете указать свойства или значения полей. Характеристиками некоторых атрибутов можно управлять только через свойства или поля, но не через аргументы конструктора. (Если атрибут имеет множество необязательных настроек, обычно проще представить их как свойства или поля вместо определения перегрузки конструктора для каждой возможной комбинации настроек.) Синтаксис заключается в одной или нескольких записях вида PropertyOrFieldName=Value после аргументов конструктора (или вместо них, если их нет). В листинге 14.3 показан другой атрибут, используемый в юнит-тестировании, ExpectedExceptionAttribute, позволяющий указать, что при выполнении теста вы ожидаете, что он выдаст конкретное исключение. Тип исключения является обязательным, поэтому мы передаем его в качестве аргумента конструктора, но данный атрибут позволяет также указать, должен ли исполнитель теста принимать исключения типа, производного от указанного. (По умолчанию он принимает только точное совпадение.) Это поведение управляется с помощью свойства AllowDerivedTypes.

Листинг 14.3. Указание необязательных настроек атрибута со свойствами

[ExpectedException(typeof(ArgumentException), AllowDerivedTypes = true)][TestMethod]public void ThrowsWhenNameMalformed(){...

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

Цели атрибутов

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

В большинстве случаев вы обозначаете цель, просто помещая атрибут непосредственно перед ней. Но это не сработает в случае сборок или модулей, потому что в вашем исходном коде нет ничего, что бы их представляло, все в вашем проекте идет в сборку, которую он производит. Модули, в свою очередь, тоже являются совокупностью (как правило, составляя сборку, как я описал в главе 12). Поэтому для них мы должны явно указать цель в начале атрибута. Вы часто будете видеть атрибуты уровня сборки, подобные показанным в листинге 14.4, в файле GlobalSuppressions.cs. Visual Studio иногда предлагает варианты для изменения вашего кода, и если вы решите подавить этот функционал, это можно сделать с помощью атрибутов уровня сборки.

Листинг 14.4. Атрибуты уровня сборки

[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage(   "StyleCop.CSharp.NamingRules",   "SA1313:Parameter names should begin with lower-case letter",   Justification = "Triple underscore acceptable for unused lambda parameter",   Scope = "member",   Target = "~M:Idg.Examples.SomeMethod")]

Атрибуты уровня модуля следуют той же схеме, хотя и встречаются гораздо реже. Не в последнюю очередь это происходит потому, что многомодульные сборки встречаются довольно редко и не поддерживаются .NET Core. В листинге 14.5 показано, как настроить возможность отладки конкретного модуля в том случае, если вы хотите, чтобы один модуль в многомодульной сборке был легко отлаживаемым, а остальные JIT-компилируемыми с полной оптимизацией. (Это специально придуманный сценарий, с помощью которого я могу показать синтаксис. На практике вы вряд ли захотите это делать.) Я расскажу об атрибуте DebuggableAttribute позже, в подразделе JIT-компиляция на с. 743.

Листинг 14.5. Атрибут уровня модуля

using System.Diagnostics;[module: Debuggable(DebuggableAttribute.DebuggingModes.DisableOptimizations)]

Возвращаемые значения методов могут быть аннотированы, и это также требует квалификации, потому что атрибуты возвращаемого значения располагаются перед методом, там же, где и атрибуты, которые применяются к самому методу. (Атрибуты для параметров не нуждаются в квалификации, потому что они располагаются в круглых скобках вместе с аргументами.) В листинге 14.6 показан метод с атрибутами, применяемыми как к методу, так и к типу возвращаемого значения. (Атрибуты в этом примере являются частью служб взаимодействия, которые позволяют коду .NET вызывать внешний код, такой как API ОС. В этом примере импортируется функция библиотеки Win32, что позволяет использовать ее из C#. Существует несколько различных представлений для логических значений в неуправляемом коде, поэтому в данном случае я аннотировал возвращаемый тип с помощью атрибута MarshalAsAttribute, указав, какой именно тип следует ожидать CLR.)

Оформить предзаказ бумажной книги можно на нашем сайте
Подробнее..

Книга Наглядный CSS

08.06.2021 14:19:38 | Автор: admin
image Привет, Хаброжители! На 1 июня 2018 года CSS содержал 415 уникальных свойств, относящихся к объекту style в любом элементе браузера Chrome. Сколько свойств доступно в вашем браузере на сегодняшний день? Наверняка уже почти шесть сотен. Наиболее важные из них мы и рассмотрим. Грег Сидельников упорядочил свойства по основной категории (положение, размерность, макеты, CSS-анимация и т. д.) и визуализировал их работу. Вместо бесконечных томов документации две с половиной сотни иллюстраций помогут вам разобраться во всех тонкостях работы CSS. Эта книга станет вашим настольным справочником, позволяя мгновенно перевести пожелания заказчика и собственное видение в компьютерный код!



Позиционирование


Тестовый элемент
image

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

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

Доступно пять типов позиционирования: static (статичное) (по умолчанию), relative (относительное), absolute (абсолютное), fixed (фиксированное) и sticky (липкое). Мы рассмотрим их на протяжении всей этой главы.

По умолчанию для всех элементов используется статичное позиционирование:

image

Относительное позиционирование практически такое же, как и статичное:

image

Статичное и относительное позиционирование
По умолчанию свойство position установлено в static, то есть элементы отображаются в том порядке, в котором они были указаны в вашем HTML-документе, в соответствии с нормальным потоком HTML-страницы.

На статично позиционированные элементы не влияют свойства top, left, right и bottom.

Чтобы понять разницу, создадим несколько основных стилей CSS:

001 /* Применить границу ко всем элементам <div> */002 div { border: 1px solid gray; }003004 /* Установить произвольные значения ширины и положения */005 #A { width: 100px; top: 25px; left: l00px; }006 #B { width: 215px; top: 50px; }007 #C { width: 250px; top: 50px; left:25px; }008 #D { width: 225px; top: 65px; }009 #E { width: 200px; top: 70px; left:50px; }

Граница 1px solid gray применена ко всем элементам div, поэтому теперь легче увидеть фактические размеры каждого HTML-элемента при отображении его в браузере.

Далее мы применим свойства position: static и position: relative к элементу div, чтобы увидеть разницу между статичным и относительным позиционированием.

image

По сути, элементы с позиционированием static и relative одинаковы, за исключением того, что элементы relative могут иметь top (верхнюю) и left (левую) позиции относительно их исходного местоположения. Относительные элементы также могут иметь right (правое) и bottom (нижнее) положение.

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

Следовательно, свойство position: relative не гарантирует полную точность при необходимости разместить элемент в идеальном месте в его родительском контейнере. Для такой цели больше всего подходит свойство position: absolute.

Абсолютное и фиксированное позиционирование

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

image

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

В данной главе мы рассмотрим более приближенные к реальности примеры.

Обратите внимание: если свойства width и height родителя не указаны явно, то применение позиционирования absolute (или fixed) к его единственному дочернему элементу преобразует его размеры в 0 0, однако данный элемент все равно будет позиционироваться относительно него:

image

На предыдущей схеме слева абсолютно позиционированные элементы не заполняют свой родительский контейнер содержимым. Они как бы плавают над ним, сохраняя положение относительно своего элемента-контейнера. Справа размеры родительского элемента заданы явно. Технически для потомка со свойством position: absolute не задан никакой эффект, его точка поворота все еще находится в положении 0 0 родительского элемента.

Чтобы элементы со свойством position: absolute были выровнены относительно их родителя, его свойство position не должно быть установлено в static (по умолчанию):

image

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

image

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

Использование свойства position: absolute для выравнивания элементов по углам родителя:

image

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

Использование свойства position: absolute с отрицательными значениями:

image

Фиксированное позиционирование

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

Использование свойства position: fixed для размещения элементов в фиксированном месте на экране относительно документа:

image

Использование свойства position: fixed с отрицательными значениями:

image

Липкое позиционирование

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

image

Далее приведен простой код, чтобы навигационная панель прилипала к верхней (top: 0) границе экрана. Обратите внимание: добавлен код -webkit-sticky для совместимости с браузерами на движке Webkit (такими как Chrome):

001 .navbar {002 /* Определение некоторых основных настроек */003 padding: 0px;004 border: 20px solid silver;005 background-color: white;006 /* Добавить липкость */007 position: -webkit-sticky;008 position: sticky;009 top: 0;010 }

Более подробно с книгой можно ознакомиться на сайте издательства
Оглавление
Отрывок

Для Хаброжителей скидка 25% по купону CSS

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Подробнее..

Книга 100 главных принципов дизайна. 2-е издание

24.03.2021 16:21:32 | Автор: admin
image Привет, Хаброжители! Цель любого дизайна получение отклика. Мы хотим, чтобы человек что-то купил, прочитал или сделал. Разработка дизайна без понимания причин того или иного поведения людей похоже на блуждание по незнакомому городу без карты: движение будет хаотично, запутанно и неэффективно. Эта книга является симбиозом науки и практики, который необходим каждому дизайнеру.

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

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

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

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

Людей мотивируют социальные норма


Джессика Нолан (2008) хотела выяснить, можно ли изменить поведение, просто предоставив людям информацию, и если да, то какой тип информации с наибольшей вероятностью приведет к изменению поведения.

Нолан написала пять сообщений об использовании меньшего количества электроэнергии:

  1. Использование меньшего количества электроэнергии защищает окружающую среду.
  2. Использование меньшего количества электроэнергии делает вас социально ответственнее.
  3. Использование меньшего количества электроэнергии экономит ваши деньги.
  4. Ваши соседи использовали меньше электроэнергии.
  5. Вот сколько электричества вы использовали.

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

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

Выводы

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

Лень двигатель прогресса


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

Ленивый синоним эффективный?


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

Удовлетворение + Достаточность = разумная достаточность


Создателем термина удовлетворительность (satisfice) является Герберт Саймон (Herbert Simon). Этим термином он описывал стратегию принятия решений, в которой человек действует скорее адекватно, нежели оптимально. Концепция удовлетворительности строится на том, что полная оценка всех параметров не только требует неоправданных затрат, но и попросту невозможна. Согласно Саймону, мы часто не владеем информацией, чтобы взвесить все. Так что в любой ситуации решение типа что нужно сделать или что достаточно хорошо пред- почтительнее оптимального или совершенного решения. Если человек склонен следовать удовлетворительному решению, а не оптимальному, то это нужно учитывать при создании сайтов, программного обеспечения и других продуктов.

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


В своей книге Не заставляй меня думать (Dont Make Me Think, 2005) Стив Круг применяет идею удовлетворительности к поведению, которое можно наблюдать у посетителей сайтов. Вы надеетесь, что посетитель прочтет страницу целиком, но, как пишет Круг, на самом деле то, что происходит чаще всего (если повезет), это взгляд, брошенный на каждую новую страницу, просмотр некоторых кусков и щелчок на первой же ссылке, которая вызвала интерес или отдаленно напоминает то, что посетитель искал. Обычно остаются большие куски страниц, на которые пользователь даже не взглянул. Круг говорит о том, что веб-страница подобна доске объявлений следует предполагать, что люди просто бросят на нее быстрый взгляд.

В свете сказанного выше быстро просмотрите следующие экранные снимки домашних страниц некоторых правительственных веб-сайтов (рис. 57.1 и 57.2).

image

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

Первое впечатление относительно удовлетворительности крайне важно для принятия решения оставаться на данном сайте или нет.

Выводы

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

Более подробно с книгой можно ознакомиться на сайте издательства
Оглавление
Отрывок

Для Хаброжителей скидка 25% по купону Дизайн

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Подробнее..

Книга Создаем динамические веб-сайты на PHP. 4-е межд. изд.

24.05.2021 18:21:26 | Автор: admin
image Привет, Хаброжители! Сложно найти что-то толковое про PHP? Проверенная временем, обновленная в четвертом издании, эта книга помогает начинающим разработчикам научиться всему, что необходимо для создания качественных веб-приложений.

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

Вы получите множество рекомендаций по стилю программирования и процессу разработки ПО от Кевина Татро и Питера Макинтайра. Этот материал, изложенный в доступной и компактной форме, поможет вам овладеть мастерством программирования на PHP. Общие сведения о том, какой результат можно получить, используя PHP. Основы языка, включая типы данных, переменные, операторы, управляющие команды. Функции, строки, массивы и объекты. Решение распространенных задач разработки: обработка форм, проверка данных, отслеживание сеансовых данных и cookie. Работа с реляционными базами данных (MySQL) и базами данных NoSQL (например MongoDB). Генерирование изображений, создание файлов PDF, парсинг файлов XML. Безопасность скриптов, обработка ошибок, оптимизация быстродействия и другие нетривиальные темы.

Для кого написана эта книга
PHP это плавильный котел для смешения направлений программирования. Веб-дизайнеры ценят его за доступность и удобство, а программистам нравится его гибкость, мощность, разнообразие и скорость. Обоим направлениям необходим понятный и точный справочник по языку. Если вы (веб-)программист, то эта книга написана для вас. Мы представим общую картину языка PHP, а затем изложим подробности, не отнимая у вас лишнего времени. Многочисленные примеры, практические рекомендации по программированию и советы по стилю помогут вам стать не просто программистом PHP, а хорошим программистом PHP.
Веб-дизайнеры оценят доступные и практичные рекомендации по использованию конкретных технологий: JSON, XML, сеансовых данных, генерирования PDF, работы с графикой и т. д. Описание PHP и базовые концепции программирования изложены в книге доступным языком.

Регулярные выражения


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

В PHP регулярные выражения применяются: для поиска совпадений (и извлечения информации из строк), для замены текста в соответствии с паттерном и для разбиения строк на массив меньших фрагментов. Функция preg_match() выполняет поиск регулярного выражения.

Perl давно считается эталонным языком для работы с регулярными выражениями. В PHP используется библиотека C pcre, обеспечивающая почти полную поддержку возможностей регулярных выражений Perl, которые работают с произвольными двоичными данными и позволяют безопасно выполнять поиск по паттернам или в строках, содержащих нулевой байт (\x00).

Большинство символов в регулярных выражениях являются литеральными, что отражается на поиске совпадений. Например, если вы ищете совпадение для регулярного выражения "/cow/" в строке Dave was a cowhand, то совпадение будет найдено, потому что последовательность символов cow встречается в этой строке.

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

preg_match("/^cow/", "Dave was a cowhand"); // возвращает falsepreg_match("/^cow/", "cowabunga!"); // возвращает true

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

preg_match("/cow$/", "Dave was a cowhand"); // возвращает falsepreg_match("/cow$/", "Don't have a cow"); // возвращает true

Точка в регулярном выражении обозначает один любой символ:

preg_match("/c.t/", "cat"); // возвращает truepreg_match("/c.t/", "cut"); // возвращает truepreg_match("/c.t/", "c t"); // возвращает truepreg_match("/c.t/", "bat"); // возвращает falsepreg_match("/c.t/", "ct"); // возвращает false

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

preg_match("/\$5.00/", "Your bill is $5.00 exactly"); // возвращает truepreg_match("/$5.00/", "Your bill is $5.00 exactly"); // возвращает false

Регулярные выражения по умолчанию учитывают регистр символов, поэтому регулярное выражение "/cow/" не совпадет со строкой COW. Чтобы выполнить поиск совпадения символов без учета регистра, установите соответствующий флаг (показан далее в этой главе).

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

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

2. Набор альтернатив для строки (например, com, edu, net или org).

3. Повторяющиеся последовательности в строке (например, как минимум одна, но не более пяти цифр).

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

Символьные классы

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

preg_match("/c[aeiou]t/", "I cut my hand"); // возвращает truepreg_match("/c[aeiou]t/", "This crusty cat"); // возвращает truepreg_match("/c[aeiou]t/", "What cart?"); // возвращает falsepreg_match("/c[aeiou]t/", "14ct gold"); // возвращает false

Движок регулярных выражений находит в строке символ c, после чего проверяет, является ли следующий символ гласной буквой (a, e, i, o или u). Если нет, то движок переходит к поиску следующего символа c. Если да, движок проверяет, является ли следующий символ буквой t. Если совпадение обнаружено, движок возвращает true или, в противном случае, возобновляет поиск следующего символа c.

Символьный класс можно инвертировать, поставив символ ^ в начало перечисления символов:

preg_match("/c[^aeiou]t/", "I cut my hand"); // возвращает falsepreg_match("/c[^aeiou]t/", "Reboot chthon"); // возвращает truepreg_match("/c[^aeiou]t/", "14ct gold"); // возвращает false

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

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

preg_match("/[0-9]%/", "we are 25% complete"); // возвращает truepreg_match("/[0123456789]%/", "we are 25% complete"); // возвращает truepreg_match("/[a-z]t/", "11th"); // возвращает falsepreg_match("/[a-z]t/", "cat"); // возвращает truepreg_match("/[a-z]t/", "PIT"); // возвращает falsepreg_match("/[a-zA-Z]!/", "11!"); // возвращает falsepreg_match("/[a-zA-Z]!/", "stop!"); // возвращает true

Когда вы задаете символьный класс, некоторые специальные символы теряют свой смысл, тогда как другие, наоборот, приобретают новые роли. Так, якорный символ $ и точка теряют свои роли в символьных классах, тогда как символ ^ уже не обозначает привязку к началу строки, а инвертирует символьный класс, если является первым символом после открывающей квадратной скобки. Например, [^\]] совпадает с любым символом, кроме закрывающей квадратной скобки, тогда как [$.^] совпадает с любым из трех знаков (доллар, точка или крышка).

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

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

preg_match("/cat|dog/", "the cat rubbed my legs"); // возвращает truepreg_match("/cat|dog/", "the dog rubbed my legs"); // возвращает truepreg_match("/cat|dog/", "the rabbit rubbed my legs"); // возвращает false

Приоритет применения альтернатив может показаться странным: так, "/^cat|dog$/" выбирает один из двух вариантов "^cat" и dog$. Это означает, что совпадение будет найдено в строке, которая либо начинается с cat, либо завершается dog. Если вам нужна строка, содержащая только cat или dog, используйте регулярное выражение "/^(cat|dog)$/".

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

preg_match("/^([a-z]|[0-9])/", "The quick brown fox"); // возвращает falsepreg_match("/^([a-z]|[0-9])/", "jumped over"); // возвращает truepreg_match("/^([a-z]|[0-9])/", "10 lazy dogs"); // возвращает true

Повторяющиеся последовательности

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

Чтобы искать повторы отдельного символа, просто поставьте квантификатор за символом:

preg_match("/ca+t/", "caaaaaaat"); // возвращает truepreg_match("/ca+t/", "ct"); // возвращает falsepreg_match("/ca?t/", "caaaaaaat"); // возвращает falsepreg_match("/ca*t/", "ct"); // возвращает true

image


С квантификаторами и символьными классами можно решать такие задачи, как проверка на действительность телефонных номеров США:

preg_match("/[0-9]{3}-[0-9]{3}-[0-9]{4}/", "303-555-1212"); // возвращает truepreg_match("/[0-9]{3}-[0-9]{3}-[0-9]{4}/", "64-9-555-1234"); // возвращает false

Подпаттерны

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

preg_match("/a (very )+big dog/", "it was a very very big dog"); // возвращаетtruepreg_match("/^(cat|dog)$/", "cat"); // возвращает truepreg_match("/^(cat|dog)$/", "dog"); // возвращает true

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

preg_match("/([0-9]+)/", "You have 42 magic beans", $captured);// возвращает true и заполняет $captured

Нулевому элементу массива присваивается вся строка, в которой ведется поиск совпадений. В первом элементе хранится подстрока, совпавшая с первым подпаттерном (если совпадение обнаружено), во втором подстрока, совпавшая со вторым подпаттерном, и т. д.

Ограничители

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

preg_match("/\/usr\/local\//", "/usr/local/bin/perl"); // возвращает truepreg_match("#/usr/local/#", "/usr/local/bin/perl"); // возвращает true

Скобки круглые (), фигурные {}, квадратные [] и угловые <> тоже могут использоваться в качестве ограничителей паттернов:

preg_match("{/usr/local/}", "/usr/local/bin/perl"); // возвращает true

В разделе Флаги-модификаторы рассматриваются односимвольные модификаторы, которые можно разместить после закрывающего ограничителя для изменения поведения движка регулярных выражений. Очень полезен флаг x, который заставляет движок отсекать пробелы и комментарии, отмеченные #, из регулярных выражений перед поиском совпадения.

Следующие два паттерна эквивалентны, но второй читается намного проще:

'/([[:alpha:]]+)\s+\1/''/( # начать сохранение[[:alpha:]]+ # слово\s+ # пробел\1 # снова то же слово) # завершить сохранение/x'

Поведение при поиске совпадения

Точка. совпадает с любым символом, кроме символа новой строки (\n). Знак $ совпадает с концом строки или, если строка завершается символом новой строки, с позицией, непосредственно предшествующей этому символу:

preg_match("/is (.*)$/", "the key is in my pants", $captured);// $captured[1] содержит 'in my pants'

Символьные классы

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

image

image

Каждый класс [: что-то :] может использоваться вместо символа в символьном классе. Например, для нахождения любого символа, который является цифрой, буквой верхнего регистра или символом @, используйте следующее регулярное выражение:

[@[:digit:][:upper:]]

Однако символьный класс не может использоваться как конечная точка диапазона:
preg_match("/[A-[:lower:]]/", string);// недопустимое регулярное выражение
Символьная последовательность, которая в локальном контексте рассматривается как один символ, называется сверткой. Чтобы найти совпадение для одной из многосимвольных последовательностей в символьном классе, заключите ее в маркеры [. и .]. Например, если в локальном контексте присутствует свертка ch, следующий символьный класс будет совпадать с s, t или ch:

[st[.ch.]]

Последнее расширение символьных классов класс эквивалентности, для определения которого символы заключаются в [= и =]. Классы эквивалентности совпадают с символами, имеющими одинаковый приоритет упорядочения по правилам локального контекста. Например, локальный контекст может определять, что символы a, и имеют одинаковый приоритет упорядочения при сортировке. Чтобы найти совпадение для любого из них, используйте класс эквивалентности [=a=].

Более подробно с книгой можно ознакомиться на сайте издательства
Оглавление
Отрывок

Для Хаброжителей скидка 25% по купону PHP

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Подробнее..

Книга Роман с Data Science. Как монетизировать большие данные

31.03.2021 12:04:48 | Автор: admin
image Привет, Хаброжители! Мы сдали в типографию новую книгу Романа Зыкова rzykov. Она предназначена для думающих читателей, которые хотят попробовать свои силы в области анализа данных и создавать сервисы на их основе. Она будет вам полезна, если вы менеджер, который хочет ставить задачи аналитике и управлять ею. Если вы инвестор, с ней вам будет легче понять потенциал стартапа. Те, кто пилит свой стартап, найдут здесь рекомендации, как выбрать подходящие технологии и набрать команду. А начинающим специалистам книга поможет расширить кругозор и начать применять практики, о которых они раньше не задумывались, и это выделит их среди профессионалов такой непростой и изменчивой области.


Отрывок из книги


ОТЧЕТ, ДАШБОРД И МЕТРИКИ


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

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

Первые два относительно просты и делаются через специальные системы, которые могут генерировать отчеты по запросу. Я стараюсь максимально оставить эту задачу на откуп пользователям. Почему? Потому, что тратить на это время высококвалифицированных сотрудников значит стрелять из пушки по воробьям. Кстати, этим могут заняться стажеры-аналитики отличный способ наработать опыт и понять бизнес-контекст. Как мотивировать пользователей стараться самостоятельно? Во-первых, они сэкономят время, которое обычно тратят на постановку задачи и ожидание результата. Во-вторых, получат возможность самим вносить правки и изменения а значит творить. По моему опыту, обычно этим занимаются очень перспективные сотрудники, которые не бояться освоить новый инструмент, чтобы делать свою работу лучше. Остальным придется пройти через стандартный цикл планирования задач: а это время (дни, а иногда недели) и очень четкая формулировка технического задания. И кстати, все генеральные директора (Ozon.ru, Wikimart.ru, Ostrovok), с которыми я работал, пользовались OLAP-кубами со своих компьютеров. С их помощью они всегда могли ответить на простые вопросы, а если не получалось обращались к аналитикам.

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

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

  • Ключевой показатель (key performance indicator, KPI) это индикатор, который показывает, насколько далеко мы находимся от цели, например отставание/опережение плана.
  • Метрика это цифра, которая характеризует процесс, обычно используется как справочная информация.

Главное различие между метрикой и ключевым показателем наличие цели. В ключевых показателях она есть, в метриках обычно нет. Как правило, ключевые показатели используются в дашбордах компаний, где уже и продукт, и бизнес-процесс устоялись. Уже накоплено некоторое множество данных, что делает возможным прогнозирование целей. Метрики я обычно вношу туда, где нет достаточно устойчивых процессов. Их обычно ставят, когда цель пока не прогнозируема. Зрелость дашборда можно определить по соотношению количества ключевых показателей и метрик: чем метрик больше, тем более незрелый дашборд мы получаем на выходе.

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

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

Часто велико искушение сделать огромную простыню цифр, закрывающую все аспекты бизнеса. И я понимаю владельцев/менеджеров компаний на старте проекта по построению внутренней аналитической системы всегда хочется большего. Я наблюдал это и в Ozon.ru, и в Ostrovok.ru. К слову, эти строки написаны по мотивам письма, которое я писал восемь лет назад операционному директору Ostrovok.ru, он хотел получить от аналитиков ту самую простыню. А я считаю такое цифровым микроменеджментом, в нем легко запутаться, самые важные показатели похоронены среди второстепенных. С первого взгляда будет сложно понять, где возникла проблема, а это основная функция дашбордов. Бороться с этим можно, например, через внедрение OKR цели и ключевые результаты (Objectives and Key Results) [13] или системы сбалансированных показателей (Balanced Scorecard). В этой книге я не буду подробно останавливаться на этих методиках, но рекомендую вам с ними ознакомиться. Также можно чаще пользоваться графическими элементами, например, добавив на график линию тренда (с помощью семиточечного скользящего среднего, чтобы убрать недельную сезонность), будет легче заметить восходящий или нисходящий тренд.

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

Никакой дашборд не заменит интерактивный анализ, для которого нужны соответствующая аналитическая система (SQL, OLAP, Google Data Studio, Tableau) и знание контекста. Мы никогда не сможем придумать ограниченный набор отчетов, которые будут отвечать на вопрос почему. Максимум, что мы можем сделать, наращивать (но не слишком) объем правильных метрик, исходя из инцидентов, за которыми будем следить.

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

Разработка дашбордов это одна из самых нелюбимых работ у тех, кто занимается анализом данных. Когда я обсуждал этот вопрос с Ди Джеем Патилом, отметив, что 50 % времени аналитического отдела занимает работа над отчетностью, он сказал, что у них в LinkedIn тоже периодически накапливался пул таких задач и приходилось их закрывать. И взгрустнул. Но дашборды очень нужны они помогают контролировать общее здоровье вашей системы вверенных вам серверов и сетей, если вы системный администратор, или всей компании, если вы генеральный директор.

КОНФЛИКТ ИССЛЕДОВАТЕЛЯ И БИЗНЕСА


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

Я много раз проводил собеседования и с новичками, и с опытными людьми, и знаю, что поиск интересной работы главный тренд на рынке труда. Действующие специалисты говорят: Надоело заниматься рутиной и всякой ерундой, хочу заниматься моделями машинного обучения. Новички: Хочу заниматься компьютерным зрением и NLP (машинное обучение в лингвистике). В целом людей объединяет любовь к машинному обучению, но для меня это звучит как любовь строителя к молотку, который пытается построить дом лишь с его помощью.

Эндрю н (Andrew Ng), которого я считаю одним из главных исследователей и популяризаторов машинного обучения, автор моего любимого курса на Coursera, в своей рассылке deeplearning.ai писал:
Существует огромная разница между построением модели в блокноте Python (Jupyter Notebook) на компьютере в лаборатории и созданием реально работающих систем, которые создают ценность. Кажется, что сфера AI переполнена людьми, но по факту она широко открыта для профессионалов, которые знают, что делают.

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

Ноа Лоранг, аналитик данных из компании Basecamp, в своем блоге пишет:
Маленькая грязная тайна продолжающегося бума data science в том, что то, что обычно подразумевается под этим на самом деле, не нужно бизнесу. Бизнесу нужна точная и полезная информация для принятия решений: как тратить время и ресурсы компании. Очень небольшое подмножество задач в бизнесе может быть лучшим образом решено машинным обучением; большинство же из них нуждается в хороших данных и понимании их смысла, что может быть достигнуто простыми методами.

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

ОБ АВТОРЕ

Роман Владимирович Зыков, 1981 года рождения, в 2004 году получил степень бакалавра, а затем магистра прикладной физики и математики в МФТИ (Московском физико-техническом институте).

В 2002 году начал свой карьерный путь в аналитике данных (Data Science) в качестве технического консультанта в компании StatSoft Russia, российского офиса одноименной американской компании-разработчика пакета статистического анализа данных STATISTICA.

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

В 2009 году консультировал ряд проектов инвестиционного фонда Fast Lane Ventures и гейм-индустрии.

В 2010 году возглавил отдел аналитики в интернет-ритейлере Wikimart.ru.

В конце 2012 года стал сооснователем и совладельцем маркетинговой платформы для интернет-магазинов RetailRocket.ru. Компания Retail Rocket занимает лидирующие позиции автоматизации маркетинга для интернет-магазинов. Среди клиентов: Сбер, Детский мир, Oysho, Decathlon. На текущий момент компания является безусловным лидером на рынке в России и успешно работает на рынках Чили, Голландии, Испании и других.

С 2007-го вел блог Аналитика на практике (KPIs.ru ныне не существует), где евангелизировал анализ данных в применении к бизнес-задачам в электронной коммерции. Выступал на отраслевых конференциях, таких как РИФ, iMetrics, Gec 2014 вместе с Аркадием Воложем (Yandex), бизнес-конференциях в Дублине и Лондоне, в посольстве США (AMC Center), университете Сбербанка. Печатался в технологическом прогнозе PwC, ToWave, Ведомостях, Секрете фирмы.

В 2016 году прочитал мини-лекцию в концертном зале MIT в Бостоне о процессах тестирования гипотез.

В 2020 году был номинирован на премию CDO Award.

Опыт работы аналитиком данных 18 лет.

Оформить предзаказ.
Подробнее..

Книга Стильный Java. Код, который работает всегда и везде

11.02.2021 18:04:31 | Автор: admin
image Привет, Хаброжители! В современном мире разработки успешность приложения уже не определяется параметром просто работает. Хороший программист должен знать возможности языка, практические приемы проектирования и платформенные средства для достижения максимальной производительности и жизнеспособности программ. Эта книга написана для разработчиков, которые хотят создавать качественный софт. Затронуты все ключевые показатели ПО: скорость, затраты памяти, надежность, удобочитаемость, потоковая безопасность, универсальность и элегантность. Реальные задачи и прикладные примеры кода на Java помогут надежно усвоить концепции. Пройдя все этапы создания центрального проекта книги, вы сможете уверенно выбрать правильный путь оптимизации собственного приложения.


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

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

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


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

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

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

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

Глава 2. Подробное описание эталонной реализации, обеспечивающей хороший баланс разных свойств.

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

Глава 4. Проведем эксперименты с эффективностью по затратам памяти и увидим, что по сравнению с эталонной реализацией затраты памяти сокращаются более чем на 50 % при использовании объектов и на 90 % при отказе от использования отдельного объекта для каждого резервуара.

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

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

Глава 7. Произведем рефакторинг эталонной реализации для применения рекомендуемых методов создания чистого самодокументируемого кода.

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

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

Приложение А. При обсуждении лаконичности кода я представлю компактную реализацию примера, объем исходного кода которого составит всего 15 % от эталонной версии. Конечно, получится заумный код, за который вас запинают на любом сеансе рецензирования кода.
Приложение Б. Наконец, мы соберем воедино все свойства и построим финальную версию класса, представляющего резервуары.


4.4. Черная дыра [Memory4]


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

Я предлагаю хранить и индексы, и объемы воды в одном массиве. Первые целые числа, вторые вещественные. Какой тип должен иметь массив? В голову приходят два варианта, приводящие к одинаковым затратам памяти (4 байта на резервуар):

1. Массив типа int, в котором содержимое ячейки должно интерпретироваться как объем воды и его можно делить на постоянную величину (фактически реализация чисел с фиксированной точкой). Например, если все объемы будут делиться на 10 000, они будут определяться с 5 цифрами в дробной части.

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

В листинге 4.12 я выбрал второй вариант, который выглядит проще, хотя, как вы вскоре увидите, у него есть свои недостатки.

Листинг 4.12. Memory4: поле конструктор не нужен

public class Container {   private static float[] nextOrAmount;

Как при чтении содержимого ячейки отличать следующие значения от объемов воды? Можно воспользоваться доисторическим трюком и закодировать один из двух случаев положительными числами, а другой отрицательными. Положительное число можно будет интерпретировать как индекс следующего резервуара, а отрицательное число будет обозначать объем воды в этом резервуаре с обратным знаком. Например, если nextOrAmount[4] == -2.5, это означает, что резервуар 4 является последним в своей группе (или изолированным) и содержит 2,5 единицы воды.

Есть небольшая проблема: в формате с плавающей точкой положительный нуль не отличается от отрицательного. Эту неоднозначность можно устранить, считая, что нуль всегда обозначает объем, и никогда не использовать его в качестве индекса следующего резервуара. Чтобы не терять нулевую ячейку, увеличьте все индексы, хранящиеся в массиве, на 1 (смещение). Например, если за резервуаром 4 следует резервуар 7, nextOrAmount[4] == 8.

На рис. 4.8 представлено распределение памяти этой реализации после выполнения первых трех частей основного сценария. Значение 2,0 в первой ячейке смещенный указатель на следующий резервуар означает, что первый резервуар (a) связан с резервуаром под номером 1 (b). Значение 4,0 в третьей ячейке указывает, что c является последним резервуаром в своей группе, а каждый резервуар в этой группе содержит 4,0 единицы воды.

image

В листинге 4.13 представлен код метода getAmount. Он переходит к следующим значениям, как в связанном списке (вторая строка), пока не найдет последний резервуар в списке, который распознается по отрицательному или нулевому значению. Это значение представляет собой объем воды в резервуаре с обратным знаком. Обратите внимание на 1 в конце третьей строки кода (удаление смещения) и знак минус после return для возврата объема воды с правильным знаком.
image

У float, использующегося для представления индексов массивов, есть еще один скрытый недостаток. Теоретически индексы массивов могут охватывать весь диапазон неотрицательных 32-разрядных целых чисел: от 0 до 2^31 1 (приблизительно 2 млрд, также обозначается Integer.MAX_VALUE). Формат с плавающей точкой имеет существенно больший диапазон, но с изменяющимся разрешением. Расстояние между двумя соседними числами изменяется в зависимости от размера (рис. 4.9). Для малых значений (близких к нулю) следующее число с плавающей точкой расположено чрезвычайно близко. Для больших значений следующее число с плавающей точкой находится дальше. В какой-то момент расстояние превышает 1, и ряд чисел с плавающей точкой начинает пропускать целочисленные значения.
image

Например, из-за расширенного диапазона тип float способен точно представить число 1E10 (10^10 или 10 млрд), чего не позволяет сделать целочисленный тип. Оба типа могут представить значение 1E8 (100 млн), но если переменная float содержит 1E8, то при увеличении на 1 она останется равной 1E8. У чисел с плавающей точкой не хватает значащих цифр для представления числа 100 000 001.

Расстояние между 1E8 и следующим числом типа float превышает 1. Хотя число 1E8 входит в диапазон чисел float, оно не входит в непрерывный целочисленный диапазон float, то есть в диапазон целых чисел, которые могут быть представлены точно и без разрывов. В табл. 4.7 приведены непрерывные целочисленные диапазоны для большинства числовых примитивных типов.
image

Неожиданный вопрос 5
Выберите тип данных и исходное значение переменной x таким образом, чтобы цикл
while (x+1==x) {} выполнялся бесконечно.

Использование float в качестве индекса массива не лучшая идея. Оно сработает, только если индексы остаются в непрерывном целочисленном диапазоне, границы которого заметно меньше Integer.MAX_VALUE. Чтобы уточнить, насколько меньше, нужно учесть, что неотрицательные целые числа содержат 31 значащий бит, тогда как неотрицательные числа с плавающей точкой имеют только 24 значащих бита. Так как 31 24 = 7, порог для float в 2^7 = 128 раз меньше Integer.MAX_VALUE.

Если создать более 2^24 резервуаров, начнут происходить странные вещи и потребуется включить проверки времени выполнения в метод newContainer. Но эта глава посвящена потреблению памяти, поэтому будем придерживаться плана и оптимизировать только одно свойство кода за раз, а с факторами надежности подождем до главы 6. Остальной исходный код Memory4 можно найти в репозитории (http://personeltest.ru/aways/bitbucket.org/mfaella/exercisesinstyle).

4.4.1. Временная сложность и затраты памяти


Один статический массив из Memory4 требует 4 байт для хранения ссылки на массив, 16 байт стандартных затрат массивов и 4 байт для каждой ячейки. В этой реализации заданное количество резервуаров всегда занимает одинаковый объем памяти, независимо от того, как они соединены. В табл. 4.8 приведены оценки затрат памяти для двух наших обычных сценариев.
image

За крайнюю экономию памяти приходится платить замедлением выполнения, как видно из табл. 4.9. Методы connect и addWater должны вычислять размер группы по заданному индексу произвольного резервуара группы. Для этого приходится возвращаться к первому резервуару группы, а затем обходить весь виртуальный список резервуаров для определения длины. Найти первый резервуар в группе не так просто, ведь это единственный элемент группы, на который не ссылается другой указатель. Чтобы найти его, необходимо обойти список в обратном порядке, что требует квадратичного времени.

image

4.5. Баланс затрат памяти и времени


Начнем с краткой сводки требований по памяти для четырех версий резервуаров из этой главы и сравним их с реализацией Reference из главы 2.

Как видно из табл. 4.10, разумный выбор коллекций и способов кодирования позволяет добиться значительной экономии памяти. Чтобы выйти за рамки, представленные служебными затратами объектов, нам пришлось нарушить API из главы 1 и идентифицировать резервуары целыми числами вместо объектов резервуаров. Все реализации этой главы также жертвуют удобочитаемостью и, как следствие, удобством сопровождения. Стремление к эффективности использования памяти ведет к использованию низкоуровневых типов (в основном массивов) вместо высокоуровневых коллекций и специальных кодировок, вплоть до применения значений float в качестве индексов массивов в Memory4. Во многих рабочих средах такие приемы считаются нежелательными, но им находится место в узкоспециализированных ситуациях с жесткими ограничениями по памяти, как в некоторых встроенных системах, или с необходимостью хранить огромные объемы данных в основной памяти.

image

Как упоминалось в главе 1, эффективности по затратам памяти и времени часто вступают в конфликт. В этой и предыдущей главах были приведены как положительные, так и отрицательные примеры такого рода. На рис. 4.10 изображены требования к затратам памяти и времени для семи реализаций из этих глав, а также реализации Reference из главы 2. Вспомните, что в Memory3 и Memory4 заметная экономия памяти достигается за счет изменения API резервуаров.
image

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

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

4.6. А теперь совсем другое


Пришло время применить методы экономии памяти в другом сценарии: работе с мультимножествами. Мультимножеством называется множество, которое может содержать дубликаты. Так, мультимножество {a, a, b} отлично от {a, b}, но неотличимо от {a, b, a}, потому что порядок элементов не важен.

Спроектируем реализацию мультимножества MultiSet, которая эффективно расходует память и поддерживает следующие методы:

  • public void add(T elem) вставляет elem в мультимножество;
  • public long count(T elem) возвращает количество вхождений elem в мультимножество.

Для сравнения и выбора между разными реализациями можно руководствоваться следующими вопросами:

1. Предположим, вы вставляете n разных объектов c возможностью многократной вставки одного объекта и всего есть m вставок (то есть m по крайней мере не меньше n). Сколько байт потребуется для их хранения?

2. Какова временная сложность операций add и count в вашей реализации?

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

4.6.1. Малое количество дубликатов


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

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

Реализация должна выглядеть примерно так:

public class MultiSet<T> {   private List<T> data = new ArrayList<>();   public void add(T elem) {       data.add(elem);   }   public long count(T elem) {      long count = 0;      for (T other: data) {         if (other.equals(elem)) {             count++;         }      }      return count;   }}

С новой библиотекой потоков можно переписать метод count в однострочной реализации:

public long count(T elem) {   return data.stream().filter(x -> x.equals(elem)).count();}

Метод add выполняется за постоянное (амортизированное) время (раздел 3.3.5), а count за линейное время. Затраты памяти после m вставок n разных объектов составят 56 + 4 m байт (не зависит от n):

  • 12 байт служебная информация объекта MultiSet;
  • 4 байта ссылка на ArrayList;
  • 40 байт минимальная коллекция ArrayList (табл. 4.4);
  • 4 m байт для ссылок на элементы мультимножества.

4.6.2. Большое количество дубликатов


Если дубликаты встречаются часто, лучше использовать два массива: для хранения самих объектов и для хранения количества повторений каждого объекта. Если вы знакомы с библиотекой коллекций, то догадаетесь, что эта задача идеально подходит для Map. Однако обе стандартные реализации Map (HashMap и TreeMap) представляют собой связанные структуры и занимают намного больше памяти, чем две коллекции ArrayList.

В итоге у вас получится нечто такое:

public class MultiSet<T> {   private List<T> elements = new ArrayList<>();   private List<Long> repetitions = new ArrayList<>();   ...

Остаток реализации я оставлю вам для самостоятельной работы. Проследите, чтобы i-й элемент repetitions (который вы получаете от repetitions.get(i)) содержал количество повторений объекта elements.get(i).

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

Затраты памяти после m вставок n разных объектов составят 100 + 28 n байт (не зависит от m):

  • 12 байт служебная информация объекта MultiSet;
  • 2 4 байта ссылки на две коллекции ArrayList;
  • 2 40 байт две минимальные коллекции ArrayList;
  • 4 n байт для хранения ссылок на уникальные элементы (первый массив);
  • (4 + 20) n байт для хранения счетчиков Long на уникальные элементы (второй массив). (Каждый объект Long занимает 12 + 8 = 20 байт.)

Решение с двумя массивами наиболее эффективно по памяти, если 100 + 28 n < 56 + 4 m, то есть в среднем каждый объект представлен в коллекции не менее 7 раз (m > 11 + 7 n).

4.7. Реальные сценарии использования


В главах 3 и 4 рассматривались два основных фактора, влияющих на эффективность алгоритма: время и затраты памяти. Было показано, что задача может быть решена разными способами (например, с использованием ArrayList вместо HashSet для хранения групп резервуаров). Выбор того или иного метода обычно приводит к компромиссу между эффективностью по времени и затратам памяти. Лучший выбор зависит от контекста решаемой задачи. Рассмотрим пару сценариев с высокой эффективностью по затратам памяти.

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

  • Смартфоны в наши дни оснащаются почти таким же объемом памяти, как стандартные портативные компьютеры. Однако когда компания Google разрабатывала ОС Android в начале 2000-х годов, ситуация была иной. Система Android также должна была работать на устройствах с существенно меньшими объемами памяти, чем у современных телефонов. По этой причине в API Android можно найти следы усилий по экономии памяти.

Например:

Пакет android.util содержит несколько классов, предоставляющих альтернативы для стандартных коллекций Java с меньшими затратами памяти. Например, SparseArray эффективная по памяти реализация карты (или ассоциативного массива), связывающей целочисленные ключи с объектами. (В упражнении 2 этой главы вам будет предложено проанализировать этот класс.)

Все классы Android, относящиеся к работе с графикой, используют для представления координат, углов поворота и т. д. значения float с одинарной точностью вместо значений double. Пример можно найти в классе android. graphics.Camera.

  • XML широко используется для обмена данными между разнородными системами. В стандартной схеме взаимодействия приложение разбирает XML, сохраняет контент в реляционной базе данных и, наконец, сохраняет XML в виде BLOB (binary large object). Далее бизнес-логика и запросы выполняются с использованием реляционной схемы, и события загрузки исходной разметки XML происходят редко. То есть лучше проектировать процесс, эффективный по затратам памяти, который сжимает XML-документы перед их сохранением в базе данных.

Об авторе

Марко Фаэлла преподаватель computer science в Неаполитанском университете имени Фридриха II (Италия). Помимо академических исследований в области computer science Марко увлеченно занимается преподаванием и программированием. Последние 13 лет он ведет курсы про-граммирования повышенной сложности, а также является автором учебника для желающих получить сертификат Java-разработчика и видеокурса по потокам в языке Java.

Более подробно с книгой можно ознакомиться на сайте издательства
Оглавление
Отрывок

Для Хаброжителей скидка 25% по купону Java

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Подробнее..

Книга Эффективный Java. Тюнинг кода на Java 8, 11 и дальше. 2-е межд. издание

10.03.2021 14:15:34 | Автор: admin
image Привет, Хаброжители! Программирование и тестирование обычно принято относить к разным профессиональным сферам. Скотт Оукс признанный эксперт по языку Java уверен, что если вы хотите работать с этим языком, то обязаны понимать, как выполняется код в виртуальной машине Java, и знать, какие настройки влияют на производительность. Вы сможете разобраться в производительности приложений Java в контексте как JVM, так и платформы Java, освоите средства, функции и процессы, которые могут повысить производительность в LTS-версиях Java, и познакомитесь с новыми возможностями (такими как предварительная компиляция и экспериментальные уборщики мусора). В этой книге вы: Узнаете, как платформы и компиляторы Java влияют на производительность. Разберетесь c механизмом уборки мусора. Освоите четыре принципа получения наилучших результатов при тестировании производительности. Научитесь пользоваться JDK и другими инструментами оценки производительности. Узнаете как настройка и приемы программирования позволяют минимизировать последствия уборки мусора. Научитесь решать проблемы производительности средствами Java API. Поймете, как улучшить производительность приложений баз данных Java.

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

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

Однако на фундаментальном уровне, для того чтобы ваши программы Java работали действительно быстро, необходимо глубоко понимать, как работает JVM (и Java API). Существуют сотни флагов настройки Java, и настройка JVM не должна сводиться к тому, чтобы бездумно изменять эти флаги и смотреть, что из этого получится. Вместо этого я постарался подробно рассказать о том, что делают JVM и API, в надежде, что вы поймете суть происходящего, разберетесь в конкретном поведении приложения и осознаете, почему оно неэффективно работает. После этого вам останется решить простую (или относительно простую) задачу: избавиться от нежелательного (неэффективного) поведения.

У работы в области производительности Java есть один интересный аспект: по уровню подготовки разработчики часто сильно отличаются от инженеров из группы производительности или контроля качества. Я знаю разработчиков, которые помнят тысячи сигнатур малоизвестных методов, редко используемых в Java API, но понятия не имеют, что делает флаг -Xmn. И я знаю инженеров по тестированию, которые могут выжать последнюю каплю производительности установкой различных флагов уборщика мусора, но едва смогут написать программу Hello World на Java.

Производительность Java включает обе области: настройку флагов компилятора, уборщика мусора и т.д., а также оптимальное использование API. Я буду считать, что вы хорошо умеете писать программы на Java. Даже если вас в первую очередь интересуют непрограммные аспекты Java, мы проведем немало времени за обсуждением программ, включая примеры, которые предоставляют тестовые данные для примеров.

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


Алгоритмы уборки мусора



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

image


S полная поддержка; D считается устаревшим; E экспериментальная поддержка; E2 экспериментальная поддержка в сборках OpenJDK, но не в сборках Oracle.

Ниже приводятся краткие описания всех алгоритмов; в главе 6 приведена более подробная информация об их настройке.

Последовательный уборщик мусора

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

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

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

Параллельный уборщик мусора

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

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

Учтите, что в старых версиях JVM параллельная уборка мусора включалась для старых и новых поколений по отдельности, поэтому иногда встречаются упоминания флага -XX:+UseParallelOldGC. Этот флаг считается устаревшим (хотя и продолжает функционировать, и его можно отключить для параллельной уборки только молодого поколения, если по какой-то причине возникнет такая необходимость).

Уборщик мусора G1

Уборщик мусора G1 использует стратегию конкурентной уборки мусора для очистки кучи с минимальными паузами. Этот уборщик используется по умолчанию в JDK 11 и более поздних версиях для 64-разрядных JVM на машинах, оснащенных двумя и более процессорами.
Уборщик мусора G1 делит кучу на области, но при этом рассматривает кучу как разделенную на два поколения. Некоторые области формируют молодое поколение, при уборке которого приостанавливаются все потоки приложения, а все живые объекты перемещаются в старое поколение или области выживших объектов (для чего используются множественные потоки).
В уборщике мусора G1 старое поколение обрабатывается фоновыми потоками, которым не нужно останавливать потоки приложения для выполнения большей части своей работы. Так как старое поколение делится на области, уборщик мусора G1 может освобождать объекты из старого поколения, копируя их из одной области в другую; это означает, что он (по крайней мере частично) сжимает кучу в ходе нормальной обработки. Это способствует предотвращению фрагментации куч G1, хотя полностью исключить ее нельзя.

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

Уборщик мусора G1 включается при помощи флага -XX:+UseG1GC. В большинстве случаев он используется по умолчанию в JDK 11, но также сохраняет функциональность в JDK 8 особенно в поздних сборках JDK 8, содержащих многие важные исправления ошибок и улучшения производительности, которые были перенесены из более поздних выпусков. Как можно увидеть при углубленном анализе уборщика G1, в JDK 8 не поддерживается один важный аспект производительности, из-за которого данный механизм может стать непригодным для этого выпуска.

Уборщик мусора CMS

Уборщик мусора CMS стал первым конкурентным уборщиком. Как и другие алгоритмы, CMS останавливает все потоки приложений в ходе малой уборки мусора, которую он проводит в нескольких потоках.

CMS официально считается устаревшим в JDK 11 и выше, а использовать его в JDK 8 не рекомендуется. С практической точки зрения главный недостаток CMS заключается в том, что он не может сжимать кучу в ходе фоновой обработки. Если куча фрагментируется (что с большой вероятностью произойдет в какой-то момент), CMS приходится остановить все потоки приложения и сжать кучу, что противоречит самой цели конкурентного уборщика. С учетом этого обстоятельства и появлением уборщика G1 использовать CMS более не рекомендуется.
CMS включается флагом -XX:+UseConcMarkSweepGC, который по умолчанию равен false. Традиционно CMS также требовал установки флага -XX:+UseParNewGC (в противном случае уборка в молодом поколении будет выполняться одним потоком), хотя этот флаг считается устаревшим.

Экспериментальные уборщики мусора

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

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

Java предоставляет механизм, при помощи которого приложения могут инициировать принудительную уборку мусора: метод System.gc(). Вызывать этот метод почти всегда нежелательно. Вызов всегда запускает полную уборку мусора (даже если JVM работает с уборщиком G1 или CMS), поэтому потоки приложения будут остановлены на относительно длительный период времени. Вызов этого метода не сделает приложение более эффективным; он заставит уборку мусора выполниться ранее, чем она могла бы произойти, но в действительности проблема не исчезает, а всего лишь перемещается в другое место.

У каждого правила есть исключения, особенно при мониторинге производительности или хронометражных тестах. Для небольших хронометражных тестов, которые выполняют код для разогрева JVM, выполнение принудительной уборки мусора перед циклом измерений может иметь смысл. (Так поступает jmh, хотя обычно это не обязательно.) Аналогичным образом, при анализе кучи обычно стоит провести полную уборку мусора перед получением дампа. Многие способы получения дампа кучи все равно проводят полную уборку мусора, но ее также можно инициировать другими способами: выполнить команду jcmd<идентификатор_процесса> GC.run или подключиться к JVM при помощи jconsole и щелкнуть на кнопке Perform GC на панели Memory.

Другим исключением является механизм RMI (Remote Method Invocation), который вызывает System.gc() каждый час в процессе работы распределенного уборщика мусора. Периодичность вызова можно изменить, присвоив другие значения двум системным свойствам: -Dsun.rmi.dgc.server.gcInterval=N и -Dsun.rmi.dgc.client.gcInterval=N. Значения N задаются в миллисекундах, а значение по умолчанию равно 3 600 000 (один час).

Если вы выполняете сторонний код, который вызывает метод System.gc(), а для вас это нежелательно, уборку мусора можно предотвратить включением флага -XX:+DisableExplicitGC в аргументы JVM; по умолчанию этот флаг равен false. Такие приложения, как серверы Java EE, часто включают этот аргумент, для того чтобы вызовы RMI GC не мешали их работе.

Резюме

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

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

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

Уборщик мусора G1 используется по умолчанию в JDK 11 и выше; он проводит конкурентную чистку старого поколения во время выполнения потоков приложения, теоретически избегая полной уборки мусора. Такая архитектура снижает вероятность полной уборки мусора по сравнению с CMS.

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

Выбор алгоритма уборки мусора

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

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

Более подробно с книгой можно ознакомиться на сайте издательства
Оглавление
Отрывок

Для Хаброжителей скидка 25% по купону Java

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Подробнее..

Книга Работа с ядром Windows

06.04.2021 14:13:41 | Автор: admin
image Привет, Хаброжители! Ядро Windows таит в себе большую силу. Но как заставить ее работать? Павел Йосифович поможет вам справиться с этой сложной задачей: пояснения и примеры кода превратят концепции и сложные сценарии в пошаговые инструкции, доступные даже начинающим.

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

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

Глава 8


Уведомления потоков и процессов


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

В этой главе:

  • Уведомления процессов
  • Реализация уведомления процессов
  • Передача данных в пользовательский режим
  • Уведомления потоков
  • Уведомления о загрузке образов
  • Упражнения

Уведомления процессов


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

При создании процесса драйвер также получает возможность остановить создание процесса и вернуть ошибку стороне, инициировавшей создание процесса. Эта возможность доступна только в режиме ядра.
Windows предоставляет другие механизмы уведомления о создании или уничтожении процессов. Например, с механизмом ETW (Event Tracing for Windows) такие уведомления могут приниматься процессами пользовательского режима (работающими с повышенными привилегиями). Впрочем, предотвратить создание процесса при этом не удастся. Более того, у ETW существует внутренняя задержка уведомлений около 13 секунд (по причинам, связанным с быстродействием), так что процесс с коротким жизненным циклом может завершиться до получения уведомления. Если в этот момент будет сделана попытка открыть дескриптор для созданного процесса, произойдет ошибка.

Основная функция API для регистрации уведомлений процессов PsCreateSetProcessNotifyRoutineEx определяется так:

NTSTATUSPsSetCreateProcessNotifyRoutineEx (    _In_ PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,    _In_ BOOLEAN Remove);

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

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

typedef void(*PCREATE_PROCESS_NOTIFY_ROUTINE_EX) (    _Inout_ PEPROCESS Process,    _In_ HANDLE ProcessId,    _Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo);

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

Аргументы функции уведомления:

  • Process объект создаваемого или уничтожаемого процесса.
  • ProcessId уникальный идентификатор процесса. Хотя аргумент объявлен с типом HANDLE, на самом деле это идентификатор.
  • CreateInfo структура с подробной информацией о создаваемом процессе. Если процесс уничтожается, то этот аргумент равен NULL.

При создании процесса функция обратного вызова драйвера выполняется создающим потоком. При выходе из процесса функция обратного вызова выполняется последним потоком, выходящим из процесса. В обоих случаях обратный вызов вызывается в критической секции (с блокировкой нормальных APC-вызовов режима ядра).
В Windows 10 версии 1607 появилась другая функция для уведомлений процессов: PsCreateSetProcessNotifyRoutineEx2. Эта расширенная функция создает обратный вызов, сходный с предыдущим, но обратный вызов также активизируется для процессов Pico. Процессы Pico используются хост-процессами Linux для WSL (Windows Subsystem for Linux). Если драйвер заинтересован в таких процессах, он должен регистрироваться с расширенной функцией.

У драйвера, использующего эти обратные вызовы, должен быть установлен флаг IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY в заголовке PE (Portable Executable). Без установки флага вызов функции регистрации возвращает STATUS_ACCESS_DENIED (значение не имеет отношения к режиму тестовой подписи драйверов). В настоящее время Visual Studio не предоставляет пользовательского интерфейса для установки этого флага. Он должен задаваться в параметрах командной строки компоновщика ключом /integritycheck. На рис. 8.1 показаны свойства проекта при указании этого ключа.

image

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

typedef struct _PS_CREATE_NOTIFY_INFO {   _In_ SIZE_T Size;   union {       _In_ ULONG Flags;       struct {            _In_ ULONG FileOpenNameAvailable : 1;            _In_ ULONG IsSubsystemProcess : 1;            _In_ ULONG Reserved : 30;        };     };     _In_ HANDLE ParentProcessId;     _In_ CLIENT_ID CreatingThreadId;     _Inout_ struct _FILE_OBJECT *FileObject;     _In_ PCUNICODE_STRING ImageFileName;     _In_opt_ PCUNICODE_STRING CommandLine;     _Inout_ NTSTATUS CreationStatus;} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;

Описание важнейших полей этой структуры:

  • CreatingThreadId комбинация идентификаторов потока и процесса, вызывающего функцию создания процесса.
  • ParentProcessId идентификатор родительского процесса (не дескриптор). Этот процесс может быть тем же, который предоставляется CreateThreadId.UniqueProcess, но может быть и другим, так как при создании процесса может быть передан другой родитель, от которого будут наследоваться некоторые свойства.
  • ImageFileName имя файла с исполняемым образом; доступен при установленном флаге FileOpenNameAvailable.
  • CommandLine полная командная строка, используемая для создания процесса. Учтите, что он может быть равен NULL.
  • IsSubsystemProcess этот флаг устанавливается, если процесс является процессом Pico. Это возможно только в том случае, если драйвер регистрируется PsCreateSetProcessNotifyRoutineEx2.
  • CreationStatus статус, который будет возвращен вызывающей стороне. Драйвер может остановить создание процесса, поместив в это поле статус ошибки (например, STATUS_ACCESS_DENIED).

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

Реализация уведомлений процессов


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

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

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

enum class ItemType : short {    None,    ProcessCreate,    ProcessExit};struct ItemHeader {    ItemType Type;    USHORT Size;    LARGE_INTEGER Time;};

Приведенное выше определение перечисления ItemType использует новую возможность C++ 11 перечисления с областью видимости (scoped enums). В таких перечислениях значения имеют область видимости (ItemType в данном случае). Также размер этих перечислений может быть отличен от int short в данном случае. Если вы работаете на C, используйте классические перечисления или даже #define.

Структура ItemHeader содержит информацию, общую для всех типов событий: тип события, время события (выраженное в виде 64-разрядного целого числа) и размер полезных данных. Размер важен, так как каждое событие имеет собственную информацию. Если позднее вы захотите упаковать массив таких событий и (допустим) предоставить его клиенту пользовательского режима, клиент должен знать, где заканчивается каждое событие и начинается новое.

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

struct ProcessExitInfo : ItemHeader {    ULONG ProcessId;};

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

struct ExitProcessInfo {    ItemHeader Header;    ULONG ProcessId;};

Для идентификатора процесса используется тип ULONG. Использовать тип HANDLE не рекомендуется, так как в пользовательском режиме он может создать проблемы. Кроме того, тип DWORD не используется, хотя в заголовках пользовательского режима тип DWORD (32-разрядное целое без знака) встречается часто. В заголовках WDK тип DWORD не определен. И хотя определить его явно нетрудно, лучше использовать тип ULONG он означает то же самое, но определяется в заголовках как пользовательского режима, так и режима ядра.


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

В новом файле с именем SysMon.h определяется параметризованная структура, в которой хранится поле LIST_ENTRY с основной структурой данных:

template<typename T>struct FullItem {    LIST_ENTRY Entry;    T Data;};

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

struct FullProcessExitInfo {     LIST_ENTRY Entry;     ProcessExitInfo Data;};

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

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

Заголовок связанного списка должен где-то храниться. Мы создадим структуру данных для хранения всего глобального состояния драйвера (вместо набора отдельных переменных). Определение структуры выглядит так:

struct Globals {    LIST_ENTRY ItemsHead;    int ItemCount;    FastMutex Mutex;};


В определении используется тип FastMutex, который был разработан в главе 6. Также в определении встречается RAII-обертка AutoLock на C++ (тоже из главы 6).

Функция DriverEntry


Функция DriverEntry для драйвера SysMon похожа на одноименную функцию драйвера Zero из главы 7. В нее нужно добавить регистрацию уведомлений процессов и инициализацию объекта Globals:

Globals g_Globals;extern "C" NTSTATUSDriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING) {    auto status = STATUS_SUCCESS;    InitializeListHead(&g_Globals.ItemsHead);    g_Globals.Mutex.Init();    PDEVICE_OBJECT DeviceObject = nullptr;    UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\sysmon");    bool symLinkCreated = false;    do {        UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\sysmon");        status = IoCreateDevice(DriverObject, 0, &devName,            FILE_DEVICE_UNKNOWN, 0, TRUE, &DeviceObject);        if (!NT_SUCCESS(status)) {            KdPrint((DRIVER_PREFIX "failed to create device (0x%08X)\n",               status));               break;          }           DeviceObject->Flags |= DO_DIRECT_IO;          status = IoCreateSymbolicLink(&symLink, &devName);          if (!NT_SUCCESS(status)) {              KdPrint((DRIVER_PREFIX "failed to create sym link (0x%08X)\n",                 status));              break;          }          symLinkCreated = true;          // Регистрация для уведомлений процессов          status = PsSetCreateProcessNotifyRoutineEx(OnProcessNotify, FALSE);          if (!NT_SUCCESS(status)) {              KdPrint((DRIVER_PREFIX "failed to register process callback\ (0x%08X)\n",              status));          break;         }    } while (false);    if (!NT_SUCCESS(status)) {        if (symLinkCreated)             IoDeleteSymbolicLink(&symLink);        if (DeviceObject)             IoDeleteDevice(DeviceObject);     }     DriverObject->DriverUnload = SysMonUnload;     DriverObject->MajorFunction[IRP_MJ_CREATE] =     DriverObject->MajorFunction[IRP_MJ_CLOSE] = SysMonCreateClose;     DriverObject->MajorFunction[IRP_MJ_READ] = SysMonRead;     return status;}

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

Обработка уведомлений о выходе из процессов


В приведенном выше коде функция уведомления процессов называется OnProcessNotify, а ее прототип был представлен ранее в этой главе. Эта функция обратного вызова обрабатывает события создания и завершения процессов. Начнем с выхода из процессов, так как это событие намного проще создания процесса (как вы вскоре увидите). Общая схема функции обратного вызова выглядит так:

void OnProcessNotify(PEPROCESS Process, HANDLE ProcessId,    PPS_CREATE_NOTIFY_INFO CreateInfo) {    if (CreateInfo) {       // Создание процесса    }    else {      // Завершение процесса    }}

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

auto info = (FullItem<ProcessExitInfo>*)ExAllocatePoolWithTag(PagedPool,   sizeof(FullItem<ProcessExitInfo>), DRIVER_TAG);if (info == nullptr) {   KdPrint((DRIVER_PREFIX "failed allocation\n"));   return;}

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

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

auto& item = info->Data;KeQuerySystemTimePrecise(&item.Time);item.Type = ItemType::ProcessExit;item.ProcessId = HandleToULong(ProcessId);item.Size = sizeof(ProcessExitInfo);PushItem(&info->Entry);

Сначала мы обращаемся к самому элементу данных (в обход LIST_ENTRY) через переменную info. Затем заполняется информация заголовка: тип элемента хорошо известен, так как текущей является ветвь, обрабатывающая уведомления о завершении процессов; время можно получить при помощи функции KeQuerySystemTimePrecise, возвращающей текущее системное время (UTC, не местное время) в формате 64-разрядного целого числа, с отчетом от 1 января 1601 года. Наконец, размер элемента величина постоянная, равная размеру структуры данных, предоставляемой пользователю (а не размеру FullItem).
Функция API KeQuerySystemTimePrecise появилась в Windows 8. В более ранних версиях следует использовать функцию API KeQuerySystemTime.

Дополнительные данные при завершении процесса состоят из идентификатора процесса. В коде используется функция HandleToULong для корректного преобразования объекта HANDLE в 32-разрядное целое без знака.

А теперь остается добавить новый элемент в конец связного списка. Для этого мы определим функцию с именем PushItem:

void PushItem(LIST_ENTRY* entry) {    AutoLock<FastMutex> lock(g_Globals.Mutex);    if (g_Globals.ItemCount > 1024) {       // Слишком много элементов, удалить самый старый       auto head = RemoveHeadList(&g_Globals.ItemsHead);       g_Globals.ItemCount--;       auto item = CONTAINING_RECORD(head, FullItem<ItemHeader>, Entry);       ExFreePool(item);     }     InsertTailList(&g_Globals.ItemsHead, entry);     g_Globals.ItemCount++;}

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

Кроме того, драйвер ограничивает количество элементов связного списка. Такая предосторожность необходима, потому что ничто не гарантирует, что клиент будет быстро потреблять эти события. Драйвер не должен допускать неограниченное потребление данных, так как это может повредить системе в целом. Значение 1024 выбрано совершенно произвольно. Правильнее было бы читать это число из раздела драйвера в реестре.
Реализуйте это ограничение с чтением из реестра в DriverEntry. Подсказка: используйте такие функции API, как ZwOpenKey или IoOpenDeviceRegistryKey, а также ZwQueryValueKey.

Если счетчик элементов превысил максимальное значение, самый старый элемент удаляется; фактически связанный список рассматривается как очередь (RemoveHeadList). При освобождении элемента его память должна быть освобождена. Указателем на элемент не обязательно должен быть указатель, изначально использованный для выделения памяти (хотя в данном случае это так, потому что объект LIST_ENTRY стоит на первом месте в структуре FullItem<>), поэтому для получения начального адреса объекта FullItem<> используется макрос CONTAINING_RECORD. Теперь элемент можно освободить вызовом ExFreePool.

На рис. 8.2 изображена структура объектов FullItem.

image


Наконец, драйвер вызывает InsertTailList, чтобы добавить элемент в конец списка, а счетчик элементов увеличивается на 1.
Использовать атомарные операции инкремента/декремента в функции PushItem не обязательно, потому что операции со счетчиком элементов всегда выполняются под защитой быстрого мьютекса.

Обработка уведомлений о создании процессов


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

struct ProcessCreateInfo : ItemHeader {    ULONG ProcessId;    ULONG ParentProcessId;    WCHAR CommandLine[1024];};

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

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

А можно ли использовать решение следующего вида:

struct ProcessCreateInfo : ItemHeader {    ULONG ProcessId;    ULONG ParentProcessId;    UNICODE_STRING CommandLine; // Будет работать?};

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

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

struct ProcessCreateInfo : ItemHeader {     ULONG ProcessId;     ULONG ParentProcessId;     USHORT CommandLineLength;     USHORT CommandLineOffset;};

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

С таким объявлением можно приступить к построению реализации для создания процесса:

USHORT allocSize = sizeof(FullItem<ProcessCreateInfo>);USHORT commandLineSize = 0;if (CreateInfo->CommandLine) {    commandLineSize = CreateInfo->CommandLine->Length;    allocSize += commandLineSize;}auto info = (FullItem<ProcessCreateInfo>*)ExAllocatePoolWithTag(PagedPool,    allocSize, DRIVER_TAG);if (info == nullptr) {    KdPrint((DRIVER_PREFIX "failed allocation\n"));return;}

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

auto& item = info->Data;KeQuerySystemTimePrecise(&item.Time);item.Type = ItemType::ProcessCreate;item.Size = sizeof(ProcessCreateInfo) + commandLineSize;item.ProcessId = HandleToULong(ProcessId);item.ParentProcessId = HandleToULong(CreateInfo->ParentProcessId);

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

Затем необходимо скопировать командную строку по адресу за базовой структурой, а также обновить длину и смещение:

if (commandLineSize > 0) {    ::memcpy((UCHAR*)&item + sizeof(item), CreateInfo->CommandLine->Buffer,        commandLineSize);    item.CommandLineLength = commandLineSize / sizeof(WCHAR); // Длина в WCHAR    item.CommandLineOffset = sizeof(item);}else {    item.CommandLineLength = 0;}PushItem(&info->Entry);

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

Передача данных в пользовательский режим


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

Начнем обработку запроса чтения с получения адреса пользовательского буфера с применением прямого ввода/вывода (настраивается в DriverEntry):

NTSTATUS SysMonRead(PDEVICE_OBJECT, PIRP Irp) {    auto stack = IoGetCurrentIrpStackLocation(Irp);    auto len = stack->Parameters.Read.Length;    auto status = STATUS_SUCCESS;    auto count = 0;    NT_ASSERT(Irp->MdlAddress); // Используем прямой ввод/вывод    auto buffer = (UCHAR*)MmGetSystemAddressForMdlSafe(Irp->MdlAddress,        NormalPagePriority);     if (!buffer) {        status = STATUS_INSUFFICIENT_RESOURCES;}else {

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

AutoLock lock(g_Globals.Mutex); // C++ 17while (true) {    if (IsListEmpty(&g_Globals.ItemsHead)) // также можно проверить                                       // g_Globals.ItemCount        break;    auto entry = RemoveHeadList(&g_Globals.ItemsHead);    auto info = CONTAINING_RECORD(entry, FullItem<ItemHeader>, Entry);    auto size = info->Data.Size;    if (len < size) {        // Пользовательский буфер заполнен, вставить элемент обратно        InsertHeadList(&g_Globals.ItemsHead, entry);        break;    }    g_Globals.ItemCount--;    ::memcpy(buffer, &info->Data, size);    len -= size;    buffer += size;    count += size;    // Освободить данные после копирования    ExFreePool(info);}

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

Наконец, запрос завершается с текущим статусом, а в поле Information сохраняется значение переменной count:

Irp->IoStatus.Status = status;Irp->IoStatus.Information = count;IoCompleteRequest(Irp, 0);return status;

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

void SysMonUnload(PDRIVER_OBJECT DriverObject) {    // Отмена регистрации уведомлений процессов    PsSetCreateProcessNotifyRoutineEx(OnProcessNotify, TRUE);    UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\sysmon");    IoDeleteSymbolicLink(&symLink);    IoDeleteDevice(DriverObject->DeviceObject);    // Освобождение оставшихся элементов    while (!IsListEmpty(&g_Globals.ItemsHead)) {       auto entry = RemoveHeadList(&g_Globals.ItemsHead);       ExFreePool(CONTAINING_RECORD(entry, FullItem<ItemHeader>, Entry));}}

Клиент пользовательского режима


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

Функция main вызывает ReadFile в цикле с небольшой приостановкой, чтобы поток не потреблял ресурсы процессора постоянно. Поступившие данные отправляются для вывода:

int main() {    auto hFile = ::CreateFile(L"\\\\.\\SysMon", GENERIC_READ, 0,         nullptr, OPEN_EXISTING, 0, nullptr);    if (hFile == INVALID_HANDLE_VALUE)         return Error("Failed to open file");    BYTE buffer[1 << 16]; // 64-килобайтный буфер    while (true) {         DWORD bytes;         if (!::ReadFile(hFile, buffer, sizeof(buffer), &bytes, nullptr))            return Error("Failed to read");         if (bytes != 0)            DisplayInfo(buffer, bytes);         ::Sleep(200);     }}

Функция DisplayInfo должна разобраться в структуре полученного буфера. Так как все события начинаются с общего заголовка, функция различает события по значению ItemType. После того как событие будет обработано, поле Size в заголовке указывает, где начинается следующее событие:

void DisplayInfo(BYTE* buffer, DWORD size) {    auto count = size;    while (count > 0) {        auto header = (ItemHeader*)buffer;        switch (header->Type) {        case ItemType::ProcessExit:        {             DisplayTime(header->Time);             auto info = (ProcessExitInfo*)buffer;             printf("Process %d Exited\n", info->ProcessId);             break;         }         case ItemType::ProcessCreate:         {              DisplayTime(header->Time);              auto info = (ProcessCreateInfo*)buffer;              std::wstring commandline((WCHAR*)(buffer +                                           info->CommandLineOffset),                     info->CommandLineLength);               printf("Process %d Created. Command line: %ws\n",                                               info->ProcessId,                      commandline.c_str());                break;           }           default:               break;     }     buffer += header->Size;     count -= header->Size;   }}

Для правильного извлечения командной строки в коде используется конструктор класса C++ wstring, который может построить строку по указателю и длине строки. Вспомогательная функция DisplayTime форматирует время в виде, удобном для чтения:

void DisplayTime(const LARGE_INTEGER& time) {    SYSTEMTIME st;    ::FileTimeToSystemTime((FILETIME*)&time, &st);    printf("%02d:%02d:%02d.%03d: ",           st.wHour, st.wMinute, st.wSecond, st.wMilliseconds);}

Драйвер устанавливается и запускается так, как было описано в главе 4.

sc create sysmon type= kernel binPath= C:\Book\SysMon.syssc start sysmon

Пример вывода, полученного при запуске SysMonClient.exe:

C:\Book>SysMonClient.exe12:06:24.747: Process 13000 Exited12:06:31.032: Process 7484 Created. Command line: SysMonClient.exe12:06:42.461: Process 3128 Exited12:06:42.462: Process 7936 Exited12:06:42.474: Process 12320 Created. Command line: "C:\$WINDOWS.~BT\                                                    Sources\mighost.\exe" {5152EFE5-97CA-4DE6-BBD2-4F6ECE2ABD7A} /InitDoneEvent:MigHost.                                                    {5152EFE5-97CA-4D\E6-BBD2-4F6ECE2ABD7A}.Event /ParentPID:11908 /LogDir:"C:\$WINDOWS.~BT\Sources\                                                     Panthe\r"12:06:42.485: Process 12796 Created. Command line: \??\C:\WINDOWS\system32\                                                     conhost.e\xe 0xffffffff -ForceV112:07:09.575: Process 6784 Created. Command line: "C:\WINDOWS\system32\cmd.exe"12:07:09.590: Process 7248 Created. Command line: \??\C:\WINDOWS\system32\                                                     conhost.ex\e 0xffffffff -ForceV112:07:11.387: Process 7832 Exited12:07:12.034: Process 2112 Created. Command line: C:\WINDOWS\system32\                                                     ApplicationFra\meHost.exe -Embedding12:07:12.041: Process 5276 Created. Command line: "C:\Windows\SystemApps\                                                     Microsoft.M\icrosoftEdge_8wekyb3d8bbwe\MicrosoftEdge.exe" -ServerName:MicrosoftEdge.                                                     AppXdnhjhccw\3zf0j06tkg3jtqr00qdm0khc.mca12:07:12.624: Process 2076 Created. Command line: C:\WINDOWS\system32\                                                     DllHost.exe /P\rocessid:{7966B4D8-4FDC-4126-A10B-39A3209AD251}12:07:12.747: Process 7080 Created. Command line: C:\WINDOWS\system32\                                                     browser_broker\.exe -Embedding12:07:13.016: Process 8972 Created. Command line: C:\WINDOWS\System32\                                                        svchost.exe -k\LocalServiceNetworkRestricted12:07:13.435: Process 12964 Created. Command line: C:\WINDOWS\system32\                                                     DllHost.exe /\Processid:{973D20D7-562D-44B9-B70B-5A0F49CCDF3F}12:07:13.554: Process 11072 Created. Command line: C:\WINDOWS\system32\                                                     Windows.WARP.\JITService.exe 7f992973-8a6d-421d-b042-6afd93a19631S-1-15-2-3624051433-2125758914-1\423191267-1740899205-1073925389-3782572162-737981194S-1-5-21-4017881901-586210945-2\666946644-1001 51612:07:14.454: Process 12516 Created. Command line: C:\Windows\System32\RuntimeBroker.exe -Embedding12:07:14.914: Process 10424 Created. Command line: C:\WINDOWS\system32\                                                    MicrosoftEdge\SH.exe SCODEF:5276 CREDAT:9730 APH:1000000000000017 JITHOST /prefetch:212:07:14.980: Process 12536 Created. Command line: "C:\Windows\System32\                                                    MicrosoftEdg\eCP.exe" -ServerName:Windows.Internal.WebRuntime.ContentProcessServer12:07:17.741: Process 7828 Created. Command line: C:\WINDOWS\system32\                                                    SearchIndexer.\exe /Embedding12:07:19.171: Process 2076 Exited12:07:30.286: Process 3036 Created. Command line: "C:\Windows\System32\                                                    MicrosoftEdge\CP.exe" -ServerName:Windows.Internal.WebRuntime.ContentProcessServer12:07:31.657: Process 9536 Exited

Уведомления потоков


Ядро предоставляет обратные вызовы создания и уничтожения потоков, аналогичные обратным вызовам процессов. Для регистрации используется функция API PsSetCreateThreadNotifyRoutine, а для ее отмены другая функция, PsRemoveCreateThreadNotifyRoutine. В аргументах функции обратного вызова передается идентификатор процесса, идентификатор потока, а также флаг создания/уничтожения потока.

Расширим существующий драйвер SysMon, чтобы он получал не только уведомления процессов, но и уведомления потоков. Начнем с добавления значений перечисления и структуры, представляющей информацию, все это добавляется в заголовочный файл SysMonCommon.h:

enum class ItemType : short {    None,    ProcessCreate,    ProcessExit,    ThreadCreate,    ThreadExit};struct ThreadCreateExitInfo : ItemHeader {    ULONG ThreadId;    ULONG ProcessId;};

Затем можно добавить вызов регистрации в DriverEntry, непосредственно за вызовом регистрации уведомлений процессов:

status = PsSetCreateThreadNotifyRoutine(OnThreadNotify);if (!NT_SUCCESS(status)) {    KdPrint((DRIVER_PREFIX "failed to set thread callbacks (status=%08X)\n", status)\);   break;}

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

void OnThreadNotify(HANDLE ProcessId, HANDLE ThreadId, BOOLEAN Create) {   auto size = sizeof(FullItem<ThreadCreateExitInfo>);   auto info = (FullItem<ThreadCreateExitInfo>*)ExAllocatePoolWithTag(PagedPool,        size, DRIVER_TAG);    if (info == nullptr) {        KdPrint((DRIVER_PREFIX "Failed to allocate memory\n"));        return;    }    auto& item = info->Data;    KeQuerySystemTimePrecise(&item.Time);    item.Size = sizeof(item);    item.Type = Create ? ItemType::ThreadCreate : ItemType::ThreadExit;    item.ProcessId = HandleToULong(ProcessId);    item.ThreadId = HandleToULong(ThreadId);    PushItem(&info->Entry);}

Большая часть кода выглядит довольно знакомо.

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

case ItemType::ThreadCreate:{    DisplayTime(header->Time);    auto info = (ThreadCreateExitInfo*)buffer;    printf("Thread %d Created in process %d\n",         info->ThreadId, info->ProcessId);    break;}case ItemType::ThreadExit:{     DisplayTime(header->Time);     auto info = (ThreadCreateExitInfo*)buffer;     printf("Thread %d Exited from process %d\n",          info->ThreadId, info->ProcessId);     break;}

Пример вывода с обновленным драйвером и клиентом:

13:06:29.631: Thread 12180 Exited from process 1197613:06:29.885: Thread 13016 Exited from process 882013:06:29.955: Thread 12532 Exited from process 856013:06:30.218: Process 12164 Created. Command line: SysMonClient.exe13:06:30.219: Thread 12004 Created in process 1216413:06:30.607: Thread 12876 Created in process 10728...13:06:33.260: Thread 4524 Exited from process 448413:06:33.260: Thread 13072 Exited from process 448413:06:33.263: Thread 12388 Exited from process 448413:06:33.264: Process 4484 Exited13:06:33.264: Thread 4960 Exited from process 577613:06:33.264: Thread 12660 Exited from process 577613:06:33.265: Process 5776 Exited13:06:33.272: Process 2584 Created. Command line: "C:\$WINDOWS.~BT\Sources\                                                      mighost.e\xe" {CCD9805D-B15B-4550-94FB-B2AE544639BF} /InitDoneEvent:MigHost.                                                     {CCD9805D-B15B-455\0-94FB-B2AE544639BF}.Event /ParentPID:11908 /LogDir:"C:\$WINDOWS.~BT\Sources\                                                      Panther\"13:06:33.272: Thread 13272 Created in process 258413:06:33.280: Process 12120 Created. Command line: \??\C:\WINDOWS\system32\                                                               conhost.e\xe 0xffffffff -ForceV113:06:33.280: Thread 4200 Created in process 1212013:06:33.283: Thread 4400 Created in process 1212013:06:33.284: Thread 9632 Created in process 1212013:06:33.284: Thread 6064 Created in process 1212013:06:33.289: Thread 2472 Created in process 12120

Добавьте в клиент код вывода имени образа процесса при создании и завершении потока.

Уведомления о загрузке образов


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

Функция API PsSetLoadImageNotifyRoutine регистрируется для получения этих уведомлений, а функция PsRemoveImageNotifyRoutine отменяет регистрацию. Функция обратного вызова имеет следующий прототип:

typedef void (*PLOAD_IMAGE_NOTIFY_ROUTINE)(    _In_opt_ PUNICODE_STRING FullImageName,    _In_ HANDLE ProcessId, // pid, с которым связывается образ    _In_ PIMAGE_INFO ImageInfo);

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

Аргумент FullImageName не так прост. Как указывает аннотация SAL, он необязателен и может содержать NULL. Но даже если он отличен от NULL, он не всегда содержит точное имя файла образа.

Причины кроются глубоко в ядре и выходят за рамки книги. В большинстве случаев решение работает нормально, а путь использует внутренний формат NT, начинающийся с \Device\HadrdiskVolumex\ вместо c:\. Преобразование может быть выполнено разными способами. Тема более подробно рассматривается в главе 11.

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

Аргумент ImageInfo содержит дополнительную информацию об образе; его объявление выглядит так:

#define IMAGE_ADDRESSING_MODE_32BIT 3typedef struct _IMAGE_INFO {    union {      ULONG Properties;      struct {         ULONG ImageAddressingMode : 8; // Режим адресации         ULONG SystemModeImage : 1; // Образ системного режима         ULONG ImageMappedToAllPids : 1; // Образ отображается во все процессы         ULONG ExtendedInfoPresent : 1; // Доступна структура IMAGE_INFO_EX         ULONG MachineTypeMismatch : 1; // Несоответствие типа архитектуры         ULONG ImageSignatureLevel : 4; // Уровень цифровой подписи         ULONG ImageSignatureType : 3; // Тип цифровой подписи         ULONG ImagePartialMap : 1; // Не равно 0 при частичном                                                       отображении         ULONG Reserved : 12;     };   };   PVOID ImageBase;   ULONG ImageSelector;   SIZE_T ImageSize;   ULONG ImageSectionNumber;} IMAGE_INFO, *PIMAGE_INFO;

Краткая сводка важных полей структуры:

  • SystemModeImage флаг устанавливается для образа режима ядра и сбрасывается для образа пользовательского режима.
  • ImageSignatureLevel уровень цифровой подписи (Windows 8.1 и выше). См. описание констант SE_SIGNING_LEVEL_ в WDK.
  • ImageSignatureType тип сигнатуры (Windows 8.1 и выше). См. описание перечисления SE_IMAGE_SIGNATURE_TYPE в WDK.
  • ImageBase виртуальный адрес, по которому загружается образ.
  • ImageSize размер образа.
  • ExtendedInfoPresent если флаг установлен, IMAGE_INFO является частью большей структуры IMAGE_INFO_EX:

typedef struct _IMAGE_INFO_EX {    SIZE_T Size;    IMAGE_INFO ImageInfo;    struct _FILE_OBJECT *FileObject;} IMAGE_INFO_EX, *PIMAGE_INFO_EX;

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

if (ImageInfo->ExtendedInfoPresent) {    auto exinfo = CONTAINING_RECORD(ImageInfo, IMAGE_INFO_EX, ImageInfo);    // Обращение к FileObject}

В расширенной структуре добавляется всего одно осмысленное поле объект файла, используемый для управления образом. Драйвер может добавить ссылку на объект (ObReferenceObject) и использовать его в других функциях по мере надобности.
Добавьте в драйвер SysMon уведомления о загрузке образов; драйвер должен собирать информацию только для образов пользовательского режима. Клиент должен выводить путь образа, идентификатор процесса и базовый адрес образа.

Упражнения


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

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

Итоги


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

Более подробно с книгой можно ознакомиться на сайте издательства
Оглавление
Отрывок

Для Хаброжителей скидка 25% по купону Windows

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Подробнее..

Книга Совершенный софт

26.01.2021 12:18:23 | Автор: admin
image Привет, Хаброжители! Совершенный софт это проверенный, структурированный и высокотехнологичный подход к разработке программного обеспечения. Множество компаний уже используют идеи Лёве в сотнях систем, но раньше эти мысли нигде не публиковались.

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

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

Расширенные методы планирования


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

Божественные активности


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

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

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

Решение проблемы божественных активностей


Лучший план действий с божественными активностями разбиение их на меньшие независимые активности. Разбиение божественных активностей значительно улучшит качество оценки, сократит неопределенность и предоставит правильное значение риска. Но что, если объем работы будет действительно огромным? Такие активности следует рассматривать как мини-проекты и заняться их уплотнением. Начните с выявления внутренних фаз божественных активностей и поиска возможностей параллельной работы в этих фазах внутри каждой божественной активности. Если это невозможно, попробуйте сделать божественные активности менее критическими, убрав их с пути других активностей в проекте. Например, разработка имитаторов для божественных активностей снижает зависимости других активностей от самих божественных активностей. Это позволит организовать работу параллельно с божественными активностями, что сделает их менее критичными (или вообще некритичными). Имитаторы также сокращают неопределенность божественных активностей, так как устанавливаемые ими ограничения раскрывают скрытые предположения; это упрощает подробное проектирование божественных активностей.

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

Точка пересечения риска


В примере из главы 11 для включения и исключения вариантов планирования проекта использовались простые правила: риск должен быть ниже 0,75 и выше 0,3. При принятии решений о вариантах планирования можно действовать с большей точностью, не ограничивающейся простейшими правилами. На рис. 11.33 в точке минимума прямых затрат и непосредственно слева от нее кривая прямых затрат практически горизонтальна, но кривая риска имеет значительный угол наклона. Такое поведение ожидаемо, потому что кривая риска обычно достигает своего максимального значения до того, как прямые затраты достигнут своего максимума в решениях с наибольшим уплотнением. Единственная возможность достичь максимального риска до максимума прямых затрат если изначально слева от минимума прямых затрат кривая риска растет намного быстрее кривой прямых затрат. В точке максимального риска (и немного справа от нее) кривая риска горизонтальная или почти горизонтальная, тогда как кривая прямых затрат имеет достаточно крутой угол наклона.

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

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

Вычисление точки пересечения


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

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

$y = ax^3 + bx^2 + cx + d.$


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

$y' = 3ax^2 + 2bx + c.$


В нашем примере формула риска выглядит так:

$R = 0,01t^3 0,36t^2 + 3,67t 11,07.$


Первая производная риска:

$R' = 0,03t^2 0,72t + 3,67.$


Формула прямых затрат:

$C = 0,99t^2 21,32t + 136,57.$


Следовательно, первая производная прямых затрат выглядит так:

$C' = 1,98t 21,32.$


Перед сравнением формул двух производных необходимо решить две проблемы. Во-первых, диапазоны значений между максимальным риском и минимальными прямыми затратами на обеих кривых монотонно убывают (что означает, что скорости роста двух кривых будут отрицательными), поэтому сравнивать нужно абсолютные значения скоростей роста. Во-вторых, исходные скорости роста несравнимы по величине. Значения риска лежат в диапазоне от 0 до 1, а значения затрат в нашем примере равны приблизительно 30. Для корректного сравнения двух производных необходимо сначала масштабировать значения риска до значений затрат в точке максимального риска.

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

image

где:

  • tmr время достижения максимального риска;
  • R(tmr) значение формулы риска проекта в точке tmr;
  • С(tmr) значение формулы затрат проекта в точке tmr.

Кривая риска достигает максимума в точке, в которой первая производная кривой риска R' равна 0. Решая уравнение риска для t при R' = 0, получаем значение tmr, равное 8,3 месяца. Соответствующее значение риска R равно 0,85, а соответствующее значение прямых затрат 28 человеко-месяцев. Отношение двух значений F равно 32,93 это и есть коэффициент масштабирования для нашего примера.

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

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

Эти условия можно объединить в следующем выражении:

$F | R' | > |C'| .$


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

$32,93 | 0,03t^2 0,72t + 3,67 | > | 1,98t 21,32 |.$


Решение уравнения дает допустимый диапазон для t:

$9,03 < t < 12,31.$


В результате получаем не одну, а две точки пересечения 9,03 и 12,31 месяца.

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

image

Если отложить в сторону математику, существование двух точек пересечения риска связано с семантикой точек с позиций планирования проекта. В точке 9,03 месяца риск равен 0,81; в точке 12,31 месяца риск равен 0,28. Наложение этих значений на кривую риска и кривую прямых затрат на рис. 12.2 раскрывает истинный смысл точек пересечения.

image

Решения планирования проекта слева от 9,03-месячной точки пересечения риска слишком рискованны; решения справа от 12,31-месячной точки слишком безопасны. Между двумя точками пересечения риска уровень риска в самый раз.

Приемлемые риски и варианты планирования

Значения риска в точках пересечения 0,81 и 0,28 хорошо согласуются с эмпирическими порогами 0,75 и 0,30. В нашем примере зона приемлемого риска включает первое уплотненное решение, нормальное решение и точки разуплотнения D4, D3 и D2 (рис. 11.35). Все эти точки являются практичными вариантами планирования. Практичность в этом контексте означает, что обязательства по проекту будут выполнены с разумной вероятностью. Более уплотненные решения слишком рискованны, а точка D1 слишком безопасна. Вы также можете выбрать между несколькими точками разуплотнения, определив лучшую цель для разуплотнения.

Поиск цели для разуплотнения


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

Если вы построили график кривой риска, то уже видите, где находится точка перегиба, и можете выбрать точку разуплотнения в точке перегиба или, если действовать более консервативно, справа от нее. Этот метод использовался в главе 11 для того, чтобы рекомендовать D3 на рис. 11.29 в качестве цели разуплотнения. Однако простое определение положения точки на глаз вряд ли можно признать хорошей инженерной практикой. Вместо этого следует применить методы математического анализа для последовательного и объективного поиска цели разуплотнения.

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

image

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

$y = ax^3 + bx^2 + cx + d;$


$y' = 3ax^2 + 2bx + c;$


$y'' = 6ax + 2b. $


Приравнивая вторую производную к нулю, получаем следующую формулу:

image

Так как модель риска выглядит так:

$R = 0,01t^3 0,36t^2 + 3,67t 11,07,$


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

image

В точке 10,62 месяца значение риска равно 0,55, что только на 10% отличается от идеальной цели 0,5. При нанесении на дискретные кривые риска на рис. 12.4 становится видно, что это значение находится в диапазоне между D4 и D3; таким образом обосновывается выбор D3 в качестве цели разуплотнения в главе 11.

image

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

Геометрический риск


Все модели риска, представленные в главе 10, используют для вычисления риска разновидность среднего арифметического временных резервов. К сожалению, среднее арифметическое плохо справляется с неравномерными распределениями значений. Возьмем последовательность [1, 2, 3, 1000]. Среднее арифметическое этой последовательности равно 252, что совершенно не типично для имеющихся значений. Такое поведение проявляется не только при вычислении рисков, а любые попытки использования среднего арифметического для крайне неравномерного распределения приводят к неудовлетворительному результату. В таком случае лучше использовать среднее геометрическое вместо среднего арифметического.

Среднее геометрическое для последовательности значений вычисляется умножением всех n значений последовательности и последующим извлечением корня n-й степени из произведения. Для последовательности значений a1 an среднее геометрическое последовательности вычисляется по следующей формуле:

image

Например, хотя среднее арифметическое последовательности [2, 4, 6] равно 4, среднее геометрическое равно 3,63:

image

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

image

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

Хотя на первый взгляд может показаться, что среднее геометрическое какая-то алгебраическая странность, оно по-настоящему проявляет себя в последовательностях с неравномерным распределением значений. При вычислении среднего геометрического выбросы намного меньше влияют на результат. В примере с последовательностью [1, 2, 3, 1000] среднее геометрическое равно 8,8, что гораздо лучше представляет первые три числа последовательности.

Геометрический риск возникновения критичности

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

image

где:

  • WC вес критических активностей;
  • WR вес красных активностей;
  • WY вес желтых активностей;
  • WG вес зеленых активностей;
  • NC количество критических активностей;
  • NR количество красных активностей;
  • NY количество желтых активностей;
  • NG количество зеленых активностей;
  • N количество активностей в проекте (N = NC + NR + NY + NG).

Для сети на рис. 10.4 геометрический риск возникновения критичности выглядит так:

image

Соответствующий арифметический риск возникновения критичности для той же сети равен 0,69. Как и ожидалось, геометрический риск несколько ниже арифметического.

Диапазон значений риска

Как и в случае с арифметическим риском, геометрический риск возникновения критичности имеет максимальное значение 1,0, когда все активности критичны, и минимальное значение WG/WC, когда все активности сети являются зелеными:

image

Геометрический риск Фибоначчи

Для получения геометрической модели риска Фибоначчи можно использовать отношение Фибоначчи между весами. Для следующего определения весов:

image

геометрическая формула Фибоначчи выглядит так:

image

Диапазон значений риска

Как и в случае с арифметическим риском Фибоначчи, геометрический риск Фибоначчи имеет максимальное значение 1,0, когда все активности критичны, и минимальное значение

$0,24 (^3),$

когда все активности сети являются зелеными.

Геометрический риск активности

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

Таким образом, формула геометрического риска активности имеет вид:

image

где:

  • Fi временной резерв для активности i;
  • N количество активностей в проекте;
  • M максимальный временной резерв по всем активностям в проекте, или Max(F1, F2, , FN).

Для сети на рис. 10.4 геометрический риск активности будет равен:

image

Соответствующий арифметический риск активности для той же сети будет равен 0,67.

Диапазон значений риска

Максимальное значение геометрической модели риска активности приближается к 1,0 по мере того, как все больше активностей приближается к критическому состоянию, но становится неопределенным, когда критическими становятся все активности. Геометрический риск активности имеет минимальное значение 0, когда временной резерв всех активностей одинаков. В отличие от арифметического риска активности, с геометрическим риском активности нет необходимости корректировать выбросы с аномально высоким временным резервом, и временные резервы могут не иметь равномерного распределения.

Об авторе


Джувел Лёве является ведущим архитектором ПО, специализирующимся на разработке систем и проектов.

Помог множеству компаний научиться поставлять качественный софт в соответствии с графиком и в рамках бюджета. Microsoft признает его ведущим мировым экспертом и лидером отрасли. Лёве привлекают для подготовки внутренних обзоров дизайна для C#, WCF и связанных технологий, его называют легендой программного обеспечения.

Более подробно с книгой можно ознакомиться на сайте издательства
Оглавление
Отрывок

Для Хаброжителей скидка 25% по купону Софт

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Подробнее..

Издательство Питер. Колонка редактора

03.06.2021 12:19:11 | Автор: admin
image

Привет, Хаброжители! Предлагаем ознакомиться с краткими обзорами сданных в типографию новинок.

image

JavaScript для глубокого обучения: TensorFlow.js


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

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

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

В этой книге:

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

Оформить предзаказ.

image

Делай как в Google. Разработка программного обеспечения


Автор(ы): Титус Винтерс, Том Маншрек, Хайрам Райт

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

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

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

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

Оформить предзаказ.

image

Программируем на C# 8.0. Разработка приложений


Автор(ы): Иэн Гриффитс

C# универсальный язык, который может практически всё! Иэн Гриффитс рассказывает о его возможностях с точки зрения разработчика, перед которым стоит задача быстро и эффективно создавать приложения любой сложности.

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

В этой книге вы:

  • Узнаете, как C# поддерживает классы, настраиваемые типы, коллекции и обработку ошибок.
  • Сможете создавать высокопроизводительный код с эффективным использованием памяти с помощью типов Span<Т> и Memory<Т>.
  • Научитесь запрашивать и работать с объектными моделями, базами, потоками данных и XML документами.
  • Примените многопоточность, чтобы задействовать всю мощь параллельной обработки.
  • Узнаете, как функции асинхронного языка помогают улучшить скорость отклика и масштабируемость приложений.

Оформить предзаказ.

image

Распределенные данные. Алгоритмы работы современных систем хранения информации


Автор(ы): Алекс Петров

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

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

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

В этой книге вы углубитесь в:

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

Оформить предзаказ.

image

Безопасность контейнеров. Фундаментальный подход к защите контейнеризированных приложений


Автор(ы): Лиз Райс

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

Лиз Райс исследует вопросы построения контейнерных систем в Linux. Узнайте, что происходит при развертывании контейнеров и научитесь оценивать возможные риски для безопасности развертываемой системы. Приступайте, если используете Kubernetes или Docker и знаете базовые команды Linux.

В этой книге:

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

Оформить предзаказ.

image

Python, например


Автор(ы): Никола Лейси

Это Python, например! Познакомьтесь с самым быстрорастущим языком программирования на сегодняшний день.

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

Оформить предзаказ.
Подробнее..

Летняя распродажа

21.06.2021 12:17:59 | Автор: admin
image

Привет, Хаброжители! Стартовала летняя распродажа от издательства Питер.

image


В хронологическом порядке представлены книги, вызвавшие интерес читателей за последние 3 мес.

Отдельные категории на сайте Бестселлеры O'Reilly, Head First O'Reilly, Manning, No Starch Press, Packt Publishing, Классика Computer Science, программирование для детей, научно-популярная серия New Science.

Книги по бизнесу, психологии, детский ассортимент.

Условия акции: 21 июня27 июня, скидка 40% на все бумажные книги по купону Бумажная книга, скидка 50% на все электронные книги по купону Электронная книга
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru