Сдам Сам

ПОЛЕЗНОЕ


КАТЕГОРИИ







Проверяемые и непроверяемые операции для элементарных типов





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

Byte b = 100;

b = (Byte) (b + 200); // После этого b равно 44

 

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

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

Внимание! Арифметические операции в CLR выполняются только над 32- и 64-разрядными числами. Поэтому b и 200 сначала преобразуются в 32-разрядные (или в 64-разрядные, если хотя бы одному из операндов недостаточно 32 разрядов) значения, а затем уже суммируются. Поэтому 200 и b (их размер меньше 32-разрядов) преобразуются в 32-разрядные значения и суммируются. Полученное 32-разрядное число, прежде чем поместить его обратно в переменную b, нужно привести к типу Byte. Так как в данном случае С# не делает неявного приведения типа, во вторую строку потребовалось ввести приведение к типу Byte.

 

В CLR есть IL-команды, позволяющие компилятору по-разному реагировать на переполнение. Так, суммирование двух чисел выполняет команда add, не реагирующая на переполнение, и команда add.ovf, которая при переполнении генерирует исключение System.OverflowException. Кроме того, в CLR есть аналогичные IL-команды для вычитания (sub/sub.ovf), умножения (mul/mul.ovf) и преобразования данных (conv/conv.ovf).

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

Чтобы включить управление процессом обработки переполнения на этапе компиляции, добавьте в командную строку компилятора параметр /checked+. Он сообщает компилятору, что для выполнения сложения, вычитания, умножения и преобразования должны быть сгенерированы IL-команды с проверкой переполнения. Такой код медленнее, так как CLR тратит время на проверку этих операций, ожидая переполнение. Когда оно возникает, CLR генерирует исключение OverflowException. Код приложения должен предусматривать корректную обработку этого исключения.

Однако программистам вряд ли подойдет включение или отключение режима проверки переполнения во всем коде. У них должна быть возможность самим решать, как реагировать на переполнение в каждом случае. И С# предлагает такой механизм гибкого управления проверкой в виде операторов checked и unchecked. Например (предполагается, что компилятор по умолчанию создает код без проверки):

UInt32 invalid = unchecked((UInt32) -1); // ОК

 

А вот пример с использованием оператора checked (предполагается, что компилятор по умолчанию создает код без проверки):

Byte b = 100;

b = checked((Byte) (b + 200)); // Генерируется OverflowException

 

Здесь b и 200 преобразуются в 32-разрядные числа и суммируются; результат равен 300. Затем преобразование 300 в Byte генерирует исключение OverflowException. Если приведение к Byte вывести из оператора checked, исключения не будет:

b = (Byte) checked(b + 200); // b содержит 44; нет OverflowException

 

Наряду с операторами checked и unchecked, в С# есть одноименные инструкции, позволяющие включить проверяемые или непроверяемые выражения внутрь блока:

Checked

{ // Начало проверяемого блока

Byte b = 100;

b = (Byte) (b + 200); // Это выражение проверяется на переполнение

} // Конец проверяемого блока

 

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

Checked

{ // Начало проверяемого блока

Byte b = 100;

b += 200; // Это выражение проверяется на переполнение

} // Конец проверяемого блока

 

Внимание! Установка режима контроля переполнения не влияет на работу метода, вызываемого внутри оператора или инструкции checked, так как действие оператора (и инструкции) checked распространяется только на выбор IL-команд сложения, вычитания, умножения и преобразования данных. Пример: checked { // Предположим, SomeMethod пытается поместить 400 в Byte SomeMethod(400); /* Возникновение OverflowException в SomeMethod зависит от наличия в нем операторов проверки*/ }

 

Используя checked и unchecked, учитывайте следующее.

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

2. Включайте в блок unchecked ту часть кода, в которой переполнение не является проблемой, например при вычислении контрольной суммы.

3. Для кода, где нет операторов и блоков checked и unchecked, делают предположение, что генерация исключения необходима при переполнении, например при вычислении (скажем, простых чисел), когда входные данные известны и переполнение считается ошибкой.

 

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

Внимание! Тип SystemDecimal стоит особняком. В отличие от многих языков программирования (включая С# и Visual Basic) в CLR Decimal не относится к элементарным типам. В CLR нет IL-команд для работы со значениями Decimal. В документации по.NET Framework сказано, что тип Decimal имеет открытые статические методы-члены Add, Subtract, Multiply, Divide и прочие, а также перегруженные операторы +, -, *, / и т. д. При компиляции кода с Decimal компилятор создает вызовы членов Decimal, которые и выполняют реальную работу. Поэтому значения Decimal обрабатываются медленнее элементарных типов CLR. Кроме того, раз нет IL-команд для манипуляции числами Decimal, то не будут иметь эффекта ни checked и unchecked, ни соответствующие параметры командной строки компилятора. И любая «небезопасная» операция над Decimal обязательно вызовет исключение OverflowException.

Ссылочные и значимые типы

CLR поддерживает две разновидности типов: ссылочные (reference types) и значимые (value types). Большинство типов в FCL — ссылочные, но программисты чаще всего используют значимые. Память для ссылочных типов всегда выделяется из управляемой кучи, а оператор С# new возвращает адрес памяти, где размещается сам объект. При работе с ссылочными типами имейте в виду следующие обстоятельства, относящиеся к производительности приложения:

§ память для ссылочных типов всегда выделяется из управляемой кучи;

§ каждый объект, размещаемый в куче, имеет некоторые дополнительные члены, подлежащие инициализации;

§ незанятые полезной информацией байты объекта обнуляются (это касается полей);

§ размещение объекта в управляемой куче со временем инициирует сборку мусора.

 

Если бы все типы были ссылочными, эффективность приложения резко упала бы. Представьте, насколько замедлится выполнение приложения, если при каждом обращении к значению типа Int32 будет выделяться память! Поэтому, чтобы ускорить обработку простых, часто используемых типов, CLR предлагает «облегченные» типы — значимые. Экземпляры этих типов обычно размещаются в стеке потока (хотя они могут быть встроены и в объект ссылочного типа). В представляющей экземпляр переменной нет указателя на экземпляр; поля экземпляра размещаются в самой переменной. Поскольку переменная содержит поля экземпляра, то для работы с экземпляром не нужно выполнять разыменовывание (dereference) экземпляра. Благодаря тому, что экземпляры значимых типов не обрабатываются сборщиком мусора, уменьшается интенсивность работы с управляемой кучей и сокращается количество наборов (collections), требуемых приложению на протяжении его существования.

В документации по.NET Framework можно сразу увидеть, какие типы относят к ссылочным, а какие — к значимым. Если тип называют классом (class), речь идет о ссылочном типе. Так, классы System.Object, System.Exception, System.IO.FileStream и System.Random — это ссылочные типы. В свою очередь значимые типы в документации называют структурами (structure) и перечислениями (enumeration). Например, структуры System.Int32, System.Boolean, System.Decimal, System.TimeSpan и перечисления System.DayOfWeek, System.IO.FileAttributes и SystemDrawing.FontStyle являются значимыми типами.

При внимательном знакомстве с документацией можно заметить, что все структуры являются прямыми потомками абстрактного типа System.ValueType, который в свою очередь является производным от типа System.Object. По умолчанию все значимые типы должны быть производными от System.ValueType. Все перечисления являются производными от типа System.Enum, производного от System.ValueType. CLR и языки программирования по-разному интерпретируют перечисления.

При определении собственного значимого типа нельзя выбрать произвольный базовый тип, однако значимый тип может реализовать один или несколько выбранных вами интерфейсов. Кроме того, в CLR значимый тип является изолированным, то есть не может служить базовым типом для какого-либо другого ссылочного или значимого типа. Поэтому, например, нельзя в описании нового типа указать в качестве базовых Boolean, Char, Int32, Uint64, Single, Double, Decimal и т. д.

Внимание! Многим разработчикам (в частности тем, кто пишет неуправляемый код на C/C++) деление на ссылочные и значимые типы поначалу будет казаться странным. В неуправляемом коде C/C++ вы объявляете тип, и уже код решает, куда поместить экземпляр типа: в стек потока или в кучу приложения. В управляемом коде иначе: разработчик, описывающий тип, указывает, где разместятся экземпляры данного типа, а разработчик, использующий тип в своем коде, управлять этим не может.

Ниже показано различие между ссылочными и значимыми типами:

// Ссылочный тип (поскольку 'class')

class SomeRef { public Int32 x; }

// Значимый тип (поскольку 'struct')

struct SomeVal { public Int32 x; }

Static void ValueTypeDemo()

{

SomeRef r1 = new SomeRef(); // Размещается в куче

SomeVal v1 = new SomeVal(); // Размещается в стеке

r1.x = 5; // Разыменовывание указателя

v1.x = 5; // Изменение в стеке

Console.WriteLine(r1.x); // Отображается 5

Console.WriteLine(vi.x); // Также отображается 5

SomeRef г2 = г1; // Копируется только ссылка (указатель)

SomeVal v2 = v1; // Помещаем в стек и копируем члены

г1.х = 8; // Изменяются г1.х и г2.х

v1.x = 9; // Изменяется v1.x, но не v2.x

Console.WriteLine(г1.х); // Отображается 8

Console.WriteLine(r2.x); // Отображается 8

Console.WriteLine(v1.x); // Отображается 9

Console.WriteLine(v2.x); // Отображается 5

}

 

В этом примере тип SomeVal объявлен с ключевым словом struct, а не более популярным class. В С# типы, объявленные как struct, являются значимыми, а объявленные как class — ссылочными. Разницы в поведении ссылочных и значимых типов практически не видно. Поэтому так важно представлять, к какому семейству относится тот или иной тип — к ссылочному или значимому: ведь это может существенно повлиять на эффективность кода.

 
 

 

Рис. 4.1 Различие между размещением в памяти значимых и ссылочных типов

 

В предыдущем примере есть строка:

SomeVal v1 = new SomeVal(); // Размещается в стеке

 

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

Ту же строку можно записать иначе:

SomeVal v1; // Размещается в стеке

 

Здесь тоже создается IL-код, который помещает экземпляр SomeVal в стек потока и обнуляет все его поля. Единственное отличие в том, что экземпляр, созданный оператором new, C# «считает» инициализированным. Поясню на примере:

/* Две следующие строки компилируются, так как С# считает, что поля

в v1 инициализируются нулем*/

SomeVal v1 = new SomeVal();

Int32 a = v1.x;

// Следующие строки вызовут ошибку компиляции, поскольку С# не считает, что поля в v1 инициализируются нулем*/

SomeVal v1;

Int32 a = v1.x; /* error CS0170: Use of possibly unassigned field 'x'

(ошибка CS0170: Используется поле 'х', которому не присвоено значение)*/

 

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

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

2. Типу не нужен любой другой тип в качестве базового.

3. Тип не будет иметь производных от него типов.

 

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

§ размер экземпляров типа мал (примерно 16 байт или меньше);

§ размер экземпляров типа велик (более 16 байт) и экземпляры не передаются в качестве параметров метода или не являются возвращаемыми из метода значениями.

 

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

§ Объекты значимого типа существуют в двух формах: неупакованной (unboxed) и упакованной (boxed). Ссылочные типы бывают только в упакованной форме.

§ Значимые типы являются производными от System.ValueType. Этот тип имеет те же методы, что и System.Object. Однако System.ValueType переопределяет метод Equals, который возвращает true, если значения полей в обоих объектах совпадают. Кроме того, в System.ValueType переопределен метод GetHashCode, который создает значение хеш-кода с помощью алгоритма, учитывающего значения полей экземпляра объекта. Из-за проблем с производительностью в реализации по умолчанию, определяя собственные значимые типы значений, надо переопределить и написать свою реализацию методов Equals и GetHashCode. О методах Equals и GetHashCode чуть ниже.

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

§ Переменные ссылочного типа содержат адреса объектов в куче. Когда переменная ссылочного типа создается, ей по умолчанию присваивается null, то есть в этот момент она не указывает на действительный объект. Попытка задействовать переменную с таким значением приведет к генерации исключения NullReferenceException. В то же время в переменной значимого типа всегда содержится некое значение соответствующего типа, а при инициализации всем членам этого типа присваивается 0. Поскольку переменная значимого типа не является указателем, при обращении к значимому типу исключение NullReferenceException возникнуть не может. CLR поддерживает понятие особого вида значимого типа, допускающего присваивание null, (nullable types).

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

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

§ Так как неупакованные значимые типы не размещаются в куче, память, отведенная для них, освобождается сразу при возвращении управления методом, в котором описан экземпляр этого типа. Это значит, что экземпляр значимого типа не получает уведомления (через метод Finalize), когда его память освобождается.

 

Примечание. Действительно, было бы довольно странно включить в описание значимого типа метод Finalize, так как он вызывается только для упакованных экземпляров. Поэтому многие компиляторы (включая С#, C++/CLI и Visual Basic) не допускают в описании значимых типов методы Finalize. Правда, CLR разрешает включать в описание значимого типа метод Finalize, однако при сборке мусора для упакованных экземпляров значимого типа этот метод не вызывается.

«Как CLR управляет размещением полей для типа», «Упаковка и распаковка значимых типов», «Изменение полей в упакованных размерных типах посредством интерфейсов», «Равенство и тождество объектов», «Хеш-коды объектов» – см. [11].

 








ЧТО ТАКОЕ УВЕРЕННОЕ ПОВЕДЕНИЕ В МЕЖЛИЧНОСТНЫХ ОТНОШЕНИЯХ? Исторически существует три основных модели различий, существующих между...

Что вызывает тренды на фондовых и товарных рынках Объяснение теории грузового поезда Первые 17 лет моих рыночных исследований сводились к попыткам вычис­лить, когда этот...

Что делать, если нет взаимности? А теперь спустимся с небес на землю. Приземлились? Продолжаем разговор...

Живите по правилу: МАЛО ЛИ ЧТО НА СВЕТЕ СУЩЕСТВУЕТ? Я неслучайно подчеркиваю, что место в голове ограничено, а информации вокруг много, и что ваше право...





Не нашли то, что искали? Воспользуйтесь поиском гугл на сайте:


©2015- 2024 zdamsam.ru Размещенные материалы защищены законодательством РФ.