Автор: Шатохина Н.А.
Для поддержания широкого диапазона функциональных возможностей управляемая
среда .NET Framework предоставляет разработчикам возможность совершенствовать
модель программирования. Задачей рекомендаций по разработке библиотек для .NET
Framework является поддержание совместимости и предсказуемости открытых членов
API вместе с обеспечением возможности Web и межъязыковой интеграции. Настойчиво
рекомендуется следовать этим рекомендациям при разработке классов и компонентов,
расширяющих возможности .NET Framework. Несовместимая разработка неблагоприятно
влияет на производительность разработчика. Инструментальные средства разработки
и дополнения могут превратить некоторые из этих рекомендаций в предписывающие
правила де факто и уменьшить количество несогласующихся компонентов.
Несогласующиеся компоненты будут функционировать, но не на полную их мощность.
Рекомендации призваны помочь разработчикам библиотеки классов решить проблему
выбора между различными решениями. Возможны ситуации, в которых для создания
хорошей библиотеки вам потребуется пренебречь этими рекомендациями разработки.
Такие случаи должны быть редки, и важно, чтобы то или иное решение было строго
обосновано. В этом разделе представлены рекомендации по присваиванию имен и
использованию типов в .NET Framework, а также рекомендации по реализации общих
схем разработки.
Общая система типов - это модель, определяющая правила, которым следует
общеязыковая среда выполнения при объявлении, использовании и управлении типами.
Общая система типов устанавливает оболочку, которая делает возможной
межъязыковую интеграцию, безопасность типов и высокопроизводительное выполнение
кода. Это сырье, из которого создаются библиотеки классов.
Общеязыковая спецификация (CLS) определяет ряд программно контролируемых
правил, которые регулируют взаимодействие типов, созданных на разных языках
программирования. Нацеливание на CLS - прекрасный способ обеспечения возможности
межъязыкового взаимодействия. Разработчики управляемой библиотеки классов могут
использовать CLS, чтобы гарантировать возможность работы их API с различными
языками программирования. Обратите внимание, что хотя CLS и способствует
разработке хорошей библиотеки, она не гарантирует ее. Более подробно смотрите в
разделе MSDN Writing CLS-Compliant Code.
При выборе того, какие свойства включать в библиотеку классов, вы должны
следовать двум основополагающим принципам по отношению к CLS:
Чтобы обеспечить возможность писать любую управляемую библиотеку, CLS
должна быть достаточно богатой. Однако, если вы обеспечиваете множество
способов решения одной и той же задачи, пользователи вашей библиотеки классов
могут запутаться при выборе пути разработки и использования. Например,
предлагая одновременно безопасные и небезопасные структурные компоненты вы
ставите пользователя перед выбором, которые из них использовать. Поэтому, CLS
способствует корректному использованию, предлагая только структурные
компоненты с безопасными типами. Всем языкам программирования понадобиться некоторая модификация для того,
чтобы нацелить их на время выполнения и общую систему типов. Однако, чтобы
сделать язык CLS-совместимым, разработчикам не понадобиться делать большое
количество дополнительно работы. Цель CLS - быть насколько возможно маленькой,
предлагая богатый выбор типов данных и свойств. Последовательная схема присваивания имен является одним из наиболее важных
элементов предсказуемости и ясности в управляемой библиотеке классов. Широкое
использование и понимание этих принципов должно устранить многие из наиболее
часто возникающих вопросов пользователей. В этом разделе представлены принципы
присваивания имен для типов .NET Framework. Для каждого типа также необходимо
обратить внимание на некоторые общие правила, касающиеся применения заглавных
букв, чувствительности к регистру и выбора слова.
При использовании заглавных букв в идентификаторах пользуйтесь следующими
тремя соглашениями.
Pascal нотация
Первая буква в идентификаторе и первая буква каждого последующего
соединенного слова заглавные. Вы можете использовать стиль Pascal для
идентификаторов, состоящих из трех и более символов. Например: Camel нотация
Первая буква идентификатора в нижнем регистре, а первая буква каждого
последующего соединенного слова заглавная. Например: Врехний регистр
Все буквы идентификатора заглавные. Используйте нотацию только для
идентификаторов, состоящих из двух и менее букв. Например: Вы также должны использовать заглавные буквы в идентификаторах для того,
чтобы сохранять совместимость с существующими символьными схемами, в которых
символы верхнего регистра часто используются для перечислений и постоянных
значений. В общем, эти символы не должны быть видимыми вне сборки, использующей
их.
В следующей таблице собраны правила применения заглавных букв и приведены
примеры различных типов идентификаторов.
Чтобы избежать неразберихи и гарантировать возможность межъязыкового
взаимодействия, следуйте этим правилам, касающимся использования
чувствительности к регистру:
Чтобы избежать неразберихи и гарантировать возможность межъязыкового
взаимодействия, при использовании аббревиатур следуйте правилам:
Избегайте использования имен классов, которые дублируют общепринятые
пространства имен .NET Framework. Например, не используйте в качестве имен
классов следующие имена: System, Collections, Forms или UI. Список имен,
используемых в .NET Framework, приведен в разделе Class Library.
Кроме того, избегайте использования идентификаторов, конфликтующих со
следующими ключевыми словами:
Различные языки программирования используют разные термины для обозначения
фундаментальных управляемых типов. Разработчики библиотеки классов должны
избегать использования терминологии, специфической для конкретного языка. Для
предотвращения неразберихи с именами типов, следуйте правилам, приведенным в
этом разделе.
Используйте имена, описывающие значение типа, а не тип. В редких случаях,
когда тип параметра не имеет семантического значения, используйте общее имя.
Например, класс, поддерживающий запись различных типов данных в поток, может
иметь следующие методы. Не создавайте язык-специфические имена методов, как в следующем примере. В чрезвычайно редких случаях, когда необходимо создать метод с уникальным
именем для каждого основного типа данных, используйте универсальные имена типов.
В следующей таблице приведены основные имена типов данных и их универсальные
замещения.
Например, класс, поддерживающий чтение различных типов данных из потока,
может иметь следующие методы. Использование имен из предыдущего примера более предпочтительно, чем
использование следующих язык-специфических имен: Общим правилом при присваивании имен пространствам имен является
использование сочетания имени компании, названия технологии, feature и design
(необязательно). Например: Использование имени компании или другой твердо установившейся марки в
качестве префикса в имени пространства имен предохраняет от возможности
появления двух пространств имен с одинаковыми именами. Например,
Microsoft.Office - префикс, соответствующий классам Office Automation,
поставляемым компанией Microsoft.
Используйте неизменное, узнаваемое название технологии на втором уровне
иерархического имени. Используйте иерархию организации, как основу для иерархии
пространства имен. Добавьте к имени пространства имен, содержащего типы, которые
обеспечивают функциональность времени разработки для основного пространства
имен, суффикс .Design. Например, пространство имен System.Windows.Forms.Design
содержит дизайнеры и классы, используемые для разработки приложений, основанных
на System.Windows.Forms.
Вложенные пространства имен должны иметь зависимость от типов содержащего их
пространства имен. Например, классы в System.Web.UI.Design зависят от классов в
System.Web.UI. Однако классы в System.Web.UI не зависят от классов в
System.UI.Design.
Вы должны использовать Pascal нотацию для пространств имен и отдельные
логические компоненты с точками, например, как Microsoft.Office.PowerPoint. Если
ваша торговая марка использует нетрадиционную нотацию, используйте эту нотацию,
даже если она отклоняется от предписанного к использованию Pascal нотации.
Например, пространства имен NeXT.WebObjects и ee.cummings иллюстрируют
соответствующие отклонения от правил Pascal нотации.
Используйте имена пространства имен в множественном числе, если это
семантически подходит. Например, лучше использовать имя System.Collections, чем
System.Collection. Исключением из этого правила являются имена торговых марок и
аббревиатуры. Например, лучше использовать System.IO, а не System.IOs.
Не используйте одинаковые имена для класса и пространства имен. Например, не
создавайте и пространство имен Debug, и класс Debug.
И в заключение, обратите внимание, что имя пространства имен не обязательно
должно дублировать имя сборки. Например, если вы называете сборку
MyCompany.MyTechnology.dll, это не значит что она должна содержать пространство
имен MyCompany.MyTechnology.
Вот основные рекомендации по присваиванию имен классам:
Далее приведены примеры правильно названных классов. Здесь приведены основные рекомендации по присваиванию имен интерфейсам:
Далее приведены примеры корректных имен интерфейсов. Следующий пример кода демонстрирует, как определить интерфейс IComponent и
его стандартную реализацию, класс Component. Вы всегда должны добавлять суффикс Attribute к именам специальных классов
атрибутов. Ниже приведен пример корректного имени класса атрибута. Перечисляемый тип наследуется от класса Enum. Вот основные рекомендации по
присваиванию имен перечисляемым типам:
При присваивании имен статическим полям руководствуйтесь следующими
рекомендациями:
Далее приведены основные рекомендации по присваиванию имен параметрам:
Далее приведены примеры корректных имен параметров. Основные рекомендации по присваиванию имен методам следующие:
Далее приведены примеры корректных имен методов. Основные рекомендации по присваиванию имен свойствам следующие:
В следующем примере кода продемонстрировано корректное присваивание имен
свойствам. В следующем примере кода приведено свойство, имеющее такое же имя, как и имя
его типа. Далее следует пример неправильного присваивания имени , потому что свойству
типа Integer присвоено имя Color. В последнем примере невозможно обратиться к членам перечисления Color.
Color.Xxx будет интерпретировано, как обращение к члену, который сначала
получает значение свойства Color (типа Integer в Visual Basic или типа int в
C#), а затем обращается к члену, имеющему такое значением (который должен быть
отдельным членом System.Int32).
Ниже приведены основные рекомендации по присваиванию имен событиям:
Следующий пример иллюстрирует обработчик события с соответствующими именем и
параметрами. Следующий пример иллюстрирует класс аргумента события с корректным именем. В этом разделе предложены рекомендации по использованию членов класса в
библиотеках классов.
Определите, что больше подходит для осуществления ваших требований, свойство
или метод. Более подробно о различиях между свойствами и методами рассказано в
части Свойства и методы этого раздела.
Выберите имя для свойства на основе рекомендаций, приведенных в разделе
Принципы присваивания имен свойствам. Не создавайте свойство с таким же именем,
как и у существующего типа, это обусловливает неоднозначности в некоторых языках
программирования. Например, System.Windows.Forms.Control имеет свойство цвета.
Т.к. также существует и структура Color, свойство цвета в
System.Windows.Forms.Control названо BackColor. Это имя более выразительное и не
конфликтует с именем структуры Color.
Возможны ситуации, в которых вам придется нарушить это правило. Например,
класс System.Windows.Forms.Form содержит свойство Icon несмотря на то, что в
.NET Framework также существует класс Icon. Это имя использовано потому, что
Form.Icon более открытое и понятное имя для свойства, чем Form.FormIcon или
Form.DisplayIcon.
При организации доступа к свойству через аксессор set, сохраните значение
свойства до того, как измените его. Это обеспечит сохранение данных в случае,
если в аксессоре set возникнет исключительная ситуация.
Вопросы состояния свойства
Разрешено определение свойств в любом порядке. Свойства не должны иметь
состояния в зависимости от других свойств. Очень распространена ситуация, когда
отдельное свойство объекта не вступает в силу до тех пор, пока разработчик не
определит определенный набор свойств, или до тех пор, пока объект находится в
определенном состоянии. Пока объект в соответствующем состоянии, свойство
неактивно. Как только объект меняет состояние, свойство автоматически
активируется без необходимости открытого запроса. Семантика аналогична
независимо от порядка, в котором разработчик определяет значения свойств, или от
того, как разработчик переводит объект в активное состояние.
Например, элемент управления TextBox может иметь два взаимосвязанных
свойства: DataSource и DataField. DataSource определяет имя таблицы, а DataField
- имя столбца. Как только оба свойства определены, элемент управления может
автоматически связывать данные таблицы со свойством Text. Следующий пример кода
иллюстрирует свойства, которые могут задаваться в любом порядке: Вы можете определить свойства DataSource и DataField в любом порядке. Таким
образом, следующий код эквивалентен предыдущему: Можно также присвоить свойству значение null (Nothing в Visual Basic), чтобы
обозначить, что значение не определено. Следующий пример иллюстрирует, как отслеживать состояние способности
связывания данных и автоматически активировать или деактивировать его в
соответствующее время. В предыдущем примере данное выражение определяет, находится ли объект в
состоянии, в котором способность связывания данных может самоактивироваться: Вы делаете активацию автоматической, создавая метод, который определяет
возможность активации объекта, исходя из его текущего состояния, и затем
активирует его, если это необходимо. Если у вас есть взаимосвязанные свойства, такие как DataSource и DataMember,
вы должны реализовать интерфейс ISupportInitialize. Это позволит разработчику
(или пользователю) вызывать методы ISupportInitialize.BeginInit и
ISupportInitialize.EndInit при определении множества свойств, чтобы дать
возможность компоненту предоставлять оптимизации. В примере, приведенном выше,
ISupportInitialize может предотвратить ненужные попытки доступа к базе данных до
тех пор пока установка не завершена корректно.
Выражение, появляющееся в этом методе, обозначает, какие части модели объекта
должны быть проверены, чтобы осуществлять эти переходы из одного состояния в
другое. В этом случае оказывают влияния свойства DataSource и DataField. Более
подробная информации о выборе между свойствами и методами представлена в разделе
Свойства и методы.
Вызов событий изменения свойства
События изменения свойств должны вызываться в компонентах, если надо
уведомлять потребителей о программном изменении свойства компонента. По
соглашению о присваивании имен имена событий изменения свойства формируются из
имени свойства и суффикса Changed, как TextChanged. Например, элемент управления
вызывает событие TextChanged, когда изменяется его свойство text. Чтобы вызвать
это событие, можно использовать protected вспомогательную процедуру
Raise<Property>Changed. Однако, возможно, не стоит вызывать событие
изменения свойства для добавления элемента хэш-таблицы, т.к. это влечет за собой
лишние затраты производительности. Следующий пример иллюстрирует реализацию
вспомогательной процедуры для события произошедшего изменения свойства. При связывании данных этот шаблон используется для того, чтобы обеспечить
возможность двухстороннего связывания свойства. Без событий
<Property>Changed и Raise<Property>Changed связывание данных
работает в одном направлении: если изменяется база данных, свойство обновляется.
Каждое свойство, вызывающее событие <Property>Changed, должно
предоставлять метаданные для подтверждения того, что свойство поддерживает
связывание данных.
Рекомендуется вызывать события происходящего/произошедшего изменения, если
значение свойства изменяется в результате внешних вмешательств. Эти события
показывают разработчику, что значение свойства изменяется или изменилось в
результате операции, а не из-за вызова методов объекта.
Хорошим примером в данном случае является свойство Text элемента управления
Edit. По мере того как пользователь записывает информацию в элемент управления,
значение свойства автоматически изменяется. Событие вызывается до того, как
значение свойства изменилось. Оно не передает старое или новое значение, и
разработчик может отменить событие, сформировав исключительную ситуацию. Имя
события состоит из имени свойства и суффикса Changing. Далее приведен пример
события происходящего изменения. Событие также вызывается после того, как значение свойства уже изменилось.
Это событие не может быть отменено. Имя события формируется из имени свойства и
суффикса Changed. Также должно вызываться характерное событие PropertyChanged.
Шаблоном для вызова этих событий является вызов особого события из метода
OnPropertyChanged. Следующий пример иллюстрирует использование метода
OnPropertyChanged. Возможны ситуации, когда базовое значение свойства не сохраняется как поле,
что осложняет отслеживание изменений значения. При вызове события происходящего
изменения найдите все места, на которые окажет влияние изменение значения
события, и обеспечьте возможность отменить событие. Например, предыдущий пример
элемента управления Edit не вполне правильный, потому что значение Text в
действительности сохраняется в описателе окна (HWND). Для вызова события
TextChanging вы должны проверить сообщения Windows, чтобы определить, когда
может измениться текст, и предусмотреть появление исключительной ситуации в
OnTextChanging, чтобы отменить событие. Если обеспечить событие происходящего
изменения слишком сложно, разумно будет поддерживать только событие
произошедшего изменения.
Свойства и методы
Разработчики библиотеки классов зачастую должны принимать решение о том, как
реализовать член класса, как свойство или как метод. Следующие рекомендации
помогут вам сделать правильный выбор.
Следующий пример иллюстрирует правильное применение свойств и методов. Свойства только для чтения и только для записи
Вы должны использовать свойства только для чтения тогда, когда пользователь
не может изменять логические члены данных свойства. Не используйте свойства
только для записи.
Использование индексированных свойств
Следующие правила описывают основные принципы использования индексированных
свойств:
Здесь приведены основные рекомендации по использованию событий:
Наследуемый класс может не вызывать базовый класс во время обработки
OnEventName. Будьте готовы к этому и не включайте никакие процессы обработки
данных в метод OnEventName в базовом классе. Это требуется для корректной
работы базового класса. Обратите внимание, что в данном случае не возникает сообщения об ошибке.
Метка является свойством только для чтения.
Событие CancelEvent не подходит в тех случаях, когда разработчик хочет
иметь возможность отменять операцию и возвращать исключительную ситуацию. В
таких случаях событие не наследуется от CancelEvent. Вы должны вызвать
исключительную ситуацию внутри обработчика события, для того чтобы отменить.
Например, пользователь может пожелать записать логику проверки правильности в
элемент управления Еdit, как показано ниже. Здесь приведены основные рекомендации по использованию методов:
Рекомендации по перезагрузке методов
Перезагрузка метода происходит тогда, когда класс содержит два метода с
одинаковыми именами, но разными сигнатурами. В этом разделе приведены некоторые
принципы использования перезагруженных методов.
Обратите внимание, что только тот метод в группе, который должен быть
виртуальным, имеет больше всего параметров. Методы с переменным количеством аргументов
Вы, возможно, захотите создать метод, имеющий переменное количество
аргументов. Классическим примером этого является метод printf в языке
программирования С. В управляемых библиотеках классов для этого структурного
компонента используйте ключевое слово params (ParamArray в Visual Basic).
Например, вместо нескольких перезагружаемых методов используйте следующий код: Следующие правила описывают основные принципы использования конструкторов:
Вот основные рекомендации по использованию полей:
Далее приведены основные рекомендации по использованию параметров:
Обратите внимание, что действительная проверка не обязательно должна пройти
в самом public или protected методе. Она может иметь место на более низком
уровне в private методах. Типы - это элементы инкапсуляции в общеязыковой среде выполнения. Детальное
описание полного списка типов данных, поддерживаемых средой выполнения,
приведено в разделе MSDN Common Type System. В этом разделе представлены
рекомендации по использованию основных типов.
Класс - это наиболее общий вид типа. Класс может быть абстрактным или sealed.
Чтобы предоставить реализацию, абстрактному классу нужен наследуемый класс. От
sealed класса наследоваться невозможно. Рекомендуется использовать классы через
другие типы.
Базовые классы являются удобным способом сгруппировать объекты, которые
совместно используют ряд функциональных возможностей. Базовые классы
предоставляют набор функциональных возможностей по умолчанию, а также
поддерживают создание специального программного продукта через расширение этих
возможностей.
Добавляйте расширяемость и полиформизм в свой проект, только если имеете
четкий сценарий заказчика. Например, обеспечить интерфейс для адаптеров данных
сложно и это не приносит реальной пользы. Разработчикам все еще придется
программировать специально для каждого адаптера, т.е. от предоставления
интерфейса есть только незначительная выгода. Однако вы должны поддерживать
совместимость между всеми адаптерами. Хотя интерфейс или абстрактный класс в
данной ситуации не подходят, очень важно обеспечить совместимую модель. Вы
можете предоставить совместимую модель разработчикам в базовых классах. Следуйте
этим рекомендациям при разработке базовых классов.
Сравнение базовых классов и интерфейсов
Тип интерфейса - это частичное описание значения, потенциально поддерживаемое
многими объектными типами. Везде, где это возможно, используйте базовые классы
вместо интерфейсов. С точки зрения поддержки версий, классы более гибкие, чем
интерфейсы. Используя классы, можно выпустить Версию 1.0, а затем в Версии 2.0
добавить в класс новый метод. Поскольку метод не абстрактен, любой существующий
дочерний класс продолжает функционировать без изменений.
Т.к. интерфейсы не поддерживают наследование реализации, модель, применяемая
к классам, неприменима к интерфейсам. Добавление метода к интерфейсу
эквивалентно добавлению абстрактного метода в базовый класс; любой класс,
реализующий интерфейс, разрушится, потому что класс не реализовывает новый
метод.
Интерфейсы подходят для следующих ситуаций:
Во всех других ситуациях наилучшей моделью является наследование классов.
Protected методы и конструкторы
Обеспечивают создание произвольных классов через protected методы. Public
интерфейс базового класса должен предоставлять потребителю класса богатый набор
функциональных возможностей. Однако для обеспечения этого богатого набора
функциональных возможностей пользователи класса часто стремятся реализовать как
можно меньшее число методов. Для достижения этой цели, предоставьте ряд
невиртуальных или final public методов, которые вызывают единственный protected
метод, обеспечивающий реализации методов. Этот метод должен быть отмечен
суффиксом Impl. Этот процесс продемонстрирован в следующем примере кода. Многие компиляторы, если вы этого не сделаете, вставят конструкторы public
или protected. Поэтому для улучшения документирования и читабельности исходного
кода, необходимо явно определить protected конструктор во всех абстрактных
классах.
Рекомендации по использованию sealed классов
Далее приведены основные рекомендации по использованию sealed классов:
Тип значения описывает значение, которое представлено как последовательность
битов, сохраненных в стеке. Описание всех встроенных типов данных .NET Framework
приведено в разделе MSDN Value Types. В этом разделе представлены рекомендации
по использованию типов значений структуры (struct) и перечисления (enum).
Рекомендации по использованию структур
Рекомендуется использовать struct для типов, имеющих любой из
нижеперечисленных признаков:
В следующем примере показана корректно определенная структура. При использовании struct не предоставляйте конструктор по умолчанию. Среда
выполнения добавит конструктор, который устанавливает все значения в нулевое
состояние. Это дает возможность более эффективно создавать массивы структур. Вы
также должны разрешить состояние, в котором данные экземпляра устанавливаются в
нуль (zero), false или null (соответственно) без вызова конструктора.
Рекомендации по применению Enum
Далее приведены рекомендации по применению перечислений:
Примечание: исключением из этого правила является инкапсуляция Win32
API. Общепринятым является иметь внутренние определения, которые следуют из
объявлений Win32. Вы можете оставить неизменным стиль Win32, обычно это
использование всех заглавных букв. Делагаты - это мощный инструментарий, обеспечивающий возможность разработчику
объектной модели управляемого кода инкапсулировать вызовы методов. Делегаты
полезны для нотификаций событий и функций обратного вызова.
Нотификации событий
Используйте соответствующую схему разработки события для событий даже в том
случае, если событие не связано с пользовательским интерфейсом. Подробнее об
использовании событий смотри в разделе Рекомендации по использованию событий.
Функции обратного вызова
Функции обратного вызова передаются методу таким образом, что код
пользователя может многократно вызываться во время выполнения, чтобы обеспечить
гибкость. Передача функции обратного вызова Compare в процедуру сортировки
является классическим примером использования функций обратного вызова. Эти
методы должны использовать соглашение функций обратного вызова, описанные в
разделе MSDN Использование функций обратного вызова (Callback Function Usage).
В имени функции обратного вызова используйте суффикс Callback.
.NET Framework дает возможность разработчикам создавать новые виды
декларативной информации, определять декларативную информацию для различных
сущностей программы и извлекать информацию атрибута в среде времени выполнения.
Например, оболочка может определить атрибут HelpAttribute, который можно
поместить в элементы программы, такие как классы и методы, чтобы обеспечить
преобразование от элементов программы к их документации. Новые виды
декларативной информации определяются через декларацию классов атрибутов,
которые могут иметь позиционные и поименованные параметры. Более подробная
информация об атрибутах приведена в разделе MSDN Создание специальных атрибутов
(Writing Custom Attributes).
В следующих правилах обозначены основные рекомендации по использованию
классов атрибутов:
Вложенные типы - это типы, определенные в контексте другого типа. Вложенные
типы очень полезны для инкапсулирования деталей реализации типа, например,
энумератор в коллекции, потому что они могут иметь доступ к private состоянию.
Public вложенные типы должны использоваться редко. Используйте их только в
таких ситуациях, когда истинны оба из нижеприведенных утверждений:
В следующих примерах проиллюстрировано, как определить типы с или без
вложенных типов: Не используйте вложенные типы, если истинны следующие утверждения:
Общеязыковая среда выполнения предоставляет богатую поддержку для
взаимодействия с компонентами COM. Компонент СОМ может использоваться из
управляемого типа, и управляемый экземпляр может использоваться компонентом СОМ.
Эта поддержка является ключом для преобразования неуправляемого кода в
управляемый; однако это порождает некоторые проблемы для разработчиков библиотек
классов. Для того, чтобы полностью раскрыть управляемый тип клиенту СОМ, тип
должен раскрыть функциональность способом, поддерживаемым СОМ и контрактом
поддержания версий СОМ.
Отметьте библиотеки управляемых классов атрибутом ComVisibleAttribute, чтобы
обозначить, как клиенты СОМ могут использовать библиотеку: прямо или через
упаковщик, формирующий функциональность таким образом, чтобы клиенты могли ее
использовать.
Типы и интерфейсы, которые должны использоваться клиентами СОМ прямо,
например, расположение в неуправляемом контейнере, должны быть отмечены
атрибутом ComVisible (true). Транзитивное замыкание всех типов, к которым
обращаются раскрытые типы, должно быть явно отмечено, как ComVisible (true); в
противном случае, они будут раскрыты, как IUnknown.
Примечание: члены типа также могут быть отмечены, как ComVisible
(false); это сокращает воздействие на СОМ и, следовательно, снижает ограничения
использования управляемого типа.
Типы, отмеченные атрибутом ComVisible (true), могут раскрывать
функциональность исключительно тем способом, который поддерживается в СОМ. В
частности, СОМ не поддерживает статические методы или параметризованные
конструкторы. Для подтверждения корректности поведения, протестируйте
функциональность типа из СОМ клиентов. Убедитесь, что вы понимаете влияние
registry на то, чтобы сделать все типы cocreateable.
Передача по ссылке
Объекты, передаваемые по ссылке, являются объектами, которые могут
выполняться удаленно. Использование удаленных объектов применяется только к трем
видам типов:
При использовании передачи по ссылке следуйте этим рекомендациям:
Далее приведены основные рекомендации по формированию и обработке ошибок:
Например, имеет смысл определить FileNotFoundException, потому что
разработчик может захотеть создать отсутствующий файл. Однако FileIOException
не является тем, что обычно должно обрабатываться именно в коде. Стандартные типы исключительных ситуаций
В следующей таблице приведен список стандартных исключительных ситуаций,
предоставляемых средой выполнения, и условий, для которых вы должны создавать
наследуемый класс.
Обернутые исключительные ситуации
Это ошибки, которые возникают на том же уровне, на котором компонент должен
сформировать исключительную ситуацию, имеющую значение для конечных
пользователей. В следующем примере сообщение об ошибке предназначено для
пользователей класса TextReader, пытающимся читать из потока. Общее описание массивов и использования массивов приведено в разделе MSDN
Arrays и System.Array Class.
Сравнение массивов и коллекций
Разработчикам библиотек классов приходится принимать сложное решение о том,
когда использовать массив, а когда вернуться к коллекции. Хотя эти типы имеют
сходные модели использования, характеристики их производительности различны. Вы
должны использовать коллекции в следующих ситуациях:
Более подробно об использовании коллекций смотри в разделе MSDN Группирование
данных в коллекции (Grouping Data in Collections).
Использование индексированных свойств в коллекциях
Вы должны использовать индексированное свойство только как члены,
используемые по умолчанию, класса коллекции или интерфейса. Не создавайте
семейств функций в типах, не являющимися коллекциями. Шаблоны таких методов, как
Add, Item и Count, предупреждают о том, что тип, использующий их, должен быть
коллекцией.
Array Valued свойства
Вы должны использовать коллекции, чтобы избежать неэффективности кода. В
следующем примере каждое обращение к свойству myObj создает копию массива. В
результате в следующем цикле будет создана 2n+1 копия массива. Более подробная информация приведена в разделе Свойства и методы.
Возвращение пустых массивов
Свойства String и Array никогда не должны возвращать нулевую ссылку. В этом
контексте может быть трудно осмыслить нуль (Null). Например, пользователь может
предположить, что следующий код будет работать: Общим правилом является то, что нуль (null), пустая строка ("") и пустые (0
элементов) массивы должны интерпретироваться одинаково. Возвращайте вместо
нулевой ссылки пустой массив.
Далее приведены основные рекомендации по перезагрузке операторов:
В данной таблице приведен список символов операторов и соответствующих имен
альтернативных методов и операторов.
Далее приведены основные рекомендации по применению метода Equals и оператора
равенства (==):
Информация, касающаяся реализации метода Equals, представлена в разделе MSDN
Реализация метода Equals (Implementing the Equals Method).
Реализация оператора равенства (==) в типах значений
В большинстве языков программирования нет реализации по умолчанию для
оператора равенства (==). Поэтому вы должны перезагрузить == в любое время,
когда равенство имеет место.
Вы должны продумать реализацию метода Equals в типах значений, потому что
реализация по умолчанию в System.ValueType не будет работать так же, как ваша
специальная реализация.
Реализовывайте == всегда, когда переопределяете метод Equals.
Реализация оператора равенства (==) в ссылочных типах
Большинство языков программирования предоставляют реализацию по умолчанию для
оператора равенства (==) для ссылочных типов. Поэтому вы должны быть осторожными
при реализации == в ссылочных типах. Большинство ссылочных типов, даже
реализующие метод Equals, не должны переопределять ==.
Переопределите ==, если ваш тип является базовым типом, таким как Point,
String, BigNumber и т.д. Всегда, когда рассматриваете переопределение операторов
сложения (+) или вычитания (-), вы должны также рассмотреть и переопределение
==.
Далее приведены основные рекомендации по применению приведений типов:
В этом разделе приведены рекомендации по реализации общих шаблонов разработки
в библиотеках классов.
Экземпляры классов часто инкапсулируют элемент управления поверх ресурсов,
таких как описатель окна (HWND), соединения с базами данных и т.д., которые не
управляются средой выполнения. Поэтому вы должны обеспечить как явный, так и
неявный способ освобождения этих ресурсов. Неявный контроль обеспечьте
реализацией в объекте protected метода Finalize (синтаксис деструктора в C# и
управляемые расширения (Managed Extensions) для С++). Сборщик мусора вызывает
этот метод в определенный момент после того, как больше не существует ни одной
действительной ссылки на объект.
В некоторых случаях может понадобится предоставить программистам,
использующим объект, возможность явно освобождать эти внешние ресурсы до того,
как это сделает сборщик мусора. В случае, если внешние ресурсы недостаточны или
дороги, лучшей производительности можно достигнуть явно освобождая ресурсы,
которые больше не используются. Чтобы предоставить явный контроль, реализуйте
метод Dispose, поставляемый интерфейсом IDisposable. Потребитель объекта должен
вызвать этот метод, когда использование объекта закончено. Метод Dispose может
быть вызван даже тогда, когда существуют и другие ссылки на объект.
Обратите внимание, что и когда вы предоставляете явный контроль, применяя
метод Dispose, вы должны обеспечить неявную очистку методом Finalize. Если
программисту не удалось вызвать метод Dispose, использование метода Finalize
предотвращает постоянную утечку ресурсов.
Более подробно об использовании методов Finalize и Dispose для очистки
неуправляемых ресурсов смотри в разделе MSDN Programming for Garbage Collection.
Следующий пример иллюстрирует основную схему разработки для реализации метода
Dispose. Более подробный пример, иллюстрирующий схему разработки для реализации
методов Finalize и Dispose, смотри в разделе MSDN Implementing a Dispose Method.
Изменение имени метода Dispose
Иногда домен-специфическое имя больше подходит, чем Dispose. Например,
инкапсуляция файла может захотеть использовать имя метода Close. В этом случае
реализуйте метод Dispose как private и создайте public метод Close, который
вызывает метод Dispose. Следующий пример иллюстрирует эту схему. Вы можете
заменить имя Close именем метода, которое соответствует вашему домену. Finalize
Далее приведены основные рекомендации по применению метода Finalize:
Примечание: Метод Finalize базового класса вызывается автоматически
с синтаксисом деструктора C# и Управляемых расширений (Managed Extensions) для
C++. Dispose
Далее приведены основные рекомендации по применению метода Dispose:
Информация, касающаяся реализации оператора равенства (==), представлена в
разделе Рекомендации по применению метода Equals и оператора равенства (==).
Примеры
Реализация метода Equals
В следующем примере содержатся два вызова реализации метода Equals,
применяемой по умолчанию: В результате получим следующее: Переопределение метода Equals
В следующем примере представлены класс Point, который переопределяет метод
Equals, чтобы обеспечить равенство значений, и класс Point3D, унаследованный от
Point. Поскольку переопределение метода Equals в классе Point является первым в
цепи наследования, метод Equals базового класса (который наследуется от Object и
проверяет ссылочное равенство) не вызывается. Однако Point3D.Equals вызывает
Point.Equals, потому что класс Point реализовывает метод Equals так, что
обеспечивается равенство значений. Метод Point.Equals проверяет наличие аргумента obj и то, что он ссылается на
экземпляр того же типа, что и данный объект. Если одна из этих проверок не
удается, метод возвращает значение false. Метод Equals использует метод
Object.GetType для того, чтобы определить идентичность типов времени выполнения
этих двух объектов. Обратите внимание, что typeof (TypeOf в Visual Basic) здесь
не используется, потому что он возвращает статический тип. Если вместо этого
метод использовал проверку в форме obj is Point, в результате проверки будет
возвращено значение true в случаях, когда obj является экземпляром класса,
наследуемого от Point, даже если obj и текущий экземпляр не одного типа времени
выполнения. Установив, что эти два объекта имеют один и тот же тип, метод
приводит obj к типу Point и возвращает результат сравнения переменных экземпляра
двух объектов.
В Point3D.Equals унаследованный метод Equals вызывается первым. Метод Equals
проверяет, существует ли obj, является ли obj экземпляром того же класса, что и
данный объект, и совпадают ли переменные экземпляра. Только когда унаследованный
Equals возвращает значение true, метод сравнивает переменные экземпляра,
представленные в дочернем классе. В частности, приведение к Point3D не
производится до тех пор, пока не будет определено, что obj имеет тип Point3D или
является классом, унаследованным от Point3D.
Использование метода Equals для сравнения переменных экземпляра
В предыдущем примере оператор равенства (==) используется для сравнения
переменных отдельного экземпляра. В некоторых случаях для сравнения переменных
экземпляра в реализации Equals лучше использовать метод Equals, как показано в
следующем примере. Перезагрузка оператора равенства (==) и метода Equals
В некоторых языках программирования, таких как С#, поддерживается
перезагрузка оператора. Когда тип перезагружает оператор ==, чтобы обеспечить те
же функциональные возможности, должен быть переопределен и метод Equals. Это
обычно достигается путем написания метода Equals в терминах перезагруженного
оператора равенства (==), как показано в следующем примере. Т.к. Complex является структурой, известно, что ни один класс не будет
наследоваться от Complex. Поэтому методу Equals не надо сравнивать результаты
GetType для каждого объекта. Вместо этого он использует оператор is, чтобы
проверить тип параметра obj.
Делегаты, интерфейсы и события дают вам возможность предоставить
функциональные возможности обратного вызова. Каждый тип имеет свои собственные
характеристики использования, которые делают их более пригодными для отдельных
ситуаций.
События
Используйте события, если истинны следующие утверждения:
Делегаты
Используйте делегаты, если истинно следующее:
Интерфейсы
Используйте интерфейсы, если функция обратного вызова требует комплексного
поведения.
Используйте ожидания, чтобы определить максимальное время ожидания завершения
вызова метода.
Ожидания должны принять форму параметра вызова метода, как показано ниже. Ожидания могут использоваться, как свойство серверного класса, как показано
ниже. Вы должны отдать предпочтение второму подходу, потому что в этом случае более
ясна связь между операцией и ожиданием. Подход, основанный на свойстве,
предпочтительнее, если серверный класс разрабатывается для того, чтобы
использоваться компонентом в визуальных дизайнерах.
Исторически сложилось, что ожидания представляются целыми числами. В этом
случае сложно определить единицы измерения ожидания, и трудно перевести единицы
времени в миллисекунды, которые обычно используются.
Лучшим подходом является использование в качестве типа ожидания структуры
TimeSpan. Применение TimeSpan решает проблемы с целочисленными ожиданиями, о
которых шла речь ранее. Далее приведен пример того, как использовать ожидание
типа TimeSpan. Когда задано TimeSpan(0), если операция не завершается немедленно, метод
формирует исключительную ситуацию. Если ожидание задано TimeSpan.MaxValue,
операция будет ожидать постоянно, как будто ожидание не задано. Не требуется,
чтобы серверный класс поддерживал какое-то из этих значений, но он должен
формировать исключительную ситуацию InvalidArgumentException в случае, если
ожиданию задается значение, не поддерживаемое классом.
Если время ожидания закончено и сформировалась исключительная ситуация,
серверный класс должен отменить эту операцию.
Если используется ожидание, применяемое по умолчанию, серверный класс должен
включать статическое свойство defaultTimeout, которое используется, если
пользователь не задал ожидания. В следующем примере в код включено статическое
свойство OperationTimeout типа TimeSpan, которое возвращает defaultTimeout. Типы, которые не могут обеспечить разрешения, установленного в TimeSpan,
должны округлять значение ожидания до ближайшего промежутка, который может быть
обеспечен. Например, тип, который может обеспечить разрешение в одну секунду,
должен округлять к ближайшей секунде. Исключением из этого правила является
округление в нуль. В таком случае ожидание должно округляться до минимально
возможного значения. Предотвращение округления в нуль предупреждает
возникновение циклов активного ожидания, когда нулевое ожидание приводит к 100%
использованию мощности процессора
Вместо того, чтобы возвращать ошибку кода при окончании периода ожидания,
рекомендуется формировать исключительную ситуацию. Истечение периода ожидания
означает, что операция не может быть успешно завершена и поэтому должна
рассматриваться и обрабатываться, как любая другая ошибка времени выполнения.
Более подробно смотри в разделе Рекомендации по формированию и обработке ошибок.
В случае асинхронных операций с ожиданиями, при первом доступе к результатам
операции должна вызываться функция обратного вызова и формироваться
исключительная ситуация. Это проиллюстрировано в следующем примере кода. Дополнительная информация представлена в разделе Рекомендации по асинхронному
программированию.
Разработчики библиотеки классов должны понимать, как обеспечить защиту
доступа к коду для того, чтобы писать защищенные библиотеки классов. При
написании библиотеки классов учитывайте два принципа безопасности: защищайте
объекты, применяя права доступа, и пишите полностью надежный код. Степень
применения этих принципов будет зависеть от того, какой класс вы создаете.
Некоторые классы, такие как класс System.IO.FileStream, представляют объекты,
которые необходимо защищать путем применения прав доступа. Реализация этих
классов проверяет права доступа пользователей и только зарегистрированным
пользователям позволяется осуществлять те операции, на которые они имеют
разрешение. Пространство имен System.Security содержит классы, которые могут
помочь вам осуществить эти проверки в создаваемых библиотеках классов. Код
библиотеки классов часто является полностью надежным или, по крайней мере,
высоконадежным. Т.к. код библиотеки классов часто работает с защищенными
ресурсами и неуправляемым кодом, любые дефекты кода представляют серьезную
угрозу целостности внутренней системы безопасности. Чтобы минимизировать эту
угрозу, при написании кода библиотеки классов следуйте рекомендациям,
приведенным в данном разделе. Более подробная информация представлена в разделе
MSDN Writing Secure Class Libraries.
Права доступа применяются для обеспечения безопасности определенных ресурсов.
Библиотека классов, которая работает с защищенными ресурсами, должна отвечать за
осуществление этой защиты. Перед проведением любого действия над защищенным
ресурсом, например, уничтожение файла, код библиотеки классов сначала должен
проверить, а имеет ли пользователь соответствующие права на уничтожение ресурса.
Если пользователь имеет разрешение, действию может быть позволено завершиться.
Если пользователь не имеет такого права, действие не будет завершено и должна
будет вызвана исключительная ситуация безопасности. Защита обычно применяется в
коде как с декларативной, так и с обязательной проверкой соответствующего
разрешения.
Очень важным является то, что классы защищают ресурсы не только от прямого
доступа, но и от всех возможных видов воздействия. Например, объект, кэширующий
файл, несет ответственность за проверку разрешения на чтение файла, даже если
реальные данные извлекаются из кэша в памяти и не происходит никакой фактической
работы с файлом. Это происходит потому, что эффект от передачи данных
пользователя такой же, как и в случае, если он осуществил фактическую операцию
чтения.
Многие библиотеки классов реализовываются, как полностью надежный код,
который инкапсулирует функциональные возможности, специфические для конкретной
платформы, как управляемые объекты, такие как СОМ или system APIs. Полностью
надежный код может привести к ослаблению безопасности всей системы. Однако, если
библиотеки классов написаны правильно, с учетом безопасности, размещение мощного
груза безопасности в относительно небольшом наборе библиотек классов и основная
безопасность времени выполнения дает возможность большему количеству
управляемого кода получить преимущества безопасности этих корневых библиотек
классов.
В обычном сценарии безопасности библиотеки классов полностью надежный класс
раскрывает ресурс, защищенный правом доступа; к ресурсу имеет доступ внутренний
код API. Обычным примером такого типа ресурсов является файл. Класс File
использует для осуществления файловых операций, таких как удаление, внутренний
код API. Для защиты ресурса выполните следующие шаги:
Код в надежной библиотеке классов имеет права, недоступные большинству кодов
приложений. Кроме того, сборка может содержать классы, которые не нуждаются в
специальных правах, а сами предоставляют эти права, потому что сборка содержит и
другие классы, которым они нужны. Такая ситуация может привести к ослаблению
безопасности системы. Поэтому необходимо быть особо аккуратным при написании
высоко или полностью надежного кода.
Разрабатывайте надежный код таким образом, чтобы его мог вызвать любой код
системы, обладающий частичной надежностью, без создания пробелов в системе
безопасности. Обычно ресурсы защищаются методом проверки прав пользователей.
Если пользователь не обладает соответствующим разрешением, попытка доступа
блокируется. Однако как только надежный код предоставляет разрешение, код
принимает ответственность за проверку на наличие необходимого права доступа.
Обычно предоставление разрешения должно следовать за проверкой пользователя на
наличие разрешения, как было описано выше. Кроме того, количество предоставления
высоких полномочий должно быть минимальным, для того чтобы сократить риск
несанкционированных воздействий.
Полностью надежному коду неявно предоставляются все остальные права. Кроме
того, ему позволено нарушать правила безопасности типов и использования
объектов. Независимость защиты ресурсов, любой аспект программного интерфейса,
который может разрушить безопасность типов или предоставить возможность доступа
к данным, обычно недоступным, могут привести к проблемам безопасности.
Проверка безопасности приводит к проверке стека на наличие разрешения у всех
пользователей. В зависимости от глубины стека эти операции потенциально требуют
очень больших затрат производительности. Если одна операция в действительности
состоит из некоторого числа действий на нижнем уровне, который требует проверки
безопасности, значительно увеличить производительность можно путем разовой
проверки всех разрешений пользователя, а потом перед осуществлением определенных
действий подтверждать необходимое разрешение. Подтверждение остановит дальнейшее
прохождение по стеку, таким образом проверка будет остановлена и будет успешной.
Обычно применение этой технологии приводит к увеличению производительности, если
есть возможность объединить три и более проверки.
Далее приведены основные рекомендации по организации поточной обработки:
Проблемы с производительностью могут возникнуть, когда статический метод в
классе А вызывает статический метод класса А. Если эти методы неправильно
спроектированы, страдает производительность, потому что будет большое
количество избыточной синхронизации. Чрезмерное использование синхронизации
может иметь негативное воздействие на производительность. Кроме того, это
может иметь существенное отрицательное влияние на масштабируемость.
Имейте ввиду проблемы с оператором lock (SyncLock в Visual Basic). Довольно
заманчиво использовать оператор lock для решения всех проблем с организацией
поточной обработки. При проверке кода вы должны остерегаться таких
экземпляров, как тот, который показан в следующем примере. Для увеличения производительности, необходимо заменить этот код
System.Threading.Interlocked.Increment(myField);
То же косается и следующего примера. Вместо этого кода: Асинхронное программирование поддерживается многими областями общеязыковой
среды выполнения, такими как Remoting, ASP.NET и Windows Forms. Асинхронное
программирование является центральной идеей .NET Framework. В этом разделе
приведены схемы разработки для асинхронного программирования.
Следует придерживаться следующих рекомендаций:
Следующий пример демонстрирует серверный класс: В следующем примере показан клиент, определяющий шаблон для асинхронного
вызова метода Factorize из класса PrimeFactorizer предыдущего примера. Компилятор создаст следующий класс FactorizingCallback после синтаксического
разбора его определения в первой строке предыдущего примера. Будут сгенерированы
методы BeginInvoke и EndInvoke. Интерфейс, используемый как параметр делегата в следующем примере, определен
в библиотеке классов .NET Framework.. Подробнее смотри в разделе MSDN
IAsyncResult Interface. Обратите внимание, что объект, реализующий интерфейс IAsyncResult, должен
быть объектом ожидания (waitable) и его базовые элементы синхронизации должны
быть уведомлен после отмены или завершения вызова. Это дает возможность клиенту
ожидать окончания вызова, вместо проведения опроса. Среда выполнения поставляет
большое количество объектов ожидания, которые зеркально отображают базовые
элементы синхронизации Win32, такие как ManualResetEvent, AutoResetEvent и
Mutex.
Метод Cancel - это запрос на отмену обработки метода после того, как
закончился программный период ожидания. Обратите внимание, что это только запрос
клиента, и серверу рекомендуется принимать его на обработку. Далее клиент не
должен принимать того, что сервер полностью остановил обработку запроса после
получения нотификации об отмене метода. Другими словами, клиенту рекомендуется
не уничтожать такие ресурсы, как объекты файла, потому что сервер в данный
момент может активно их использовать. Свойству IsCanceled будет присвоено
значение true, если вызов был отменен, и свойству IsCompleted будет присвоено
значение true после того, как сервер закончит обработку вызова. После того, как
сервер присвоит свойству IsCompleted значение true, сервер не сможет
использовать никакие предоставленные клиентом ресурсы вне обусловленной
семантики совместного использования. Таким образом, клиент может безопасно
уничтожать ресурсы после того, как свойство IsCompleted возвращает true.
Свойство Server возвращает серверный объект, который предоставил
IAsyncResult.
Следующий пример демонстрирует программную модель со стороны клиента для
асинхронного вызова метода Factorize. Обратите внимание, что если FactorizeCallback является
контекстно-ограниченным классом, который требует синхронизированного или
потоко-родственного контекста, функция обратного вызова отправляется через
инфраструктуру диспетчера контекста. Иначе говоря, сама функция обратного вызова
для таких контекстов может выполняться асинхронно в зависимости от ее вызывающей
функции. Это семантика одностороннего классификатора сигнатур метода. Любой
такой вызов метода может выполняться синхронно или асинхронно в зависимости от
вызывающей функции, и вызывающая функция не может делать никаких предположений о
завершении такого вызова, когда контроль за выполнением возвращается к нему.
Вызов EndInvoke до завершения асинхронной операции также заблокирует
вызывающую функцию. Повторный его вызов с тем же AsyncResult не определен.
Сервер разбивает любую асинхронную операцию на две логические части: часть,
которая принимает входной сигнал от клиента и начинает асинхронную операцию, и
часть, которая поставляет результаты асинхронной операции клиенту. Кроме
входного сигнала, необходимого асинхронной операции, первая часть также выбирает
объект AsyncCallbackDelegate, который должен будет вызываться после завершения
асинхронной операции. Первая часть возвращает объект ожидания, который реализует
интерфейс IAsyncResult, используемый клиентом для определения статуса
асинхронной операции. Обычно сервер также использует объект ожидания, который он
возвращает клиенту, чтобы поддерживать любое состояние, связанное с асинхронной
операцией. Клиент использует вторую часть, чтобы получить результаты асинхронной
операции, поставляя объект ожидания.
При инициировании асинхронных операций клиент может поставлять или не
поставлять делегата функции обратного вызова.
Клиенту доступны следующие варианты завершения асинхронных операций:
Один сценарий, в котором желательны и синхронные, и асинхронные методы чтения
и записи, - это использование ввода/вывода (input/output) файла. Следующий
пример иллюстрирует схему разработки, показывая как объект File реализует
операции чтения и записи. Клиент не может просто ассоциировать состояние с данной асинхронной операцией
без определения нового делегата функции обратного вызова для каждой операции.
Этот недостаток может быть устранен путем вынуждения методов Begin, таких как
BeginWrite, принимать параметр дополнительного объекта, который представляет
состояние.
Этот раздел адресован разработчикам, желающим достигнуть оптимальной
производительности приложений в управляемом мире. Приведены образцы кода,
описания и рекомендации разработки для баз данных, Windows Forms и ASP
приложений.
Этот раздел создан как руководство для разработчиков, создающих приложения
для .NET и ищущих различные способы улучшения производительности. Если даже вы
новичок в .NET, для работы с этим разделом вы должны знать и платформу, и
выбранный язык программирования. Материал в данном разделе изложен, исходя из
предположения, что программист уже знает достаточно, чтобы понять процесс
выполнения программы. Если вы хотите перенести существующее приложение в .NET,
предварительно будет полезным прочитать этот раздел. Некоторые из подсказок,
приведенных здесь, полезны во время разработки и предоставляют информацию,
которую необходимо знать до того, как вы начнете перенос.
Этот раздел состоит из нескольких частей, в которых подсказки организованы в
соответствии с типами проектов и категориями разработчиков. Первый набор
подсказок относится к любому языку программирования и содержит совет, который
поможет вам в любом языке программирования Общеязыковой среды выполнения (CLR).
Далее следуют подсказки для ASP.NET.
Из-за сжатых сроков, первая версия среды выполнения должна сначала была
обеспечить самые широкие функциональные возможности, а затем работать с особыми
случаями оптимизации. В результате существует несколько моментов, в которых
возникают проблемы с производительностью. По существу, этот раздел рассматривает
несколько возможностей выхода из этих ситуаций. Эти подсказки скорее всего не
будут нужны в следующей версии, т.к. такие случаи систематически выявляются и
оптимизируются.
Здесь приведено несколько подсказок, которые следует учитывать при работе с
любым языком программирования в CLR. Они подходят для любого из них и должны
быть первоочередными средствами борьбы с проблемами производительности.
Формируйте меньшее количество исключительных ситуаций
Формирование исключительной ситуации может требовать очень больших затрат
производительности, так что убедитесь, что вы не формируете их слишком много.
Чтобы увидеть, сколько исключительных ситуаций формирует ваше приложение,
используйте Perfmon. Вы будете удивлены обнаружив, что определенные области
вашего приложения формирует больше исключительных ситуаций, чем вы ожидали. Для
большей конкретизации вы также можете проверить их количество программно,
применяя Performance Counters (Счетчики производительности).
Обнаружение и удаление кода, формирующего большое количество исключительных
ситуаций, может привести к большому выигрышу в производительности. Помните, что
не надо ничего делать с блоками try/catch: вы несете потери только при
формировании фактической исключительной ситуации. Вы можете использовать столько
блоков try/catch, сколько хотите. Использование исключительных ситуаций
неоправданно там, где вы теряете производительность. Например, вы должны
избегать использования исключительных ситуаций для формирования управляющей
логики.
Вот простой пример того, как дороги могут быть исключительные ситуации: мы
просто попытаемся прогнать цикл For, генерируя тысячи исключительных ситуаций и
обрабатывая их. Попытайтесь закомментировать оператор throw и вы увидите разницу
в скорости: эти исключительные ситуации приводят к гигантским непроизводительным
издержкам. Делайте емкие вызовы
Емкий вызов - это вызов функции, который выполняет несколько задач, так же
как метод, инициализирующий несколько полей объекта. Его надо рассматривать в
противопоставлении неёмкому вызову, который выполняет очень простые задачи и
использует для этого множество вызовов (например, установка каждого поля объекта
отдельным вызовом). В методах, в которых непроизводительные расходы выше, чем
для простых вызовов методов внутри AppDomain, важно делать емкие вызовы. Вызовы
P/Invoke, interop и remoting приводят к непроизводительным расходам и вы хотите
пореже их использовать. В каждом из этих случаев надо попытаться спроектировать
приложение так, чтобы оно не использовало маленькие, частые вызовы, которые
приводят к таким большим непроизводительным расходам.
Переход через границы происходит всегда, когда управляемый код вызывается из
неуправляемого кода и наоборот. Среда выполнения крайне облегчает программисту
реализацию возможности взаимодействия, но это делается за счет
производительности. Когда происходит такой переход, необходимо предпринять
следующие шаги:
Чтобы уменьшить время перехода через границы, старайтесь по возможности
пользоваться P/Invoke. Непроизводительные расходы соответствуют 31 команде плюс
затраты на маршаллинг, если нужен маршаллинг данных, и только 8 в
противоположном случае. Взаимодействие СОМ намного более дорого, более 65
команд.
Маршаллинг данных не всегда требует больших затрат. Примитивным типам вообще
не нужен маршаллинг, также дешевы классы с четкой компоновкой. Реальное снижение
производительности происходит во время преобразования данных, например,
конвертации текста из ASCI в Unicode. Убедитесь, что данные, передаваемые через
границы управляемого кода преобразуются только в том случае, если это
необходимо: может оказаться, что просто соглашаясь на определенный тип данных
или формат в вашей программе, вы можете избежать больших затрат на маршаллинг.
Следующие типы называют блитируемыми, имея ввиду, что их можно копировать
прямо через управляемую/неуправляемую границу вообще без маршаллинга: sbyte,
byte, short, ushort, int, uint, long, ulong, float и double. Вы можете свободно
передавать эти типы без всяких потерь производительности, так же как типы
значений и одномерные массивы, содержащие блитируемые типы. Мельчайшие детали
маршаллинга можно изучить дополнительно в библиотеке MSDN.
Разработка с использованием типов значений
По возможности используйте простые структуры, даже когда редко упаковываете и
распаковываете. Вот простой пример, демонстрирующий разницу в скоростях: Когда запустите этот пример, вы увидите, что цикл с использованием структуры
выполняется на порядок быстрее. Однако важно быть осторожным при использовании
типов значений в качестве объектов. Это создает дополнительные затраты на
упаковку и распаковку, которые могут в конце концов превысить затраты, которые
могли бы возникнуть, если бы вы использовали объекты! Чтобы увидеть это на деле,
измените код и используйте массив структур foo и объектов bar. Вы обнаружите,
что производительность приблизительно та же.
Примечание: Типы значений гораздо менее гибкие, чем объекты, и при
некорректном использовании очень сильно снижают производительность. Необходима
крайняя осторожность в том, когда и как их использовать.
Поработайте с примером, приведенном выше, сохраняя foos и bars в массивах или
хэш-таблицах. Вы увидите, что выигрыш в скорости пропадет сразу же, как только
появится хотя бы одна операция упаковывания и распаковывания.
Можно проследить за тем, как интенсивно вы используете упаковку и распаковку,
просмотрев распределения и сборки мусора GC. Это можно сделать прямо с помощью
Perfmon или через Performance Counters в коде.
Полную информацию по типам значений смотрите в Приложении Б.
Используйте AddRange для добавления групп элементов
Вместо многократного добавления каждого элемента коллекции по отдельности,
используйте AddRange, чтобы добавить сразу всю коллекцию. Практически все
элементы управления Windows и коллекции имеют и метод Add, и метод AddRange, и
каждый из них оптимизирован для разных целей. Add используется для добавления
отдельного элемента, в то время как использование AddRange приводит к некоторым
потерям производительности, но выигрывает при добавлении множества элементов.
Здесь приведены только некоторые из классов, поддерживающих Add и AddRange:
Ограничьте ваш Working Set
Чтобы поддерживать небольшие размеры Working Set, минимизируйте число
используемых сборок. Если вы загружаете всю сборку просто для того, чтобы
использовать один метод, вы платите огромную цену за очень несущественные
преимущества. Посмотрите нельзя ли скопировать функциональные возможности этого
метода, используя код, который вы уже загрузили.
Отслеживание размера Working Set является довольно сложной задачей и может
стать темой целой статьи. Здесь приведены некоторые подсказки, которые помогут
вам:
Используйте циклы For для перебора символов в строке
В C# ключевое слово foreach позволяет пройти по элементам списка, строки и
т.д. и осуществить операции над каждым из них. Это очень мощный инструмент, т.к.
он работает как универсальный нумератор для многих типов. Альтернативой этого
обобщения является скорость, и если вы действительно часто используете итерацию
строк, лучше используйте цикл For. Поскольку строки - это простые массивы
символов, их можно пройти с намного меньшими потерями, чем другие структуры. JIT
компиляция достаточно интеллектуальна (в большинстве случаев), чтобы
оптимизировать граничные проверки и другие вещи внутри цикла For, но при обходах
foreach этого сделать нельзя. В результате цикл For для строк работает более,
чем в пять раз быстрее, чем цикл foreach.
Здесь приведен простой тестовый метод для демонстрации разницы скоростей.
Попытайтесь запустить его, затем удалите цикл For и раскомментируйте выражение
foreach. Примечание: Foreach намного более удобочитаемый и в будущем он станет
более быстрым, чем цикл For, для таких специальных случаев, как строки.
Используйте StringBuilder для работы со строками
После изменения строки среда выполнения создаст новую строку и вернет ее, а
оригинальная строка будет собрана сборщиком мусора. В большинстве случаев это
быстрый и простой процесс, но частые изменения строки приводят к снижению
производительности: все эти размещения в конечном концов приводят к
непроизводительным расходам. Здесь приведены два примера. Первый - это простая
программа, в которой происходит дополнение строки 50 000 раз; во втором примере
для модификации строки используется объект StringBuilder. Код, использующий
StringBuilder, выполняется намного быстрее, если вы запустите оба этих примера,
это сразу станет очевидным.
Посмотрите в Perfmon сколько времени экономится, когда не надо распределять
тысячи строк. Посмотрите на счетчик "% времени в GC" под списком .NET CLR
Memory. Здесь вы также можете увидеть число сохраненных распределений, а так же
статистику сборки мусора.
Примечание: с созданием объекта StringBuilder связаны некоторые
непроизводительные затраты времени и памяти. На машинах с быстрой памятью имеет
смысл применять StringBuilder, если вы делаете около пяти операций. Как правило,
10 и более строковых операций являются оправданием для непроизводительных
расходов на любой машине, даже самой медленной.
Прекомпилируйте приложения Windows Forms
Методы подвергаются JIT компилированию при первом использовании. Это
означает, что, чем больше методов вызывает ваше приложение при запуске, тем
дольше будет идти загрузка программы. Windows Forms используют большое
количество совместно используемых библиотек в ОС, и непроизводительные расходы
при их запуске могут быть намного большими, чем для любых других приложений.
Хотя это и не всегда так, но прекомпилирование приложений Windows Forms обычно
приводит к выигрышу в производительности.
Microsoft обеспечивает возможность прекомпилировать приложения, вызывая
ngen.exe. Вы можете запустить ngen.exe во время инсталляции или перед поставкой
приложения. Определенно больший смысл имеет запускать ngen.exe во время
инсталляции, т.к. в этом случае вы сможете удостовериться, что приложение
оптимизировано для машины, на которой оно было инсталлировано. Если вы
запускаете ngen.exe до поставки программы, вы ограничиваете оптимизацию тем, что
доступно на вашей машине. В таблице приведены объективные времена запуска для
ShowFormComplex, приложений winforms с примерно сотней элементов управления.
Каждый эксперимент проводился после перезагрузки. Как вы видите, приложения
Windows Forms сразу используют много методов, поэтому прекомпилирование приводит
к существенным выигрышам в производительности.
Используйте Jagged массивы
JIT компиляция оптимизирует jagged массивы (?массивы массивов?) более
эффективно, чем прямоугольные массивы, и разница весьма заметна. Данная таблица
демонстрирует выигрыш в производительности при использовании jagged массивов
вместо прямоугольных массивов в C# и Visual Basic (большие числа лучше):
Как видите, применение jagged массивов может привести к поразительному
повышению производительности.
Поддерживайте размер буфера ввода/вывода (IO) в пределах 4KB - 8KB
Практически для всех приложений размер буфера в пределах 4KB - 8KB обеспечит
максимальную производительность. В особых случаях вы, возможно, сможете достичь
лучших результатов с большим буфером (загрузка больших графических объектов
предсказуемого размера, например), но в 99.99% случаев это только приведет к
ненужной растрате памяти. Во всех буферах, унаследованных от BufferedStream,
предоставлена возможность установить любой желаемый размер, но в большинстве
случаев 4 и 8 обеспечат наилучшую производительность.
Будьте внимательны с асинхронным вводом/выводом (IO)
В редких случаях вы сможете получить выгоду от асинхронного ввода/вывода
(IO). Одним из примеров может быть загрузка и декомпрессия ряда файлов: вы
можете считать биты из одного потока, декодировать их и записать в другой поток.
Чтобы эффективно использовать асинхронный ввод/вывод, необходимы большие усилия,
и если что-то сделано неправильно, это приведет только к потерям
производительности. Преимущество асинхронного ввода/вывода в том, что при
правильном его использовании есть возможность достичь в десять раз большей
производительности.
Основной идеей при совершенствовании доступа к базам данных является
использование только тех функциональных возможностей, которые необходимы для
этого, и разработка с использованием ?разъединенного? подхода: вместо
поддержания открытым длительное время одного соединения делать несколько
соединений последовательно. Вы должны разрабатывать приложения с учетом этих
изменений.
Microsoft для обеспечения максимальной производительности рекомендует
стратегию N-связей, как альтернативу прямому соединению клиент-база данных.
Учитывайте эти при разработке приложений, т.к. многие технологии оптимизируются,
чтобы использовать преимущества множества связей.
Используйте оптимальный управляемый провайдер
Вместо того, чтобы полагаться на универсальный провайдер, сделайте правильный
выбор управляемого провайдера. Есть управляемые провайдеры, написанные
специально для различных баз данных, например, SQL (System.Data.SqlClient). При
использовании универсального интерфейса, например, System.Data.Odbc, в то время
как есть возможность использования специализированного компонента, вы потеряете
в производительности занимаясь дополнительными уровнями преобразований.
Использование оптимального провайдера даст возможность использовать различные
языки: Управляемый SQL клиент обращается к SQL базе данных на TDS, что намного
лучше, чем универсальный OleDbprotocol.
Используйте Data Reader вместо Data Set когда это возможно
Используйте data reader как можно чаще. Это обеспечивает быстрое считывание
данных, которые могут быть кэшированы по желанию пользователя. Data reader - это
просто поток, который обеспечивает возможность считывать данные по мере их
поступления и затем удаляет их без сохранения в dataset для дальнейшей
навигации. Потоковый подход более быстрый и вызывает меньшие непроизводительные
затраты, поскольку вы можете сразу использовать данные. Чтобы решить, имеет ли
смысл кэширование для навигации, вы должны определить, как часто вам нужны одни
и те же данные. Данная таблица приведена, чтобы продемонстрировать разницу между
DataReader и DataSet в ODBC и SQL провайдерах при извлечении данных с сервера
(большие числа лучше):
Как видите, большая производительность достигается при использовании
оптимального управляемого провайдера вместе с data reader. Когда вам не надо
кэшировать данные, использование data reader может обеспечить вам огромный
выигрыш в производительности.
Используйте Mscorsvr.dll для многопроцессорных машин
Для автономных серверных приложений и приложений среднего уровня
(middle-tier) убедитесь, что используете mscorsvr для многопроцессорных машин.
Mscorwks не оптимизированы по масштабированию и пропускной способности, а
серверные версии имеют несколько оптимизаций, которые обеспечивают им хорошую
масштабируемость, когда доступно более одного процессора.
По возможности используйте хранимые процедуры
Хранимые процедуры являются высоко оптимизированными инструментальными
средствами, которые при эффективном использовании обеспечивают великолепную
производительность. Используйте хранимые процедуры для обработки вставок,
обновлений и удалений с data adapter. Хранящиеся процедуры не надо
интерпретировать, компилировать или даже передавать от клиента, что уменьшает
непроизводительные расходы как сетевого трафика, так и сервера. Убедитесь, что
используете CommandType.StoredProcedure вместоCommandType.Text.
Будьте осторожны с использованием строк динамического соединения
Организация связного пула - лучший способ повторного использования соединений
для множества запросов, чем затраты на открытие и закрытие соединения для
каждого отдельного запроса. Это делается неявно, но вы получаете один пул на
отдельную строку соединения. Если вы генерируете строки соединения динамически,
убедитесь, что каждый раз строки идентичны, чтобы произошла организация связного
пула. Также помните, что если происходит делегирование, вы получаете один пул на
клиента. Вы можете установить много опций для связного пула и используя Perfmon
можете отслеживать время ответа, количество транзакций в секунду и т.д.
Отключите возможности, которые не используете
Отключите автоматическое управление пранзакциями, если не используете его.
Для SQL управляемого провайдера это делается посредством строки соединения: При заполнении набора данных с помощью адаптера данных не получайте
информацию первичного ключа, если вам не надо этого делать (т.е. не задавайте
MissingSchemaAction.AddWithKey): Избегайте команд, генерируемых автоматически
При использовании адаптера данных избегайте команд, генерируемых
автоматически. Они требуют дополнительных обращений к серверу для извлечения
метаданных и понижают уровень контроля взаимодействия. Хотя команды,
генерируемые автоматически, использовать удобно, в требовательных к
производительности приложениях стоит делать это самостоятельно.
Правильно используйте ADO
Помните, что при выполнении команды или вызов функции заполнения адаптера,
каждая запись, определенная вашим запросом, возвращается.
Если серверные курсоры совершенно необходимы, они могут быть реализованы
через хранимые процедуры в t-sql. По возможности избегайте этого, т.к.
реализации, основанные на курсорах, не очень хорошо масштабируются.
Если надо, реализовывайте пейджинг без установления соединения. Вы можете
добавить дополнительные записи в набор данных:
Поддерживайте небольшие размеры своих наборов данных
В наборы данных помещайте только необходимые вам записи. Помните, что набор
данных все свои данные сохраняет в памяти, и чем больше данных вы запрашиваете,
тем больше времени понадобится для их передачи.
Как можно чаще используйте последовательный доступ
Используйте CommandBehavior.SequentialAccess с data reader. Это важно при
работе с blob типами данных, т.к. дает возможность считывать данные маленькими
порциями. Поскольку вы можете в данный момент времени работать только с одной
частью данных, исчезает задержка на загрузку большого количества данных. Если
нет необходимости работать сразу со всем объектом, использование
последовательного доступа обеспечит намного лучшую производительность.
Активно используйте кэширование
При разработке приложений, используя ASP.NET, будьте внимательны с
кэшированием. В серверных версиях ОС у вас есть множество средств для тонкой
настройки использования кэшей со стороны сервера и клиента. В ASP.NET для
повышения производительности есть несколько возможностей и инструментальных
средств.
Выходное кэширование - сохраняет статический результат ASP.NET запроса.
Определяется с помощью директивы <@% OutputCache %>:
Разумное использование кэширования может обеспечить превосходную
производительность. Важно продумать то, какой тип кэширования вам необходим.
Представьте сложный коммерческий сайт с несколькими статическими страницами для
регистрации, а затем множество динамически генерируемых страниц, содержащих
графические элементы и текст. Вы, возможно, захотите использовать кэширование
для этих страниц регистрации и фрагментарное кэширование для динамических
страниц. Панель инструментов, например, может кэшироваться фрагментарно. Для еще
лучшей производительности вы можете кэшировать часто используемые графические
элементы и шаблонные тексты, часто появляющиеся на сайте, используя Cache API.
Используйте Session State только по необходимости
Одной чрезвычайно мощной особенностью ASP.NET является возможность сохранения
session state для пользователей, например, корзину покупок на сайте электронной
коммерции или историю браузера. Т.к. это свойство используется по умолчанию,
даже если вы не пользуетесь им, расходуется память. Если вы не используете
session state, выключите его и предохраните себя от непроизводительных расходов
добавлением <@% EnabledSessionState = false %> в свое приложение.
Для страниц, только считывающих session state, можно выбрать
EnabledSessionState=readonly. В этом случае затраты будут меньшими, чем на
полное считывание/запись состояния соединения, и этот вариант более приемлем,
если необходима только часть функциональных возможностей.
Используйте View State только по необходимости
Примером View State может быть длинная форма, которую пользователи должны
заполнить: если они нажимают кнопку Back в своем браузере и затем возвращаются,
форма останется заполненной. Когда данные функциональные возможности не
используются, это состояние расходует память и производительность. Возможно,
наибольшая потеря производительности в этом случае происходит из-за того, что
возвращаемые данные должны посылаться по сети каждый раз при загрузке страницы,
чтобы обновить и проверить кэш. Т.к. это происходит по умолчанию, необходимо
обозначить, что вы не хотите использовать View State, следующим образом: <@%
EnabledViewState = false %>.
Избегайте STA COM
Апартаменты СОМ созданы для работы с потоковой и неуправляемой средой.
Существует два вида апартаментов СОМ: однопоточный и многопоточный.
Многопоточный (MTA) СОМ разработан для обработки многопоточности, в то время как
однопоточный (STA) СОМ основывается на системе обмена сообщениями, чтобы
сериализовывать запросы потока. Управляемый мир потоконезависим, и использование
STA COM требует, чтобы все неуправляемые потоки совместно использовали один
поток для взаимодействия. В результате - огромное снижение производительности,
т.е. этого надо избегать. Если вы не можете перенести апартамент COM объект в
управляемый мир, используйте <@%AspCompat = "true" %> для страниц, которые
его используют.
Удалите ненужные Http модули
В зависимости от используемых возможностей удалите неиспользуемые или
ненужные http модули из конвейера (pipeline).
Избегайте использования свойства Autoeventwireup
Вместо того, чтобы полагаться на autoeventwireup, переопределите события
Page. Например, вместо записи метода Page_Load() попытайтесь перезагрузить
public метод void OnLoad(). Это освобождает среду выполнения от необходимости
выполнять CreateDelegate() для каждой страницы.
Кодируйте, используя ASCII, когда нет необходимости использовать UTF
По умолчанию, ASP.NET настроена так, чтобы кодировать запросы и ответы, как
UTF-8. Если ASCII - это все, в чем нуждается ваше приложение, устранение затрат
на UTF может повысить производительность. Обратите внимание, что это можно
делать только для каждого отдельного приложения.
Используйте оптимальную процедуру аутентификации
Существует несколько способов аутентификации пользователя и несколько более
дорогих, чем другие (в порядке возрастания затрат: None, Windows, Forms,
Passport). Убедитесь, что используете самую дешевую из подходящих для вашего
случая как классы.
Этот раздел включает обзор различных технологий, работающих в управляемом
мире, и техническое описание того, как они влияют на производительность. Это
касается работы сборщика мусора, JIT, remoting, типов значений, безопасности и
т.д.
Среда выполнения .NET представляет несколько передовых технологий,
предназначенных для обеспечения безопасности, облегчения разработки и
производительности. Для разработчика важно понимать каждую из этих технологий и
эффективно применять их в коде. Передовые инструментальные средства,
предоставляемые средой выполнения, облегчают построение надежных приложений, но
заставить эти приложения "летать" является (и всегда являлось) задачей
разработчика.
Основные сведения
Сборка мусора (GC) освобождает память, занятую объектами, которые больше не
используются, тем самым освобождая программиста от распространенных и сложных в
отладке ошибок. Общая схема жизненного цикла объекта, как для управляемого, так
и для машинного кода, такая: В неуправляемом коде вам надо проделывать все эти операции самостоятельно.
Упущение этапов распределения или очистки памяти, может привести к совершенно
непредсказуемому поведению, что будет очень трудно отладить, а если вы забудете
освободить объекты, могут возникнуть утечки памяти. Последовательность
распределения памяти в Общеязыковой среде выполнения (CLR) очень похожа на
только что рассмотренную. Если мы добавим GC-специфичную информацию, мы получим
нечто очень похожее: До тех пор, пока объект может быть освобожден, в обоих мирах предпринимаются
одни и те же шаги. В неуправляемом коде вы должны помнить о необходимости
освобождения объекта по окончании работы с ним. В управляемом коде, как только
объект больше не используется, GC может удалить его. Конечно же, если ваш ресурс
требует особого внимания для освобождения (скажем, закрытие соединения), GC
может понадобиться помощь, для того чтобы закрыть его правильно. До сих пор
применяется такой же код, как вы писали ранее для очистки ресурса перед
освобождением, в форме методов Dispose() и Finalize().
Если вы сохраняете указатель на ресурс, GC никак не может знать, собираетесь
ли вы использовать его в будущем. Это означает то, что правила, используемые
вами в неуправляемом коде для явного освобождения объектов, до сих пор
применимы, но в основном обрабатывать все для вас будет GC. Вместо того, чтобы
100% времени посвящать управлению памятью, это займет у вас всего 5% времени.
Сборщик мусора CLR - это относящийся к определенному поколению, сборщик. Он
следует некоторым принципам, которые позволяют достигать превосходной
производительности. Во-первых, известно, что объекты с коротким временем жизни
обычно небольшие, к ним часто обращаются. GC разделяет таблицу распределения на
несколько подтаблиц, называемых поколениями, что позволяет максимально сократить
время сборки мусора. Поколение 0 включает молодые, часто используемые объекты.
Это поколение самое маленькое, для сборки мусора в нем требуется около 10
миллисекунд. GC может игнорировать другие поколения во время этой сборки мусора,
таким образом обеспечивается намного большая производительность. Поколения 1 и 2
предназначены для больших и более старых объектов, и сборка мусора в них
происходит не так часто. Когда происходит сборка мусора в поколении 1, также
просматривается и поколение 0. Сборка мусора в поколении 2 - это полная сборка
мусора, и только здесь GC проходит всю таблицу. Это также приводит к разумному
использованию кэшей CPU, который может настроить подсистему памяти под
конкретный процессор, на котором выполняется GC.
Когда происходит сборка мусора?
Когда сделано распределение времени, GC проверяет необходимость сборки
мусора. Учитывается размер мусора, размер оставшейся памяти, размеры каждого
поколения, а затем для принятия решения используется эвристическое правило. Во
время сборки мусора скорость распределения объекта обычно такая же (или больше),
чем в С или С++.
Что происходит, когда идет сборка мусора?
Давайте проследим шаг за шагом, что происходит во время сборки мусора. GC
сохраняет список ссылок, которые указывают на кучу GC. Если объект существует,
есть и ссылка на его местоположение в куче. Объекты в куче также могут указывать
друг на друга. Эта таблица указателей и должна быть проверена GC для того, чтобы
освободить память. Последовательность событий такова:
Рисунок 1. Перед сборкой мусора: обратите внимание, что не все блоки
доступны!
Рисунок 2. После сборки мусора: блоки, к которым происходят обращения,
уплотнены. Больше свободного пространства! Удаление объекта
Некоторые объекты нуждаются в специальной обработке до того, как ресурс
сможет быть возвращен. Примерами таких ресурсов являются файлы, сетевые
соединения или соединения с базами данных. Если вы хотите закрыть эти ресурсы
изящно, простого освобождения памяти в куче будет недостаточно. Чтобы
осуществить очистку объекта, вы можете использовать или метод Dispose(), или
метод Finalize(), или сразу оба этих метода.
Метод Finalize():
Метод Dispose():
Управляемые объекты, которые удерживают только управляемые ресурсы, не
нуждаются в этих методах. Ваша программа, вероятно, будет использовать только
несколько комплексных ресурсов, и, возможно, вы будете знать, что они из себя
представляют и когда они вам нужны. Если вам известно и то, и другое, нет
причины надеяться на финализаторы, т.к. вы можете вручную произвести очистку.
Есть несколько причин на то, чтобы вы захотели сделать так, и все они связаны с
очередью финализатора.
В GC, когда объект, имеющий финализатор, отмечен как подлежащий уничтожению,
он и любой объект, на который он указывает, помещаются в специальную очередь.
Отдельный поток проходит по этой очереди, вызывая метод Finalize() каждого
элемента очереди. Программист не контролирует этот поток или порядок
расположения элементов в очереди. GC может вернуть управление программе, в то
время как финализация объектов в очереди еще не будет проведена. Эти объекты
могут остаться в памяти, сохраняемые в очереди длительное время. Вызовы на
финализацию делаются автоматически, и сам вызов не оказывает прямого влияния на
производительность. Однако недетерминированная модель финализации определенно
может иметь другие непрямые последствия:
Диаграмма состояния на рисунке 3 иллюстрирует различные пути, которые может
пройти ваш объект в условиях финализации или освобождения:
Рисунок 3. Пути освобождения и финализации, которые может пройти объект
Как видите, финализация добавляет несколько шагов в жизненный цикл объекта.
Если вы самостоятельно освобождаете объект, он может быть удален и память будет
возвращена при следующей сборке мусора. Когда необходимо, чтобы произошла
финализация, вам придется ожидать до тех пор, пока ни будет вызван существующий
метод. Т.к. неизвестно, когда это произойдет, вы можете иметь большое количество
связанной памяти и зависеть от очереди финализации. Это может быть крайне
проблематичным, если ваш объект соединен со всем деревом объектов, и они все
располагаются в памяти в ожидании финализации.
Как выбрать, какой сборщик мусора использовать?
В CLR есть два разных GC: Рабочая станция (mscorwks.dll) и Сервер
(mscorsvr.dll). При работе в режиме Рабочей станции больший интерес, чем
пространство и эффективность, представляет задержка . Сервер с множеством
процессоров и клиентов, соединенных через сеть, может позволить некоторую
задержку, но основной является производительность. Microsoft включила два
сборщика мусора, каждый из которых приспособлен к определенной ситуации.
GC Сервер:
GC Рабочая станция:
GC Сервер разработан для обеспечения максимальной пропускная способность, он
масштабируется с очень высокой производительностью. Фрагментация памяти более
проблематична в серверах, чем в рабочих станциях, что делает сборку мусора очень
привлекательным предложением. В однопроцессорном сценарии оба сборщика мусора
работают одинаково: режим рабочей станции без одновременной сборки мусора. В
многопроцессорных машинах GC рабочая станция использует второй процессор для
того, чтобы одновременно запустить сборку мусора, минимизируя запаздывание. GC
сервер использует множество куч и потоков сборки мусора, чтобы обеспечить
максимальное увеличение производительности и лучшую масштабируемость.
Вы можете выбирать, какой GC использовать. Когда вы загружаете среду
выполнения в процесс, вы определяете, какой сборщик мусора использовать.
Миф: сборка мусора с помощью GC всегда медленнее, чем сборка мусора
вручную
В действительности, GC работает намного быстрее, чем ручная сборка мусора в
С. Это удивляет многих людей, поэтому требует некоторого объяснения. Прежде
всего обратите внимание, что поиск свободного пространства идет постоянно. Т.к.
все свободное пространство непрерывно, GC просто следует по указателю и
проверяет, достаточно ли там места. В С вызов malloc() обычно приводит к поиску
связного списка свободных блоков. Это может потребовать определенного времени,
особенно если ваша куча сильно фрагментирована. Чтобы еще усугубить дело,
некоторые реализации среды выполнения С блокируют кучу во время этой процедуры.
Как только память распределена или использована, список должен быть обновлен.
При использовании сборщика мусора распределение происходит свободно и память
высвобождается во время сборки мусора.
Более опытные программисты будут резервировать большие блоки памяти и
обрабатывать распределение в пределах этих блоков самостоятельно. Недостатком
этого подхода является то, что фрагментация памяти станет огромной проблемой для
программистов, и это вынудит их добавлять большое количество логики для
обработки памяти в свои приложения. И наконец, сборщик мусора не добавляет
непроизводительных издержек. Распределение происходит так же быстро или быстрее
и уплотнение проводится автоматически, это позволяет программистам
сосредоточиться на реализации приложений.
У некоторых людей, возможно, возникнет вопрос, почему GC недоступен в других
средах, например, С или С++. Ответом являются типы. В этих языках
программирования разрешено преобразование указателей на любой тип, что очень
осложняет определение того, на что ссылается указатель. В управляемой среде,
такой как CLR, гарантируется постоянство указателей, что делает возможным
использование GC. Управляемый мир является единственным местом, где мы можем
безопасно остановить выполнение потока, чтобы осуществить сборку мусора, в С++
эта операция и небезопасна, и очень ограничена.
Настройка для повышения скорости
Наибольшим беспокойством для программы в управляемом мире является сохранение
данных в памяти. Некоторые проблемы неуправляемой среды не являются таковыми в
управляемом мире: утечки памяти и указатели, указывающие на несуществующие
объекты, здесь не проблема. Однако необходимо обратить особое внимание на
ресурсы, которые остаются в памяти тогда, когда они уже не нужны.
Наиболее важным правилом для поддержания производительности и также самое
легкое правило для программистов, которые пишут неуправляемый код: отслеживайте
выделения памяти, которые необходимо сделать и освобождайте их, когда они больше
не нужны. GC никак не может знать, что вы не собираетесь использовать созданную
вами строку размером 20KB, если она является частью объекта, который остается в
рабочем состоянии. Предположим, вы где-то сохраняете этот объект и никогда не
собираетесь снова использовать эту строку. Обнуление поля даст возможность GC
позже собрать эти 20KB, даже если этот объект до сих пор нужен вам для других
целей. Если объект больше не нужен, убедитесь, что вы не сохраняете ссылки на
него. Для более маленьких объектов это является меньшей проблемой.
Следующий важный аспект, влияющий на производительность, касается деталей
очистки объекта. Как упоминалось ранее, финализация имеет огромное влияние на
производительность. Наиболее общим примером является применение управляемого
обработчика к неуправляемому ресурсу: вам надо применить некоторые методы
очистки, и здесь производительность становится проблемой. Если вы зависите от
финализации, вы открываетесь проблемам производительности, о которых говорилось
ранее. Надо помнить еще о том, что GC практически совершенно не осведомлен о
сжатии памяти в мире неуправляемого кода, таким образом вы можете использовать
огромное количество ресурсов только сохраняя указатель в управляемой куче.
Отдельный указатель не занимает много памяти, т.е. он может существовать
некоторое время до того, как понадобится сборка мусора. Чтобы обойти эти
проблемы производительности, вы должны выбрать схему разработки для всех
объектов, требующих специальной очистки.
У программиста есть три варианта при работе с очисткой объекта:
Рекомендуемая схема очистки объекта. Это объект с некоторой смесью
неуправляемых и управляемых ресурсов. Примером будет
System.Windows.Forms.Control. У него есть неуправляемый ресурс (HWND) и
потенциально управляемый ресурс (DataConnection, и т.д.). Если вы не уверены в
том, когда используются неуправляемые ресурсы, можно открыть манифест вашей
программы в ILDASM и проверить наличие ссылок на неуправляемые библиотеки.
Другой вариант - это воспользоваться vadump.exe, чтобы посмотреть, какие
ресурсы загружаются вместе с вашей программой. С помощью обоих этих способов
вы сможете понять, какие ресурсы используете.
Шаблон, приведенный ниже, предлагает пользователю отдельный рекомендуемый
способ, вместо переопределения логики очистки (переопределения Dispose(bool)).
Это обеспечивает максимальную гибкость. Сочетание максимальной скорости и
гибкости наряду с безопасным подходом делает эту схему наиболее желательной.
Пример:
Делайте это в случае, когда объект имеет только управляемые ресурсы, и вы
хотите гарантировать, что его очистка детерминирована. Примером такого объекта
является System.Web.UI.Control.
Пример:
Требуется только в исключительно редких ситуациях, и я настойчиво
рекомендую не пользоваться этим методом. Включение Finalize() свидетельствует
о том, что программист понятия не имеет, когда объект должен быть убран
сборщиком мусора, применяя при этом достаточно сложные ресурсы, требующие
специальной очистки. Такая ситуация никогда не возникнет в хорошо
разработанном проекте, если вы оказываетесь в подобной ситуации, необходимо
вернуться и разобраться, что пошло не так.
Пример: Этот вариант применим к управляемым объектам, которые указывают только на
другие управляемые объекты, которые и не являются удаляемыми, и не должны
финализироваться. Рекомендации
Рекомендации для работы с управлением памятью хорошо знакомы: освобождайте
объекты после окончания работы с ними и не допускайте, чтобы оставались
указатели на объекты. Когда доходите до очистки объекта с неуправляемыми
ресурсами, реализовывайте оба метода: и Finalize(), и Dispose(). Это
предотвратит непредсказуемость поведения в будущем и обеспечит хорошие навыки
программирования.
Метод Dispose() должен поддерживаться объектами, которые используют
неуправляемые ресурсы; однако метод Finalize() должен быть помещен только в те
объекты, которые прямо используют эти ресурсы, например, OS Handle или
распределение неуправляемой памяти. Рекомендуется создавать маленькие
управляемые объекты, такие как "упаковщики", для реализации Finalize() в
дополнение к поддержанию метода Dispose(), которые будут вызываться методом
Dispose() родительского объекта. Т.к. родительские объекты не имеют
финализатора, все дерево объектов не переживет сборку мусора невзирая на то, был
вызван метод Dispose() или нет.
Хорошим правилом при работе с финализаторами является использование их только
в наиболее примитивных объектах, которым необходима финализация. Помните:
используйте метод Finalize() только там и тогда, когда вы должны.
Основные сведения
Как и любая VM, CLR нуждается в методе компилирования промежуточного языка в
машинный код. Когда вы компилируете программу, чтобы запустить в CLR, компилятор
преобразовывает ваш источник из языка высокого уровня в комбинацию метаданных
MSIL (Microsoft Intermediate Language - промежуточный язык Microsoft). Все это
собирается в РЕ файл, который затем может выполняться на любой имеющей CLR
машине. При запуске этого исполняемого файла JIT начинает компилирование IL в
машинный код и выполнение этого кода на реальной машине. Это делается на основе
пометодной компиляции, т.е. при JIT компилировании длительность задержки зависит
от размера кода, который вы хотите запустить.
JIT компилирование проходит очень быстро и генерирует очень хороший код.
Некоторые осуществляемые им оптимизации (и некоторые пояснения каждой из них)
обсуждаются ниже. Помните, что большинство из этих оптимизаций имеет
ограничения, наложенные для обеспечения того, чтобы JIT не занимал слишком много
времени.
Рекомендации по разработке библиотеки классов
Взаимосвязь между Общей системой типов и Общеязыковой спецификацией
Рекомендации по присваиванию имен
Применение заглавных букв
BackColor
backColor
System.IO
System.Web.UI
Идентификатор
Регистр
Пример
Class
Pascal
AppDomain
Enum type
Pascal
ErrorLevel
Enum values
Pascal
FatalError
Event
Pascal
ValueChange
Exception class
Pascal
WebException
Примечание: всегда
оканчивается суффиксом Exception.
Read-only Static field
Pascal
RedValue
Interface
Pascal
IDisposable
Примечание: всегда
начинается с префикса I.
Method
Pascal
ToString
Namespace
Pascal
System.Drawing
Parameter
Camel
typeName
Property
Pascal
BackColor
Protected instance field
Camel
redValue
Примечание: Редко
используется. В свойстве предпочтительнее применять protected instance
field.
Public instance field
Pascal
RedValue
Примечание: Редко
используется. В свойстве предпочтительнее применять public instance
field.Чувствительность к регистру
namespace ee.cummings;
namespace Ee.Cummings;
void MyFunction(string a, string A)
System.Windows.Forms.Point p
System.Windows.Forms.POINT p
int Color {get, set}
int COLOR {get, set}
void calculate()
void Calculate()
Аббревиатуры
Выбор слова
AddHandler
AddressOf
Alias
And
Ansi
As
Assembly
Auto
Base
Boolean
ByRef
Byte
ByVal
Call
Case
Catch
CBool
CByte
CChar
Cdate
CDec
CDbl
Char
CInt
Class
CLng
CObj
Const
CShort
CSng
CStr
CType
Date
Decimal
Declare
Default
Delegate
Dim
Do
Double
Each
Else
ElseIf
End
Enum
Erase
Error
Event
Exit
ExternalSource
False
Finalize
Finally
Float
For
Friend
Function
Get
GetType
Goto
Handles
If
Implements
Imports
In
Inherits
Integer
Interface
Is
Let
Lib
Like
Long
Loop
Me
Mod
Module
MustInherit
MustOverride
MyBase
MyClass
Namespace
New
Next
Not
Nothing
NotInheritable
NotOverridable
Object
On
Option
Optional
Or
Overloads
Overridable
Overrides
ParamArray
Preserve
Private
Property
Protected
Public
RaiseEvent
ReadOnly
ReDim
Region
REM
RemoveHandler
Resume
Return
Select
Set
Shadows
Shared
Short
Single
Static
Step
Stop
String
Structure
Sub
SyncLock
Then
Throw
To
True
Try
TypeOf
Unicode
Until
volatile
When
While
With
WithEvents
WriteOnly
Xor
eval
extends
instanceof
package
Var
Предупреждение неразберихи с именами типов
void Write(double value);
void Write(float value);
void Write(long value);
void Write(int value);
void Write(short value);
void Write(double doubleValue);
void Write(float floatValue);
void Write(long longValue);
void Write(int intValue);
void Write(short shortValue);
Имя типа C#
Имя типа Visual Basic
Имя типа JScript
Имя типа Visual C++
Ilasm.exe представление
Универсальное имя типа
sbyte
SByte
sByte
char
int8
SByte
byte
Byte
byte
unsigned char
unsigned int8
Byte
short
Short
short
short
int16
Int16
ushort
UInt16
ushort
unsigned short
unsigned int16
UInt16
int
Integer
int
int
int32
Int32
uint
UInt32
uint
unsigned int
unsigned int32
UInt32
long
Long
long
__int64
int64
Int64
ulong
UInt64
ulong
unsigned __int64
unsigned int64
UInt64
float
Single
float
float
float32
Single
double
Double
double
double
float64
Double
bool
Boolean
boolean
bool
bool
Boolean
char
Char
char
wchar_t
Char
Char
string
String
string
String
string
String
object
Object
object
Object
object
Object double ReadDouble();
float ReadSingle();
long ReadInt64();
int ReadInt32();
short ReadInt16();
double ReadDouble();
float ReadFloat();
long ReadLong();
int ReadInt();
short ReadShort();
Рекомендации по присваиванию имени пространству имен
CompanyName.TechnologyName[.Feature][.Design]
Microsoft.Media
Microsoft.Media.Design
Рекомендации по присваиванию имен классам
public class FileStream
public class Button
public class String
Рекомендации по присваиванию имен интерфейсам
public interface IServiceProvider
public interface IFormatable
public interface IComponent
{
...
}
public class Component: IComponent
{
...
}
Рекомендации по присваиванию имен атрибутам
public class ObsoleteAttribute{}
Рекомендации по присваиванию имен перечисляемым типам
Рекомендации по присваиванию имен статическим полям
Рекомендации по присваиванию имен параметрам
Type GetType(string typeName)
string Format(string format, args() As object)
Рекомендации по присваиванию имен методам
RemoveAll()
GetCharArray()
Invoke()
Рекомендации по присваиванию имен свойствам
public class SampleClass
{
public Color BackColor
{
// Code for Get and Set accessors goes here.
}
}
public enum Color
{
// Insert code for Enum here.
}
public class Control
{
public Color Color
{
get {// Insert code here.}
set {// Insert code here.}
}
}
public enum Color {// Insert code for Enum here.}
public class Control
{
public int Color
{
get {// Insert code here.}
set {// Insert code here.}
}
}
Рекомендации по присваиванию имен событиям
public delegate void MouseEventHandler(object sender, MouseEventArgs e);
public class MouseEventArgs : EventArgs
{
int x;
int y;
public MouseEventArgs(int x, int y)
{ this.x = x; this.y = y; }
public int X { get { return x; } }
public int Y { get { return y; } }
}
Рекомендации по использованию членов класса
Рекомендации по использованию свойств
TextBox t = new TextBox();
t.DataSource = "Publishers";
t.DataField = "AuthorID";
// The data-binding feature is now active.
TextBox t = new TextBox();
t.DataField = "AuthorID";
t.DataSource = "Publishers";
// The data-binding feature is now active.
TextBox t = new TextBox();
t.DataField = "AuthorID";
t.DataSource = "Publishers";
// The data-binding feature is now active.
t.DataSource = null;
// The data-binding feature is now inactive.
public class TextBox
{
string dataSource;
string dataField;
bool active;
public string DataSource
{
get
{
return dataSource;
}
set
{
if (value != dataSource)
{
// Set the property value first, in case activate fails.
dataSource = value;
// Update active state.
SetActive(dataSource != null && dataField != null);
}
}
}
public string DataField
{
get
{
return dataField;
}
set
{
if (value != dataField)
{
// Set the property value first, in case activate fails.
dataField = value;
// Update active state.
SetActive(dataSource != null && dataField != null);
}
}
}
void SetActive(Boolean value)
{
if (value != active)
{
if (value)
{
Activate();
Text = dataBase.Value(dataField);
}
else
{
Deactivate();
Text = "";
}
// Set active only if successful.
active = value;
}
}
void Activate()
{
// Open database.
}
void Deactivate()
{
// Close database.
}
}
dataSource != null && dataField != null
void UpdateActive()
{
SetActive(dataSource != null && dataField != null);
}
class Control: Component
{
string text;
public string Text
{
get
{
return text;
}
set
{
if (!text.Equals(value))
{
text = value;
RaiseTextChangedEvent();
}
}
}
}
class Edit : Control
{
public string Text
{
get
{
return text;
}
set
{
if (text != value)
{
OnTextChanging(Event.Empty);
text = value;
}
}
}
}
class Edit : Control
{
public string Text
{
get
{
return text;
}
set
{
if (text != value)
{
OnTextChanging(Event.Empty);
text = value;
RaisePropertyChangedEvent(Edit.ClassInfo.text);
}
}
}
protected void OnPropertyChanged(PropertyChangedEvent e)
{
if (e.PropertyChanged.Equals(Edit.ClassInfo.text))
OnTextChanged(Event.Empty);
if (onPropertyChangedHandler != null)
onPropertyChangedHandler(this, e);
}
}
public string Name
get
{
return Name;
}
set
{
Name = value;
}
Type type = // Get a type.
for (int i = 0; i < type.Methods.Length; i++)
{
if (type.Methods[i].Name.Equals ("text"))
{
// Perform some operation.
}
}
class Connection
{
// The following three members should be properties
// because they can be set in any order.
string DNSName {get{};set{};}
string UserName {get{};set{};}
string Password {get{};set{};}
// The following member should be a method
// because the order of execution is important.
// This method cannot be executed until after the
// properties have been set.
bool Execute ();
}
// Change the MethodInfo Type.Method property to a method.
MethodInfo Type.Method[string name]
MethodInfo Type.GetMethod (string name, Boolean ignoreCase)
// The MethodInfo Type.Method property is changed to
// the MethodInfo Type.GetMethod method.
MethodInfo Type.GetMethod(string name)
MethodInfo Type.GetMethod (string name, Boolean ignoreCase)
Рекомендации по использованию событий
public delegate void MouseEventHandler(object sender, MouseEventArgs e);
public class MouseEvent: EventArgs {}
public class MyComponent : Component
{
static readonly object EventClick = new object();
static readonly object EventMouseDown = new object();
static readonly object EventMouseUp = new object();
static readonly object EventMouseMove = new object();
public event MouseEventHandler Click
{
add
{
Events.AddHandler(EventClick, value);
}
remove
{
Events.RemoveHandler(EventClick, value);
}
}
// Code for the EventMouseDown, EventMouseUp, and
// EventMouseMove events goes here.
// Define a private data structure to store the event delegates
}
public class Button
{
ButtonClickHandler onClickHandler;
protected virtual void OnClick(ClickEvent e)
{
// Call the delegate if non-null.
if (onClickHandler != null)
onClickHandler(this, e);
}
}
public class Button
{
ButtonClickHandler onClickHandler;
protected void DoClick()
{
// Paint button in indented state.
PaintDown();
try
{
// Call event handler.
OnClick();
}
finally
{
// Window might be deleted in event handler.
if (windowHandle != null)
// Paint button in normal state.
PaintUp();
}
}
protected virtual void OnClick(ClickEvent e)
{
if (onClickHandler != null)
onClickHandler(this, e);
}
}
public class Form1: Form
{
TreeView treeView1 = new TreeView();
void treeView1_BeforeLabelEdit(object source,
NodeLabelEditEvent e)
{
e.cancel = true;
}
}
public class Form1: Form
{
Edit edit1 = new Edit();
void edit1_TextChanging(object source, Event e)
{
throw new RuntimeException("Invalid edit");
}
Рекомендации по использованию методов
int String.IndexOf (String name);
int String.IndexOf (String name, int startIndex);
// Method #1: ignoreCase = false.
MethodInfo Type.GetMethod(String name);
// Method #2: Indicates how the default behavior of method #1 is being // changed.
MethodInfo Type.GetMethod (String name, Boolean ignoreCase);
public class SampleClass
{
readonly string defaultForA = "default value for a";
readonly string defaultForB = "default value for b";
readonly string defaultForC = "default value for c";
public void Execute()
{
Execute(defaultForA, defaultForB, defaultForC);
}
public void Execute (string a)
{
Execute(a, defaultForB, defaultForC);
}
public void Execute (string a, string b)
{
Execute (a, b, defaultForC);
}
public virtual void Execute (string a, string b, string c)
{
Console.WriteLine(a);
Console.WriteLine(b);
Console.WriteLine(c);
Console.WriteLine();
}
}
public class SampleClass
{
public void Execute(string a)
{
Execute(new string[] {a});
}
public void Execute(string a, string b)
{
Execute(new string[] {a, b});
}
public void Execute(string a, string b, string c)
{
Execute(new string[] {a, b, c});
}
public virtual void Execute(string[] args)
{
foreach (string s in args)
{
Console.WriteLine(s);
}
}
}
public class SampleClass
{
private string myString;
public MyClass(string str)
{
this.myString = str;
}
public int IndexOf(string s)
{
return IndexOf (s, 0);
}
public int IndexOf(string s, int startIndex)
{
return IndexOf(s, startIndex, myString.Length - startIndex );
}
public virtual int IndexOf(string s, int startIndex, int count)
{
return myString.IndexOf(s, startIndex, count);
}
}
void Format(string formatString, params object [] args)
Рекомендации по использованию конструктора
public sealed class Environment
{
// Private constructor prevents the class from being created.
private Environment()
{
// Code for the constructor goes here.
}
}
// Example #1.
Class SampleClass = new Class();
SampleClass.A = "a";
SampleClass.B = "b";
// Example #2.
Class SampleClass = new Class("a");
SampleClass.B = "b";
// Example #3.
Class SampleClass = new Class ("a", "b");
public class SampleClass
{
private const string defaultForA = "default value for a";
private const string defaultForB = "default value for b";
private const string defaultForC = "default value for c";
private string a;
private string b;
private string c;
public MyClass():this(defaultForA, defaultForB, defaultForC) {}
public MyClass (string a) : this(a, defaultForB, defaultForC) {}
public MyClass (string a, string b) : this(a, b, defaultForC) {}
public MyClass (string a, string b, string c)
{
this.a = a;
this.b = b;
this.c = c;
}
}
Рекомендации по использованию полей
public struct Point
{
private int xValue;
private int yValue;
public Point(int x, int y)
{
this.xValue = x;
this.yValue = y;
}
public int X
{
get
{
return xValue;
}
set
{
xValue = value;
}
}
public int Y
{
get
{
return yValue;
}
set
{
yValue = value;
}
}
}
public class Control: Component
{
private int handle;
protected int Handle
{
get
{
return handle;
}
}
}
public struct Int32
{
public static readonly int MaxValue = 2147483647;
public static readonly int MinValue = -2147483648;
// Insert other members here.
}
class SampleClass
{
string url;
string destinationUrl;
}
public struct Color
{
public static readonly Color Red = new Color(0x0000FF);
public static readonly Color Green = new Color(0x00FF00);
public static readonly Color Blue = new Color(0xFF0000);
public static readonly Color Black = new Color(0x000000);
public static readonly Color White = new Color(0xFFFFFF);
public Color(int rgb)
{ // Insert code here.}
public Color(byte r, byte g, byte b)
{ // Insert code here.}
public byte RedValue
{
get
{
return Color;
}
}
public byte GreenValue
{
get
{
return Color;
}
}
public byte BlueValue
{
get
{
return Color;
}
}
}
Рекомендации по использованию параметров
class SampleClass
{
public int Count
{
get
{
return count;
}
set
{
// Check for valid parameter.
if (count < 0 || count >= MaxValue)
throw newArgumentOutOfRangeException(
Sys.GetString(
"InvalidArgument","value",count.ToString()));
}
}
public void Select(int start, int end)
{
// Check for valid parameter.
if (start < 0)
throw new ArgumentException(
Sys.GetString("InvalidArgument","start",start.ToString()));
// Check for valid parameter.
if (end < 0)
throw new ArgumentException(
Sys.GetString("InvalidArgument","end",end.ToString()));
}
}
Рекомендации по использованию типов
Рекомендации по использованию основных классов
public class MyClass
{
private int x;
private int y;
private int width;
private int height;
BoundsSpecified specified;
public void SetBounds(int x, int y, int width, int height)
{
SetBoundsCore(x, y, width, height, this.specified);
}
public void SetBounds(int x, int y, int width, int height,
BoundsSpecified specified)
{
SetBoundsCore(x, y, width, height, specified);
}
protected virtual void SetBoundsCore(int x, int y, int width, int
height, BoundsSpecified specified)
{
// Add code to perform meaningful opertions here.
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.specified = specified;
}
}
public sealed class Runtime
{
// Private constructor prevents the class from being created.
private Runtime();
// Static method.
public static string GetCommandLine()
{
// Implementation code goes here.
}
}
Рекомендации по использованию типов значений
public struct Int32: IComparable, IFormattable
{
public const int MinValue = -2147483648;
public const int MaxValue = 2147483647;
public static string ToString(int i)
{
// Insert code here.
}
public string ToString(string format, IFormatProvider formatProvider)
{
// Insert code here.
}
public override string ToString()
{
// Insert code here.
}
public static int Parse(string s)
{
// Insert code here.
return 0;
}
public override int GetHashCode()
{
// Insert code here.
return 0;
}
public override bool Equals(object obj)
{
// Insert code here.
return false;
}
public int CompareTo(object obj)
{
// Insert code here.
return 0;
}
}
public enum FileMode
{
Append,
Create,
CreateNew,
Open,
OpenOrCreate,
Truncate
}
public FileStream(string path, FileMode mode);
[Flags]
public enum Bindings
{
IgnoreCase = 0x01,
NonPublic = 0x02,
Static = 0x04,
InvokeMethod = 0x0100,
CreateInstance = 0x0200,
GetField = 0x0400,
SetField = 0x0800,
GetProperty = 0x1000,
SetProperty = 0x2000,
DefaultBinding = 0x010000,
DefaultChangeType = 0x020000,
Default = DefaultBinding | DefaultChangeType,
ExactBinding = 0x040000,
ExactChangeType = 0x080000,
BinderBinding = 0x100000,
BinderChangeType = 0x200000
}
public void SetColor (Color color)
{
if (!Enum.IsDefined (typeof(Color), color)
throw new ArgumentOutOfRangeException();
}
Рекомендации по использованию делегатов
Рекомендации по использованию атрибутов
public class ObsoleteAttribute{}
[AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]
public class ObsoleteAttribute: Attribute {}
public class NameAttribute: Attribute
{
public NameAttribute (string username)
{
// Implement code here.
}
public string UserName
{
get
{
return UserName;
}
}
public int Age
{
get
{
return Age;
}
set
{
Age = value;
}
}
// Positional argument.
}
Рекомендации по использованию вложенных типов
// With nested types.
ListBox.SelectedObjectCollection
// Without nested types.
ListBoxSelectedObjectCollection
// With nested types.
RichTextBox.ScrollBars
// Without nested types.
RichTextBoxScrollBars
Рекомендации по раскрытию функциональных возможностей для COM
Рекомендации по генерированию и обработке ошибок
public class FileNotFoundException : Exception
{
// Implementation code goes here.
}
public class XxxException : ApplicationException
{
XxxException() {... }
XxxException(string message) {... }
XxxException(string message, Exception inner) {... }
}
class FileRead
{
void Open()
{
FileStream stream = File.Open("myfile.txt", FileMode.Open);
byte b;
// ReadByte returns -1 at end of file.
while ((b = stream.ReadByte()) != true)
{
// Do something.
}
}
}
class File
{
string fileName;
public byte[] Read(int bytes)
{
if (!ReadFile(handle, bytes))
throw NewFileIOException();
}
FileException NewFileIOException()
{
string description =
// Build localized string, include fileName.
return new FileException(description);
}
}
Тип исключительной ситуации
Базовый тип
Описание
Пример
Exception
Object
Базовый класс для всех исключительных ситуаций.
SystemException
Exception
Базовый класс для всех ошибок, генерируемых во
время выполнения.
IndexOutOfRangeException
SystemException
Формируется средой выполнения только в случае,
если массив неправильно проиндексирован.
Indexing an array outside of its valid range:
arr[arr.Length+1]
NullReferenceException
SystemException
Формируется средой выполнения только в случае,
если идет обращение к несуществующему объекту.
object o = null;
o.ToString();
InvalidOperationException
SystemException
Формируется методами в неправильном
состоянии.
Вызов Enumerator.GetNext() после перемещения
Item из основной коллекции.
ArgumentException
SystemException
Базовый класс для всех исключительных ситуаций
аргументов.
ArgumentNullException
ArgumentException
Формируется методами, которые не допускают чтобы
значение аргумента было null.
String s = null;
"Calculate".IndexOf (s);
ArgumentOutOfRangeException
ArgumentException
Формируется методами, которые проверяют,
находятся ли аргументы в данном диапазоне.
String s = "string";
s.Chars[9];
ExternalException
SystemException
Базовый класс для исключительных ситуаций,
которые случаются или нацелены на среды вне среды выполнения.
COMException
ExternalException
Исключительная ситуация, инкапсулирующая
информацию COM Hresult.
Используется в COM взаимодействии.
SEHException
ExternalException
Исключительная ситуация, инкапсулирующая Win32
структурированную информацию обработки исключительной ситуации.
Используется во взаимодействии неуправляемого
кода. public class TextReader
{
public string ReadLine()
{
try
{
// Read a line from the stream.
}
catch (Exception e)
{
throw new IOException ("Could not read from stream", e);
}
}
}
Рекомендации по использованию массивов
for (int i = 0; i < obj.myObj.Count; i++)
DoSomething(obj.myObj[i]);
public void DoSomething()
{
string s = SomeOtherFunc();
if (s.Length > 0)
{
// Do something else.
}
}
Рекомендации по использованию перезагрузки операторов
class Time
{
TimeSpan operator -(Time t1, Time t2) { }
TimeSpan Difference(Time t1, Time t2) { }
}
Символ оператора C++
Имя альтернативного метода
Имя оператора
Не определен
ToXxx или FromXxx
op_Implicit
Не определен
ToXxx или FromXxx
op_Explicit
+ (binary)
Add
op_Addition
- (binary)
Subtract
op_Subtraction
* (binary)
Multiply
op_Multiply
/
Divide
op_Division
%
Mod
op_Modulus
^
Xor
op_ExclusiveOr
& (binary)
BitwiseAnd
op_BitwiseAnd
|
BitwiseOr
op_BitwiseOr
&&
And
op_LogicalAnd
||
Or
op_LogicalOr
=
Assign
op_Assign
<<
LeftShift
op_LeftShift
>>
RightShift
op_RightShift
Не определен
LeftShift
op_SignedRightShift
Не определен
RightShift
op_UnsignedRightShift
==
Equals
op_Equality
>
Compare
op_GreaterThan
<
Compare
op_LessThan
!=
Compare
op_Inequality
>=
Compare
op_GreaterThanOrEqual
<=
Compare
op_LessThanOrEqual
*=
Multiply
op_MultiplicationAssignment
-=
Subtract
op_SubtractionAssignment
^=
Xor
op_ExclusiveOrAssignment
<<=
LeftShift
op_LeftShiftAssignment
%=
Mod
op_ModulusAssignment
+=
Add
op_AdditionAssignment
&=
BitwiseAnd
op_BitwiseAndAssignment
|=
BitwiseOr
op_BitwiseOrAssignment
,
Не присвоено
op_Comma
/=
Divide
op_DivisionAssignment
--
Decrement
op_Decrement
++
Increment
op_Increment
- (одинарный)
Negate
op_UnaryNegation
+ (одинарный)
Plus
op_UnaryPlus
~
OnesComplement
op_OnesComplement
Рекомендации по применению метода Equals и оператора равенства (==)
Рекомендации по приведению типов
Общие шаблоны разработки
Применение методов Finalize и Dispose для очистки неуправляемых ресурсов
// Design pattern for a base class.
public class Base: IDisposable
{
//Implement IDisposable.
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// Free other state (managed objects).
}
// Free your own state (unmanaged objects).
// Set large fields to null.
}
// Use C# destructor syntax for finalization code.
~Base()
{
// Simply call Dispose(false).
Dispose (false);
}
// Design pattern for a derived class.
public class Derived: Base
{
protected override void Dispose(bool disposing)
{
if (disposing)
{
// Release managed resources.
}
// Release unmanaged resources.
// Set large fields to null.
// Call Dispose on your base class.
base.Dispose(disposing);
}
// The derived class does not have a Finalize method
// or a Dispose method with parameters because it inherits
// them from the base class.
}
// Do not make this method virtual.
// A derived class should not be allowed
// to override this method.
public void Close()
{
// Call the Dispose method with no parameters.
Dispose();
}
Реализация метода Equals
using System;
class SampleClass
{
public static void Main()
{
Object obj1 = new Object();
Object obj2 = new Object();
Console.WriteLine(obj1.Equals(obj2));
obj1 = obj2;
Console.WriteLine(obj1.Equals(obj2));
}
}
False
True
using System;
class Point: object
{
int x, y;
public override bool Equals(Object obj)
{
// Check for null values and compare run-time types.
if (obj == null || GetType() != obj.GetType())
return false;
Point p = (Point)obj;
return (x == p.x) && (y == p.y);
}
public override int GetHashCode()
{
return x ^ y;
}
}
class Point3D: Point
{
int z;
public override bool Equals(Object obj)
{
return base.Equals(obj) && z == ((Point3D)obj).z;
}
public override int GetHashCode()
{
return base.GetHashCode() ^ z;
}
}
using System;
class Rectangle
{
Point a, b;
public override bool Equals(Object obj)
{
if (obj == null || GetType() != obj.GetType()) return false;
Rectangle r = (Rectangle)obj;
// Use Equals to compare instance variables.
return a.Equals(r.a) && b.Equals(r.b);
}
public override int GetHashCode()
{
return a.GetHashCode() ^ b.GetHashCode();
}
}
public struct Complex
{
double re, im;
public override bool Equals(Object obj)
{
return obj is Complex && this == (Complex)obj;
}
public override int GetHashCode()
{
return re.GetHashCode() ^ im.GetHashCode();
}
public static bool operator ==(Complex x, Complex y)
{
return x.re == y.re && x.im == y.im;
}
public static bool operator !=(Complex x, Complex y)
{
return !(x == y);
}
}
Использование функции обратного вызова
Использование ожиданий (Time-Out)
server.PerformOperation(timeout);
server.Timeout = timeout;
server.PerformOperation();
public class Server
{
void PerformOperation(TimeSpan timeout)
{
// Insert code for the method here.
}
}
public class TestClass
{
public Server server = new Server();
server.PerformOperation(new TimeSpan(0,15,0));
}
class Server
{
TimeSpan defaultTimeout = new TimeSpan(1000);
void PerformOperation()
{
this.PerformOperation(OperationTimeout);
}
void PerformOperation(TimeSpan timeout)
{
// Insert code here.
}
TimeSpan OperationTimeout
{
get
{
return defaultTimeout;
}
}
}
void OnReceiveCompleted(Object sender, ReceiveAsyncEventArgs asyncResult)
{
MessageQueue queue = (MessageQueue) sender;
// The following code will throw an exception
// if BeginReceive has timed out.
Message message = queue.EndReceive(asyncResult.AsyncResult);
Console.WriteLine("Message: " + (string)message.Body);
queue.BeginReceive(new TimeSpan(1,0,0));
}
Безопасность библиотек классов
Применение прав доступа для защиты объектов
Полностью надежный код библиотеки классов
Меры предосторожности для высоконадежного кода
Производительность
Заключение
Рекомендации по организации поточной обработки
lock(this)
{
myField++;
}
if (x == null)
{
lock (this)
{
if (x == null)
{
x = y;
}
}
}
System.Threading.Interlocked.CompareExchange(ref x, y, null);
Рекомендации по асинхронному программированию
Схемы разработки для асинхронного программирования
public class PrimeFactorizer
{
public bool Factorize(long factorizableNum,
ref long primefactor1,
ref long primefactor2)
{
primefactor1 = 1;
primefactor2 = factorizableNum;
// Factorize using a low-tech approach.
for (int i=2;i<factorizableNum;i++)
{
if (0 == (factorizableNum % i))
{
primefactor1 = i;
primefactor2 = factorizableNum / i;
break;
}
}
if (1 == primefactor1 )
return false;
else
return true ;
}
}
// Define the delegate.
public delegate bool FactorizingCallback(long factorizableNum,
ref long primefactor1,
ref long primefactor2);
// Create an instance of the Factorizer.
PrimeFactorizer pf = new PrimeFactorizer();
// Create a delegate on the Factorize method on the Factorizer.
FactorizingDelegate fd = new FactorizingDelegate(pf.Factorize);
public class FactorizingCallback : Delegate
{
public bool Invoke(ulong factorizableNum,
ref ulong primefactor1, ref ulong primefactor2);
// Supplied by the compiler.
public IAsyncResult BeginInvoke(ulong factorizableNum,
ref unsigned long primefactor1,
ref unsigned long primefactor2, AsyncCallback cb,
Object AsyncState);
// Supplied by the compiler.
public bool EndInvoke(ref ulong primefactor1,
ref ulong primefactor2, IAsyncResult ar);
}
public delegate AsyncCallback (IAsyncResult ar);
public interface IAsyncResult
{
// Returns true if the asynchronous operation has completed.
bool IsCompleted { get; }
// Caller can use this to wait until operation is complete.
WaitHandle AsyncWaitHandle { get; }
// The delegate object for which the async call was invoked.
Object AsyncObject { get; }
// The state object passed in through BeginInvoke.
Object AsyncState { get; }
// Returns true if the call completed synchronously.
bool CompletedSynchronously { get; }
}
public class ProcessFactorizeNumber
{
private long _ulNumber;
public ProcessFactorizeNumber(long number)
{
_ulNumber = number;
}
[OneWayAttribute()]
public void FactorizedResults(IAsyncResult ar)
{
long factor1=0, factor2=0;
// Extract the delegate from the AsynchResult.
FactorizingCallback fd =
(FactorizingCallback) ((AsyncResult)ar).AsyncDelegate;
// Obtain the result.
fd.EndInvoke(ref factor1, ref factor2, ar);
// Output the results.
Console.Writeline("On CallBack: Factors of {0} : {1} {2}",
_ulNumber, factor1, factor2);
}
}
// Async Variation 1.
// The ProcessFactorizeNumber.FactorizedResults callback function
// is called when the call completes.
public void FactorizeNumber1()
{
// Client code.
PrimeFactorizer pf = new PrimeFactorizer();
FactorizingCallback fd = new FactorizingCallback(pf.Factorize);
long factorizableNum = 1000589023, temp=0;
// Create an instance of the class that
// will be called when the call completes.
ProcessFactorizedNumber fc =
new ProcessFactorizedNumber(factorizableNum);
// Define the AsyncCallback delegate.
AsyncCallbackDelegate cb = new AsyncCallback(fc.FactorizedResults);
// Any object can be the state object.
Object state = new Object();
// Asynchronously invoke the Factorize method on pf.
// Note: If you have pure out parameters, you do not need the
// temp variable.
IAsyncResult ar = fd.BeginInvoke(factorizableNum, ref temp, ref temp,
cb, state);
// Proceed to do other useful work.
// Async Variation 2.
// Waits for the result.
// Asynchronously invoke the Factorize method on pf.
// Note: If you have pure out parameters, you do not need
// the temp variable.
public void FactorizeNumber2()
{
// Client code.
PrimeFactorizer pf = new PrimeFactorizer();
FactorizingCallback fd = new FactorizingCallback(pf.Factorize);
long factorizableNum = 1000589023, temp=0;
// Create an instance of the class
// to be called when the call completes.
ProcessFactorizedNumber fc =
new ProcessFactorizedNumber(factorizableNum);
// Define the AsyncCallback delegate.
AsyncCallback cb = new AsyncCallback(fc.FactorizedResults);
// Any object can be the state object.
Object state = new Object();
// Asynchronously invoke the Factorize method on pf.
IAsyncResult ar = fd.BeginInvoke(factorizableNum, ref temp, ref temp,
null, null);
ar.AsyncWaitHandle.WaitOne(10000, false);
if(ar.IsCompleted)
{
int factor1=0, factor2=0;
// Obtain the result.
fd.EndInvoke(ref factor1, ref factor2, ar);
// Output the results.
Console.Writeline("Sequential : Factors of {0} : {1} {2}",
factorizableNum, factor1, factor2);
}
}
Заключение
public class File
{
// Other methods for this class go here.
// Synchronous read method.
long Read(Byte[] buffer, long NumToRead);
// Asynchronous read method.
IAsyncResult BeginRead(Byte[] buffer, long NumToRead,
AsyncCallbackDelegate cb);
long EndRead(IAsyncResult ar);
// Synchronous write method.
long Write(Byte[] buffer, long NumToWrite);
// Asynchrnous write method.
IAsyncResult BeginWrite(Byte[] buffer, long NumToWrite,
AsyncCallbackDelegate cb);
long EndWrite(IAsyncResult ar);
}
Приложение А. Методы повышения производительности .NET приложений
Обзор
Рекомендации по повышению производительности для всех приложений
public static void Main(string[] args){
int j = 0;
for(int i = 0; i < 10000; i++){
try{
j = i;
throw new System.Exception();
} catch {}
}
System.Console.Write(j);
return;
}
using System;
namespace ConsoleApplication{
public struct foo{
public foo(double arg){ this.y = arg; }
public double y;
}
public class bar{
public bar(double arg){ this.y = arg; }
public double y;
}
class Class1{
static void Main(string[] args){
System.Console.WriteLine("starting struct loop...");
for(int i = 0; i < 50000000; i++)
{foo test = new foo(3.14);}
System.Console.WriteLine("struct loop complete.
starting object loop...");
for(int i = 0; i < 50000000; i++)
{bar test2 = new bar(3.14); }
System.Console.WriteLine("All done");
}
}
}
public static void Main(string[] args) {
string s = "monkeys!";
int dummy = 0;
System.Text.StringBuilder sb = new System.Text.StringBuilder(s);
for(int i = 0; i < 1000000; i++)
sb.Append(s);
s = sb.ToString();
//foreach (char c in s) dummy++;
for (int i = 0; i < 1000000; i++)
dummy++;
return;
}
}
namespace ConsoleApplication1.Feedback{
using System;
public class Feedback{
public Feedback(){
text = "You have ordered: \n";
}
public string text;
public static int
Main(string[] args) {
Feedback test = new Feedback();
String str = test.text;
for(int i=0;i<50000;i++){
str = str + "blue_toothbrush";
}
System.Console.Out.WriteLine(
"done");
return 0;
}
}
}
namespace ConsoleApplication1.Feedback{
using System;
public class Feedback{
public Feedback(){
text = "You have ordered: \n";
}
public string text;
public static int Main(string[]
args) {
Feedback test = new Feedback();
System.Text.StringBuilder SB =
new System.Text.StringBuilder(
test.text);
for(int i=0;i<50000;i++){
SB.Append("blue_toothbrush");
}
System.Console.Out.WriteLine(
"done");
return 0;
}
}
}
Состояние кода
Время
Framework JITed
ShowFormComplex JITed3.4 c
Framework Precompiled, ShowFormComplex JITed
2.5 c
Framework Precompiled, ShowFormComplex
Precompiled
2.1c
C#
Visual Basic 7
Assignment (jagged)
Assignment
(rectangular)14.16
8.3712.24
8.62
Neural Net (jagged)
Neural net
(rectangular)4.48
3.004.58
3.13
Numeric Sort (jagged)
Numeric Sort
(rectangular)4.88
2.055.07
2.06Рекомендации по доступу к базам данных
ADO
SQL
DataSet
801
2507
DataReader
1083
4585 SqlConnection conn = new SqlConnection(
"Server=mysrv01;
= Integrated Security true;
Enlist=false");
public DataSet SelectSqlSrvRows(DataSet dataset,string connection,string query){
SqlConnection conn = new SqlConnection(connection);
SqlDataAdapter adapter = new SqlDataAdapter();
adapter.SelectCommand = new SqlCommand(query, conn);
adapter.MissingSchemaAction = MissingSchemaAction.AddWithKey;
adapter.Fill(dataset);
return dataset;
}
Рекомендации по повышению производительности для ASP.NET приложений
Приложение Б. Вопросы производительности .NET Framework
Обзор
Сборка мусора
Foo a = new Foo(); // Allocate memory for the object and Initialize
…a… // Use the object
delete a; // Tear down the state of the object, clean up
// and free the memory for that object
Foo a = new Foo(); // Allocate memory for the object and Initialize
…a…
// Use the object (it is strongly reachable)
a = null; // A becomes unreachable (out of scope, nulled, etc)
// Eventually a collection occurs, and a?s resources
// are torn down and the memory is freed
public class MyClass : IDisposable {
public void Dispose() {
Dispose(true);
GC.SuppressFinalizer(this);
}
protected virtual void Dispose(bool disposing) {
if (disposing) {
…
}
…
}
~MyClass() {
Dispose(false);
}
}
public class MyClass : IDisposable {
public virtual void Dispose() {
…
}
public class MyClass {
…
~MyClass() {
…
}
JIT
До
После
x = 5 + 7
x = 12
До | После |
x = a | x = a |
y = x | y = a |
z = 3 + y | z = 3 + a |
До | После |
… x=foo(4, true); … } foo(int a, bool b){ if(b){ return a + 5; } else { return 2a + bar(); } |
… x = 9 … } foo(int a, bool b){ if(b){ return a + 5; } else { return 2a + bar(); } |
До | После |
for(i=0; i< a.length;i++){ if(i < a.length()){ a[i] = null } else { raise IndexOutOfBounds; } } |
for(int i=0; i<a.length; i++){ a[i] = null;} |
До | После |
for(i=0; i< 3; i++){ print("flaming monkeys!"); } |
print("flaming monkeys!"); print("flaming monkeys!"); print("flaming monkeys!"); |
До | После |
x = 4 + y z = 4 + y |
x = 4 + y z = x |
Когда код должен быть JIT компилирован?
Здесь приведены этапы, которые проходит ваш код во время выполнения:
Миф: программы, использующие JIT выполняются медленнее прекомпилированных программ
Это редкий случай. Затраты, связанные с обработкой JIT нескольких методов, незначительны в сравнении со временем, которое затрачивается на считывание нескольких страниц с диска, и методы подвергаются JIT только тогда, когда они необходимы. Время, затраченное на JIT, настолько мало, что оно практически всегда незаметно, и если метод однажды был подвергнут JIT, вы никогда не будете снова тратить время на его обработку.
Более важным является то, что JIT может выполнять некоторые оптимизации, которые недоступны обычным компиляторам, такие как оптимизации, характерные для CPU, и настройка кэша.
Оптимизации, присущие только JIT
Поскольку JIT активизируется во время выполнения, он использует большое количество информации, которую не может использовать компилятор. Это позволяет осуществлять некоторые оптимизации, которые доступны только во время выполнения:
Перекомпилирование кода (использование ngen.exe)
Для производителей приложения привлекательна возможность прекомпиляции кода во время инсталляции. Компания Microsoft предоставляет эту возможность в форме ngen.exe, который позволит единожды запустить нормальный JIT компилятор по всей вашей программе и сохранит результат. Поскольку оптимизация только времени выполнения не может быть осуществлена во время прекомпиляции, генерируемый код не всегда так же хорош, как при нормальной JIT компиляции. Но из-за того, что не надо на лету обрабатывать JIT методы, затраты на запуск намного меньшие, и некоторые программы будут запускаться заметно быстрее. В будущем ngen.exe сможет делать больше, чем просто запускать ту же JIT компиляцию времени выполнения: более активные оптимизации, чем время выполнения, раскрытие оптимизации порядка загрузки разработчикам (оптимизация способа упаковки кода в VM страницы) и более сложные, требующие затрат времени оптимизации, которые могут воспользоваться преимуществом времени во время прекомпиляции.
Сокращение времени запуска помогает в двух случаях и для всего остального не может соревноваться с оптимизацией только времени выполнения, которую может делать обычная JIT компиляция. Первая ситуация - когда огромное количество методов вызывается рано в вашей программе. Вам придется подвергнуть JIT компиляции сразу много методов, что будет выражено в неприемлемо долгом времени загрузки. JIT прекомпиляция может иметь смысл, если это мешает вам, но не должна стать правилом. Прекомпиляция также имеет смысл в случае совместного использования больших библиотек, т.к. вы намного чаще тратите время на их загрузку. Microsoft прекомпилирует свои библиотеки для CLR, потому что большинство приложений будет их использовать.
ngen.exe легко использовать, чтобы увидеть полезна ли прекомпиляция для вас, рекомендуем попробовать воспользоваться ею. Однако в большинстве случаев действительно лучше использовать нормальную JIT компиляцию и пользоваться преимуществами оптимизации времени выполнения. Они обеспечивают огромный выигрыш времени, что более чем компенсирует затраты на загрузку в большинстве случаев.
Повышение производительности
Для программиста существует только две вещи, которые действительно ничего не стоят. Первое, то что JIT компиляция очень интеллектуальна. Не пытайтесь передумать компьютер. Пишите код так, как вам удобно. Например, предположим у вас есть следующий код:
… for(int i = 0; i < myArray.length; i++){ … } … |
int l = myArray.length; for(int i = 0; i < l; i++){ … } … |
Некоторые программисты верят, что они могут увеличить скорость тем, что уберут длинные расчеты и сохранят их во временные переменные, как в примере справа.
Истина в том, что такие оптимизации были полезны примерно в течение 10 лет: современные компиляторы способны осуществлять эти оптимизации. Кстати, иногда такие вещи могут действительно навредить производительности. В примере, приведенном выше, компилятор, вероятно, будет проверять то, что длина myArray постоянна, и вставит постоянную в сравнение цикла for. Но код справа может заставить компилятор думать, что это значение должно быть сохранено в регистре, т.к. l активна на всем протяжении цикла. Вывод таков: пишите наиболее читабельный и осмысленный код. Нет смысла стараться передумать компьютер, а иногда это может даже навредить.
Основные сведения
Межпроцессное взаимодействие становится все более и более распространенным. Из соображений стабильности и безопасности OS содержат приложения в разных адресных пространствах. Простым примером является способ, которым в NT выполняется 16-битное приложение: при запуске в отдельном процессе одно приложение не может пересекаться с выполнением другого. Проблемой здесь являются затраты на контекстные переключения и открытие связи между процессами. Эти операции имеют огромное негативное влияние на производительность. В серверных приложениях, которые обычно выполняют несколько web приложений, это основной непроизводительный расход как в производительности, так и в масштабируемости.
CLR представляет концепцию AppDomain, который похож на процесс, в котором для приложения есть модульное пространство. Однако AppDomains не ограничен одним процессом. Благодаря безопасности типов, предоставляемой управляемым кодом, есть возможность запуска двух совершенно независимых AppDomains в одном процессе. Для ситуаций, в которых вы обычно тратили большое количество времени выполнения на межпроцессное взаимодействие, выигрыш в производительности огромен: IPC между сборками в пять раз быстрее, чем между процессами в NT. Значительно уменьшая эти затраты, вы получаете и выигрыш в скорости, и новую возможность во время разработки программы: теперь имеет смысл использовать разделенные процессы там, где раньше это могло быть слишком дорого. Возможность запуска множества программ в одном процессе с той же системой безопасности, как раньше, имеет громадные последствия для масштабируемости и безопасности.
В OS нет поддержки для AppDomains. AppDomains обрабатывается хостом CLR, таким как представлен в ASP.NET, выполняемый оболочкой, или Microsoft Internet Explorer. Вы также можете написать собственный хост. Каждый хост определяет домен, применяемый по умолчанию, который загружается при первой загрузке приложения и закрывается только после завершения процесса. Когда вы загружаете другие сборки в процесс, вы можете определить, что они будут загружаться в определенный AppDomain, и установить различные политики безопасности для каждой из них. Это детально описано в документации Microsoft .NET Framework SDK.
Повышение производительности
Чтобы эффективно использовать AppDomains, вы должны подумать о том, какое приложение вы пишите, и какую работу оно должно выполнять. Применение AppDomains наиболее эффективно, если ваше приложение соответствует некоторым из следующих характеристик:
Пример ситуации, в которой полезны AppDomains, можно найти в сложном приложении ASP.NET. Предположим, что вы хотите усилить изоляцию двух различных vRoots: в собственном пространстве вам надо поместить каждый vRoot в отдельный процесс. Это, а также переключение контекста между ними, требует довольно больших затрат. В управляемом мире каждый vRoot может быть отдельным AppDomain. Это сохраняет требуемую изоляцию и полностью исключает непроизводительные издержки.
AppDomains - это то, что вы должны использовать только, если ваше приложение достаточно сложное и требует тесной работы с другими процессами или другими собственными экземплярами. Т.к. меж-доменная коммуникация намного более быстрая, чем коммуникация между процессами, затраты на запуск и закрытие AppDomain в действительности могут быть намного большими. AppDomains могут навредить производительности при неправильном использовании, поэтому убедитесь, что вы используете их в нужной ситуации. Обратите внимание, что только управляемый код может загружаться в AppDomain, т.к. нельзя гарантировать безопасность неуправляемого кода.
Сборки, которые совместно используются множеством AppDomains, должны быть JIT скомпилированы для каждого домена, для того чтобы сохранить изолированность доменов. В результате создается много дубликатов кода и идет пустое растрачивание памяти. Рассмотрите случай приложения, которое отвечает на запросы с помощью определенного XML сервиса. Если определенные запросы должны быть изолированы друг от друга, вы должны направлять их к разным AppDomains. В данном случае проблема в том, что каждому AppDomain теперь нужны одни и те же XML библиотеки, и одна и та же сборка будет загружаться много раз.
Единственным выходом из сложившейся ситуации является объявление Домен-нейтральной сборки. Это означает, что не допускаются прямые ссылки и изоляция поддерживается через преобразование логических адресов в физические. Это сохраняет время, т.к. сборка JIT компилируется только однажды. Это также бережет память, т.к. ничего не дублируется. К сожалению, из-за необходимости преобразования логических адресов в физические возникают потери производительности. Объявление сборки домен-нейтральной приводит к выигрышу в производительности только тогда, когда дело касается памяти или когда слишком много времени тратится на JIT компилирование кода. Такие сценарии распространены в случае большой сборки, которая совместно используется несколькими доменами.
Основные сведения
Безопасность доступа к коду является мощной, исключительно полезной характеристикой. Она предлагает пользователям безопасное выполнение ненадежного кода, защищает от злонамеренного программного обеспечения и некоторых видов атак, позволяет контролируемый, основанный на идентичности доступ к ресурсам. В машинном коде обеспечить безопасность очень сложно, т.к. очень низка безопасность типов и память обрабатывает программист. В CLR среда выполнения достаточно знает о запуске кода, чтобы добавить надежную поддержку безопасности. Для большинства программистов это является новшеством.
Безопасность влияет как на скорость, так и на размер working set приложения. И, как и в большинстве областей программирования, то, как программист использует систему безопасности, может иметь огромное влияние на производительность. Система безопасности разрабатывается с учетом производительности. Однако есть несколько вещей, которые вы можете сделать в системе безопасности, чтобы еще хоть немного повысить производительность.
Повышение производительности
Проверка безопасности обычно требует проверки стека вызовов, чтобы убедиться в том, что код, вызывающий текущий метод, имеет соответствующие допуски. Среда выполнения имеет несколько оптимизаций, которые дают возможность не проходить по всему стеку, но кое-что может сделать и программист. Это привело нас к упоминанию обязательной и декларативной безопасности: декларативная система безопасности присваивает типам ее членов различные права, в то время как обязательная система безопасности создает объект безопасности и осуществляет операции над ним.
Есть два способа оптимизировать систему безопасности:
В первую очередь вы должны сконцентрироваться на переносе на время компоновки как можно большего количества этих проверок. Помните, что это может оказывать влияние на безопасность вашего приложения, поэтому убедитесь, что вы не перемещаете проверки в компоновщик, который зависит от состояния времени выполнения. Как только максимально возможное количество проверок перемещено на время компоновки, вы должны оптимизировать проверки времени выполнения, используя декларативную и обязательную безопасность: выберите, какая из них больше подходит для конкретного, выбранного вами, типа проверки.
Основные сведения
Remoting технология в .NET распространяет богатую систему типов и функциональных возможностей CLR по всей сети. Используя XML, SOAP и HTTP вы можете вызывать процедуры и передавать объекты удаленно так, как будто они размещены на одном и том же компьютере. Вы можете рассматривать это, как .NET версию DCOM или CORBA, в которой реализован расширенный набор их функциональных возможностей.
Это исключительно полезно в серверной среде, когда вы имеете несколько серверов, размещающих различные сервисы, которые общаются друг с другом, чтобы обеспечить непрерывную связь этих сервисов. Также улучшается и масштабируемость, т.к. процессы могут физически размещаться на множестве компьютеров без потери функциональности.
Повышение производительности
Т.к. remoting часто имеет проблемы в показателях времени ожидания сети, в CLR применяются те же правила, как обычно: попытайтесь минимизировать поток посылаемой вами информации обмена и освободить программу от необходимости ожидания возвращения уделенного вызова. Вот некоторые правила, которым необходимо следовать при использовании удаления, чтобы увеличить производительность:
Основные сведения
Гибкость, доступная объекту, обеспечивается за счет очень небольших затрат производительности. Объекты, размещаемые в куче, требуют больше времени для распределения, доступа и обновления, чем объекты размещаемые в стеке. Это происходит потому, что, например, структура в С++ намного более эффективна, чем объект. Конечно, объекты могут делать то, что не под силу структуре, и намного более универсальны.
Но иногда вам не нужна вся эта гибкость. Иногда вы хотите использовать что-то настолько же простое, как структура, и не хотите лишних затрат производительности. CLR предоставляет возможность определить то, что называется типом значения, и интерпретируется во время компилирования как структура. Типы значений управляются стеком и предоставляют вам скорость структуры. Как и ожидалось, они также имеют ограниченную гибкость (например, у них нет наследования). Но в экземплярах, в которых вам нужно использовать структуру, типы значений обеспечивают невероятное повышение скорости.
Повышение производительности
Типы значений полезны только тогда, когда вы используете их как структуры. Если надо интерпретировать тип значений как объект, среда выполнения обработает для вас упаковку и распаковку объекта. Однако это требует даже больших затрат, чем создание его сразу как объекта!
Далее приведен пример простой проверки, проведенной для сравнения времени, необходимого для создания большого числа объектов и типов значений:
using System; using System.Collections; namespace ConsoleApplication{ public struct foo{ public foo(double arg){ this.y = arg; } public double y; } public class bar{ public bar(double arg){ this.y = arg; } public double y; } class Class1{ static void Main(string[] args){ Console.WriteLine("starting struct loop...."); int t1 = Environment.TickCount; for (int i = 0; i < 25000000; i++) { foo test1 = new foo(3.14); foo test2 = new foo(3.15); if (test1.y == test2.y) break; // prevent code from being eliminated JIT } int t2 = Environment.TickCount; Console.WriteLine("struct loop: (" + (t2-t1) + "). starting object loop...."); t1 = Environment.TickCount; for (int i = 0; i < 25000000; i++) { bar test1 = new bar(3.14); bar test2 = new bar(3.15); if (test1.y == test2.y) break; // prevent code from being eliminated JIT } t2 = Environment.TickCount; Console.WriteLine("object loop: (" + (t2-t1) + ")"); }
Сами проверьте этот пример. Разница во времени порядка нескольких секунд. Теперь давайте изменим программу таким образом, чтобы среде выполнения пришлось упаковывать и распаковывать нашу структуру. Обратите внимание, что преимущества в скорости от использования типов значений полностью исчезнут! Вывод: типы значений надо использовать только в исключительно редких ситуациях, когда вы не используете их, как объекты. Ознакомьтесь с этими ситуациями, поскольку выигрыш производительности при правильном использовании типов значений очень велик.
using System; using System.Collections; namespace ConsoleApplication{ public struct foo{ public foo(double arg){ this.y = arg; } public double y; } public class bar{ public bar(double arg){ this.y = arg; } public double y; } class Class1{ static void Main(string[] args){ Hashtable boxed_table = new Hashtable(2); Hashtable object_table = new Hashtable(2); System.Console.WriteLine("starting struct loop..."); for(int i = 0; i < 10000000; i++){ boxed_table.Add(1, new foo(3.14)); boxed_table.Add(2, new foo(3.15)); boxed_table.Remove(1); } System.Console.WriteLine("struct loop complete. starting object loop..."); for(int i = 0; i < 10000000; i++){ object_table.Add(1, new bar(3.14)); object_table.Add(2, new bar(3.15)); object_table.Remove(1); } System.Console.WriteLine("All done"); } } }
Типы значений широко используются в Microsoft: все простые типы являются типами значений. Использовать типы значений везде, где вы хотите использовать структуру. Если вы не будете упаковывать/распаковывать их, они обеспечат существенный выигрыш в производительности.
Еще одна очень важная вещь, о которой надо упомянуть, - это то, что типы значений не нуждаются в маршаллинге при взаимодействии. Поскольку маршаллинг является одним из наибольших поглотителей производительности при взаимодействии с машинным кодом, использование типов значений в качестве аргументов, будет единственной возможностью улучшить производительность.