Типы и операции, принимающие значение null 


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Типы и операции, принимающие значение null



Взглянем на булевский тип. Он допускает присваивание значения либо true, либо false. Но как в таком случае поступить, если нужно определить неизвестное значение? Здесь как раз пригодится тип, допускающий значение null (nullable type). Если в своих программах вы используете типы, допускающие значение null, то всегда должны учиты­вать эффект, который дает применение значения null с различными операциями. Обычно при использовании унарной или бинарной операции с такими типами результатом будет null, если один или оба операнда будут равны null. Например:

int? а = null;

int? b = а + 4; // Ь = null

int? с = а * 5; // с = null

Однако при сравнении типов, принимающих значение null, если хотя бы один из операндов будет null, сравнение всегда даст false. Это значит, что нельзя ожидать, что условие даст в результате true, только потому, что противоположное условие дает false, как часто случается в программах, работающих с типами, не принимающими null. Например:

int? а = null;

int? b = -5;

if (a >= b)

System.Console.WriteLine("a >= b");

else

System.Console.WriteLine("a < b");

Возможность значения null означает, что произвольно комбинировать в выражении типы, принимающие значения null, и типы, не допускающие их, нельзя. Это обсуждается ниже в подразделе “Преобразования типов ”.

Операция поглощения null

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

Операция поглощения null работает следующим образом:

- если первый операнд не равен null, то все выражение принимает значение перво­го операнда;

- если первый операнд равен null, все выражение принимает значение второго опе­ранда.

Например:

int? а = null;

int b;

b = a?? 10; // b имеет значение 10

a = 3;

b = a?? 10; //b имеет значение 3

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

Приоритеты операций

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

Таблица 7.4. Приоритеты операций C#

Группа Операции
Первичные ().[] x++ x-- new typeof sizeof checked unchecked
Унарные + -! ~ ++x -x приведения
Умножения/деления / * %
Сложения/вычитания + -
Побитового сдвига << >>
Отношения <= >= <> is as
Сравнения ==!=
Битовое «И» &
Битовое исключающее «ИЛИ» ^
Битовое «ИЛИ» |
Логическое «И» &&
Логическое «ИЛИ» ||
Тернарная операция ?:
Присваивания = += -= *=.= %= &= |= ^= <<= >>= >>>=

 

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

Безопасность типов

В разделе 6.1 отмечалось, что промежуточный язык (IL) поддерживает строгую безопас­ность типов в коде. Строгая типизация обеспечивает работу многих служб, представляе­мых.NET, включая безопасность и межъязыковое взаимодействие. Как и следовало ожи­дать от языка, компилируемого в IL, C# также является строго типизированным. Помимо прочего, это означает, что типы данных не всегда являются гладко взаимозаменяемыми. В этом разделе речь пойдет о преобразованиях между примитивными типами.

Язык C# также поддерживает преобразование между разными ссылочными ти­пами и всегда позволяет определить, как создаваемые вами типы данных ведут себя при преобразовании в другие типы и обратно. Обе эти темы обсуждаются далее в настоящей главе. С другой стороны, обобщения (generics) позволяют избе­жать некоторых наиболее часто встречающихся ситуаций, когда может пона­добиться преобразование типов. Подробности ищите.в главах 5 и 10.

Преобразования типов

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

byte value1 = 10;

byte value2 = 23;

byte total;

total = valuel + value2;

Console.WriteLine(total);

Если вы попытаетесь скомпилировать эти строки, то получите сообщение об ошибке:

Cannot implicitly convert type ’int' to 'byte'

Неявное преобразование типа 'int' в ’byte' невозможно

Проблема здесь в том, что если складывать два байта, результат будет возвращен как int, а не другой byte. Это потому, что byte может содержать только восемь бит данных, поэтому сложение двух байт очень легко может дать результат, который не уместится в один тип byte. Если результат необходимо сохранить в переменной типа byte, его потребуется преобразовать в byte. Последующие разделы посвящены двум механизмам такого преобра­зования, поддерживаемым в C# - явному (explicit) и неявному (implicit) преобразованию.

Неявные преобразования

Преобразования типов обычно могут быть выполнены автоматически (неявно) толь­ко в том случае, когда при этом можно гарантировать, что значение не будет изменено никоим образом. Именно по этой причине не работает предыдущий пример; пытаясь пре­образовать int в byte, потенциально можно потерять 3 байта данных. Компилятор не по­зволит это сделать, если только явно не сообщить ему о том, что именно это и требуется. Однако если поместить результат в переменную long вместо byte, то проблем не будет:

byte valuel = 10;

byte value2 = 23;

long total; // это скомпилируется нормально

total = valuel + value2;

 

Console.WriteLine(total);

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

В табл. 7.5 показаны неявные преобразования типов, поддерживаемые С#.

Таблица 7.5. Неявные пребразования типов, поддерживаемые в C#

Из В
sbyte short, int, long, float, double, decimal, BigInteger
byte short, ushort, int, uint, long, ulong, float, double, decimal, BigInteger
short int, long, float, double, decimal, BigInteger
ushort int, uint, long, ulong, double, decimal, BigInteger
int long, float, double, decimal, BigInteger
uint long, ulong, float, double, decimal, BigInteger
long, ulong double, decimal, BigInteger
float double, BigInteger
char ushort, int, uint, long, ulong, floar, double, decimal, BigInteger

 

Как и следовало ожидать, неявные преобразования допускаются только от меньших це­лых типов к большим, но не наоборот. Можно также преобразовывать данные между целы­ми и действительными значениями, однако здесь правила слегка отличаются. Допускается преобразовывать данные между типами одинакового размера, например, из int/uint в float и long/ulong - в double, однако можно также выполнять преобразование long/ulong в float. При этом может быть потеряно 4 байта, но это означает только, что полу­ченное значение float будет менее точным, чем double; компилятор не трактует это как ошибку, потому что модуль значения не изменяется. Можно также присваивать значение переменной типа без знака переменной типа со знаком, если значение первой окажется в пределах допустимых значений второй.

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

- Допускающие null типы неявно преобразуются в другие типы, допускающие null, подчиняясь правилам, описанным в табл. 7.5; т.е. int? неявно преобразуется в long?, float?, double? и decimal?.

- Не допускающие null типы неявно преобразуются в типы, допускающие null, со­гласно тем же правилам. То есть int неявно преобразуется в long, float, double и decimal.

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

Явные преобразования

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

- int в short - возможна потеря данных;

- int в uint - возможна потеря данных;

- uint в int - возможна потеря данных;

- float в int - теряется все, что находится после десятичной запятой;

- любой числовой тип в char - возможна потеря данных;

- decimal в любой числовой тип - потому что тип decimal внутренне устроен иначе, чем целые числа и числа с плавающей точкой;

- int? в int - тип, допускающий null, может иметь значение null.

Однако такие преобразования можно выполнить принудительно, используя приведения (cast). Когда вы приводите один тип к другому, то сознательно заставляете компилятор вы­полнить преобразование. Приведение типов выглядит следующим образом:

long val = 30000;

int i = (int)val; // Корректное приведение.

// Максимальное значение int равно 2147483647.

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

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

long val = 3000000000;

int i = (int)val; // Неверное приведение.

// Максимальное значение int равно 2147483647.

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

-1294967296

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

long val = 3000000000;

int i = checked ((int)val);

Памятуя о том, что явные приведения типов потенциально опасны, вы должны поза­ботиться о включении в свое приложение кода, обрабатывающего возможные сбои таких приведений. Раздел 6.15 посвящена описанию обработки структурированных исключений с применением операторов try и catch. Используя приведения, можно преобразовывать большинство примитивных типов друг в друга; например, в следующем коде 0.5 добавля­ется к price, а результат приводится к int:

double price = 25.30;

int approximatePrice = (int)(price + 0.5);

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

В следующем примере показано, что произойдет при преобразовании беззнакового це­лого в char:

ushort с = 43;

char symbol = (char) с;

Console.WriteLine(symbol);

На экран будет выведен символ с ASCII-кодом 43 - т.е. знак +. Таким образом, вы мо­жете попробовать выполнить любое, какое захотите, преобразование, между числовыми типами (включая char), и оно будет работать (например, decimal в char или наоборот). Преобразование между типами значений не ограничено отдельными переменными, как мы видели до сих пор. Можно также преобразовывать элемент массива типа double в переменную-член структуры типа int:

struct ItemDetails

{

public string Description;

public int ApproxPrice;

}

// ...

double [] Prices = { 25.30, 26.20, 27.40, 30.00 };

ItemDetails id;

id.Description = "Что-то";

id.ApproxPrice = (int) (Prices [0] +0.5);

Чтобы преобразовать тип, допускающий null, к типу, null не допускающему, или же в другой тип, допускающий null, когда при этом может происходить потеря данных, следует применять явное приведение. Важно отметить, что это истинно даже при преобразовании данных между элементами с одинаковым типом, лежащим в основе, например, из int? в int или из float? в float. Это потому, что допускающий null тип может Содержать зна­чение null, которое невозможно представить типом, такого значения не допускающим. Там, где возможно явное приведение между двумя эквивалентными, не допускающими null типами, возможно также явное приведение между двумя аналогичными типами, до­пускающими null. Однако если выполняется приведение допускающего null типа в тип, не допускающий null, и при этом первая переменная имеет значение null, то генериру­ется исключение InvalidOperationException, например:

int? а = null;

int b = (int)а; // Сгенерирует исключение

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

Для выполнения преобразований между числами и строками в библиотеке классов.NET предусмотрены специальные методы. Класс Object реализует возвращающий строку метод ToString(), который переопределен во всех базовых типах.NET:

int i = 10;

string s = i.ToString();

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

string s = "100";

int i = int.Parse (s);

Console.WriteLine (i + 50); // Добавляем 50, чтобы доказать,

// что это действительно int.

Следует отметить, что Parse() регистрирует ошибку, генерируя исключение, если ока­зывается не в состоянии преобразовать строку (например, если вы попытаетесь преобра­зовать строку Hello в целое число). Исключения описаны в разделе 6.15.

Упаковка и распаковка

В разделе 6.2 вы узнали, что все типы - простые предопределенные вроде int и char, а также сложные наподобие классов и структур - наследуются от типа object. Это значит, что даже с литералами можно обращаться как с объектами:

string s = 10.ToString();

Однако вы также видели, что типы данных C# подразделяются на типы значений, кото­рые размещаются в стеке, и ссылочные типы, размещаемые в куче. Как объяснить возмож­ность вызова методов для int, если int - не более чем четырехбайтовое значение в стеке?

Способ, который использует C# для достижения такого эффекта, называется упаковкой (boxing); Упаковка и ее противоположность - распаковка (unboxing) - позволяют преоб­разовывать типы значений в ссылочные типы и затем обратно в типы значений. Эта тема была включена в раздел, посвященный приведению, потому, что, по сути, при этом выпол­няется приведение значения к типу object. Упаковка - термин, применяемый для описа­ния трансформации типа значения в ссылочный тип. В основном, при этом исполняющая среда создает временный “ящик” типа ссылки на объект в куче.

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

int myIntNumber = 20;

object myObject = myIntNumber;

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

int myIntNumber = 20;

object myObject = mylntNumber; // Упаковать int

int mySecondNumber = (int)myObject; // Распаковать обратно в int

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

Одно важное предостережение: при распаковке следует соблюдать осторожность, что­бы принимающая переменная имела в себе достаточно места для сохранения всех байтов распаковываемого значения. Например, int в C# имеет длину только 32 бита, а потому распаковка значения long (длиной 64 бита) в int, как показано ниже, приведет к генера­ции исключения InvalidCastException:

long myLongNumber = 333333423;

object myObject = (object)myLongNumber;

int mylntNumber = (int)myObject;

Проверка равенства объектов

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

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



Поделиться:


Последнее изменение этой страницы: 2016-12-30; просмотров: 321; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.144.124.232 (0.045 с.)