В этой главе рассматриваются три основополагающих элемента С#: типы данных, литералы и переменные. В целом, типы данных, доступные в языке программи рования, определяют те виды задач, для решения которых можно применять данный язык. Как и следовало ожидать, в C# предоставляется богатый набор встроенных типов дан ных, что делает этот язык пригодным для самого широкого применения. Любой из этих типов данных может служить для создания переменных и констант, которые в языке C# называются литералами.
Типы данных имеют особенное значение в С#, посколь ку это строго типизированный язык. Это означает, что все операции подвергаются строгому контролю со стороны компилятора на соответствие типов, причем недопустимые операции не компилируются. Следовательно, строгий кон троль типов позволяет исключить ошибки и повысить на дежность программ. Для обеспечения контроля типов все переменные, выражения и значения должны принадлежать к определенному типу. Такого понятия, как "бестиповая" переменная, в данном языке программирования вообще не существует. Более того, тип значения определяет те опера ции, которые разрешается выполнять над ним. Операция, разрешенная для одного типа данных, может оказаться не допустимой для другого.
ПРИМЕЧАНИЕ В версии C# 4.0 внедрен новый тип данных, называемый dynamic и приводящий к отсрочке контроля типов до времени выполнения, вместо того чтобы производить подобный контроль во время компиляции. Поэтому тип dynamic является исключением из обычного правила контроля типов во время компиляции. Подробнее о типе dynamic речь пойдет в главе 17.
В C# имеются две общие категории встроенных типов данных: типы значений и ссы лочные типы. Они отличаются по содержимому переменной. Если переменная от носится к типу значения, то она содержит само значение, например 3,1416 или 212. А если переменная относится к ссылочному типу, то она содержит ссылку на значение. Наиболее распространенным примером использования ссылочного типа является класс, но о классах и ссылочных типах речь пойдет далее в этой книге. А здесь рассма триваются типы значений.
В основу языка C# положены 13 типов значений, перечисленных в табл. 3.1. Все они называются простыми типами, поскольку состоят из единственного значения. (Иными словами, они не состоят из двух или более значений.) Они составляют основу системы типов С#, предоставляя простейшие, низкоуровневые элементы данных, которыми можно оперировать в программе. Простые типы данных иногда еще называют при митивными.
Таблица. 3.1. Типы значений в C#
Тип | Значение |
---|---|
bool | Логический, предоставляет два значения: “истина” или “ложь” |
byte | 8-разрядный целочисленный без знака |
char | Символьный |
decimal | Десятичный (для финансовых расчетов) |
double | С плавающей точкой двойной точности |
float | С плавающей точкой одинарной точности |
int | Целочисленный |
long | Длинный целочисленный |
sbyte | 8-разрядный целочисленный со знаком |
short | Короткий целочисленный |
uint | Целочисленный без знака |
ulong | Длинный целочисленный без знака |
ushort | Короткий целочисленный без знака |
В C# строго определены пределы и характер действия каждого типа значения. Исходя из требований к переносимости программ, C# не допускает в этом отношении никаких компромиссов. Например, тип int должен быть одинаковым во всех средах выполнения. Но в этом случае отпадает необходимость переписывать код для кон кретной платформы. И хотя строгое определение размерности типов значений может стать причиной незначительного падения производительности в некоторых средах, эта мера необходима для достижения переносимости программ.
ПРИМЕЧАНИЕ Помимо простых типов, в C# определены еще три категории типов значений: перечисления, структуры и обнуляемые типы. Все они рассматриваются далее в этой книге.
В C# определены девять целочисленных типов: char, byte, sbyte, short, ushort, int, uint, long и ulong. Но тип char применяется, главным образом, для пред ставления символов и поэтому рассматривается далее в этой главе. Остальные восемь целочисленных типов предназначены для числовых расчетов. Ниже представлены их диапазон представления чисел и разрядность в битах.
Тип | Разрядность в битах | Диапазон представления чисел |
---|---|---|
byte | 8 | 0-255 |
sbyte | 8 | -128-127 |
short | 16 | -32 768-32 767 |
ushort | 16 | 0-65 535 |
int | 32 | -2 147 483 648-2 147 483 647 |
uint | 32 | 0-4 294 967 295 |
long | 64 | -9 223 372 036 854 775 808-9 223 372 036 854 775 807 |
ulong | 64 | 0-18 446 744 073 709 551 615 |
Как следует из приведенной выше таблицы, в C# определены оба варианта различ ных целочисленных типов: со знаком и без знака. Целочисленные типы со знаком от личаются от аналогичных типов без знака способом интерпретации старшего разряда целого числа. Так, если в программе указано целочисленное значение со знаком, то компилятор C# сгенерирует код, в котором старший разряд целого числа использу ется в качестве флага знака. Число считается положительным, если флаг знака равен 0, и отрицательным, если он равен 1. Отрицательные числа практически всегда представ ляются методом дополнения до двух, в соответствии с которым все двоичные разряды отрицательного числа сначала инвертируются, а затем к этому числу добавляется 1.
Целочисленные типы со знаком имеют большое значение для очень многих алго ритмов, но по абсолютной величине они наполовину меньше своих аналогов без знака. Вот как, например, выглядит число 32 767 типа short в двоичном представлении.
0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
Если установить старший разряд этого числа равным 1, чтобы получить значение со знаком, то оно будет интерпретировано как -1, принимая во внимание формат до полнения до двух. Но если объявить его как значение типа ushort, то после установки в 1 старшего разряда оно станет равным 65 535.
Вероятно, самым распространенным в программировании целочисленным типом является тип int. Переменные типа int нередко используются для управления цикла ми, индексирования массивов и математических расчетов общего назначения. Когда же требуется целочисленное значение с большим диапазоном представления чисел, чем у типа int, то для этой цели имеется целый ряд других целочисленных типов. Так, если значение нужно сохранить без знака, то для него можно выбрать тип uint, для больших значений со знаком — тип long, а для больших значений без знака — тип ulong. В качестве примера ниже приведена программа, вычисляющая расстояние от Земли до Солнца в дюймах. Для хранения столь большого значения в ней использует ся переменная типа long.
// Вычислить расстояние от Земли до Солнца в дюймах.
using System;
class Inches {
static void Main() {
long inches;
long miles;
miles = 93000000; // 93 000 000 миль до Солнца
// 5 280 футов в миле, 12 дюймов в футе,
inches = miles * 5280 * 12;
Console.WriteLine("Расстояние до Солнца: " +
inches + " дюймов.");
}
}
Вот как выглядит результат выполнения этой программы.
Расстояние до Солнца: 5892480000000 дюймов.
Очевидно, что этот результат нельзя было бы сохранить в переменной типа int или uint.
Самыми мелкими целочисленными типами являются byte и sbyte. Тип byte представляет целые значения без знака в пределах от 0 до 255. Переменные типа byte особенно удобны для обработки исходных двоичных данных, например байтового по тока, поступающего от некоторого устройства. А для представления мелких целых зна чений со знаком служит тип sbyte. Ниже приведен пример программы, в которой переменная типа byte используется для управления циклом, где суммируются числа от 1 до 100.
// Использовать тип byte.
using System;
class Use_byte {
static void Main() {
byte x;
int sum;
sum = 0;
for(x = 1; х <= 100; х++)
sum = sum + х;
Console.WriteLine("Сумма чисел от 1 до 100 равна " + sum);
}
}
Результат выполнения этой программы выглядит следующим образом.
Сумма чисел от 1 до 100 равна 5050
В приведенном выше примере программы цикл выполняется только от 1 до 100, что не превышает диапазон представления чисел для типа byte, и поэтому для управ ления этим циклом не требуется переменная более крупного типа.
Если же требуется целое значение, большее, чем значение типа byte или sbyte, но меньшее, чем значение типа int или uint, то для него можно выбрать тип short или ushort.
Типы с плавающей точкой позволяют представлять числа с дробной частью. В С# имеются две разновидности типов данных с плавающей точкой: float и double. Они представляют числовые значения с одинарной и двойной точностью соответственно. Так, разрядность типа float составляет 32 бита, что приближенно соответствует диапа зону представления чисел от 5Е-45 до 3,4Е+38. А разрядность типа double составляет 64 бита, что приближенно соответствует диапазону представления чисел от 5Е-324 до 1,7Е+308.
В программировании на С# чаще применяется тип double, в частности, потому, что во многих математических функциях из библиотеки классов С#, которая одновре менно является библиотекой классов для среды .NET Framework, используются чис ловые значения типа double. Например, метод Sqrt(), определенный в библиотеке классов System.Math, возвращает значение типа double, которое представляет собой квадратный корень из аргумента типа double, передаваемого данному методу. В при веденном ниже примере программы метод Sqrt() используется для вычисления ра диуса окружности по площади круга.
// Определить радиус окружности по площади круга.
using System;
class FindRadius {
static void Main() {
Double r;
Double area;
area = 10.0;
r = Math.Sqrt(area / 3.1416);
Console.WriteLine("Радиус равен " + r);
}
}
Результат выполнения этой программы выглядит следующим образом.
Радиус равен 1.78412203012729
В приведенном выше примере программы следует обратить внимание на вызов ме тода Sqrt(). Как упоминалось выше, метод Sqrt() относится к классу Math, поэтому в его Вызове имя Math предшествует имени самого метода. Аналогичным образом имя класса Console предшествует имени метода WriteLine() в его вызове. При вызове некоторых, хотя и не всех, стандартных методов обычно указывается имя их класса, как показано в следующем примере.
В следующем примере программы демонстрируется применение нескольких три гонометрических функций, которые относятся к классу Math и входят в стандартную библиотеку классов С#. Они также оперируют данными типа double. В этом примере на экран выводятся значения синуса, косинуса и тангенса угла, измеряемого в пределах от 0,1 до 1,0 радиана.
// Продемонстрировать применение тригонометрических функций.
using System;
class Trigonometry {
static void Main() {
Double theta; // угол в радианах
for(theta = 0.1; theta <= 1.0;
theta = theta +0.1) {
Console.WriteLine("Синус угла " + theta +
" равен " + Math.Sin(theta));
Console.WriteLine("Косинус угла " + theta +
" равен " + Math.Cos(theta));
Console.WriteLine("Тангенс угла " + theta +
" равен " + Math.Tan(theta));
Console.WriteLine();
}
}
}
Ниже приведена лишь часть результата выполнения данной программы.
Синус угла 0.1 равен 0.0998334166468282
Косинус угла 0.1 равен 0.995004165278026
Тангенс угла 0.1 равен 0.100334672085451
Синус угла 0.2 равен 0.198669330795061
Косинус угла 0.2 равен 0.980066577841242
Тангенс угла 0.2 равен 0.202710035508673
Синус угла 0.3 равен 0.29552020666134
Косинус угла 0.3 равен 0.955336489125606
Тангенс угла 0.3 равен 0.309336249609623
Для вычисления синуса, косинуса и тангенса угла в приведенном выше примере были использованы стандартные методы Math.Sin(), Math.Cos() и Math.Tan(). Как и метод Math.Sqrt(), эти тригонометрические методы вызываются с аргументом типа double и возвращают результат того же типа. Вычисляемые углы должны быть указаны в радианах.
Вероятно, самым интересным среди всех числовых типов данных в C# является тип decimal, который предназначен для применения в финансовых расчетах. Этот тип имеет разрядность 128 бит для представления числовых значений в пределах от 1Е-28 до 7,9Е+28. Вам, вероятно, известно, что для обычных арифметических вычислений с плавающей точкой характерны ошибки округления десятичных значений. Эти ошиб ки исключаются при использовании типа decimal, который позволяет представить числа с точностью до 28 (а иногда и 29) десятичных разрядов. Благодаря тому что этот тип данных способен представлять десятичные значения без ошибок округления, он особенно удобен для расчетов, связанных с финансами.
Ниже приведен пример программы, в которой тип decimal используется в кон кретном финансовом расчете. В этой программе цена со скидкой рассчитывается на основании исходной цены и скидки в процентах.
// Использовать тип decimal для расчета скидки.
using System;
class UseDecimal {
static void Main() {
decimal price;
decimal discount;
decimal discounted_price;
// Рассчитать цену со скидкой.
price = 19.95m;
discount = 0.15m; // норма скидки составляет 15%
discounted_price = price - ( price * discount);
Console.WriteLine("Цена со скидкой: $" + discounted_price);
}
}
Результат выполнения этой программы выглядит следующим образом.
Цена со скидкой: $16.9575
Обратите внимание на то, что значения констант типа decimal в приведенном выше примере программы указываются с суффиксом m. Дело в том, что без суффикса m эти значения интерпретировались бы как стандартные константы с плавающей точ кой, которые несовместимы с типом данных decimal. Тем не менее переменной типа decimal можно присвоить целое значение без суффикса m, например 10. (Подробнее о числовых константах речь пойдет далее в этой главе.)
Рассмотрим еще один пример применения типа decimal. В этом примере рассчи тывается будущая стоимость капиталовложений с фиксированной нормой прибыли в течение ряда лет.
/*
Применить тип decimal для расчета будущей стоимости
капиталовложений.
*/
using System;
class FutVal {
static void Main() {
decimal amount;
decimal rate_of_return;
int years, i;
amount = 1000.0M;
rate_of_return = 0.07M;
years = 10;
Console.WriteLine("Первоначальные капиталовложения: $" + amount);
Console.WriteLine("Норма прибыли: " + rate_of_return);
Console.WriteLine("В течение " + years + " лет");
for(i = 0; i < years; i++)
amount = amount + (amount * rate_of_return);
Console.WriteLine("Будущая стоимость равна $" + amount);
}
}
Вот как выглядит результат выполнения этой программы.
Первоначальные капиталовложения: $1000
Норма прибыли: 0.07
В течение 10 лет
Будущая стоимость равна $1967.151357289565322490000
Обратите внимание на то, что результат выполнения приведенной выше програм мы представлен с точностью ДО целого ряда десятичных разрядов, т.е. с явным избыт ком по сравнению с тем, что обычно требуется! Далее в этой главе будет показано, как подобный результат приводится к более "привлекательному" виду.
В C# символы представлены не 8-разрядным кодом, как во многих других языках программирования, например C++, а 16-разрядным кодом, который называется унико дом (Unicode). В уникоде набор символов представлен настолько широко, что он охва тывает символы практически из всех естественных языков на свете. Если для многих естественных языков, в том числе английского, французского и немецкого, характерны относительно небольшие алфавиты, то в ряде других языков, например китайском, употребляются довольно обширные наборы символов, которые нельзя представить 8-разрядным кодом. Для преодоления этого ограничения в C# определен тип char, представляющий 16-разрядные значения без знака в пределах от 0 до 65 535. При этом стандартный набор символов в 8-разрядном коде ASCII является подмножеством уни кода в пределах от 0 до 127. Следовательно, символы в коде ASCII по-прежнему оста ются действительными в С#.
Для того чтобы присвоить значение символьной переменной, достаточно заклю чить это значение (т.е. символ) в одинарные кавычки. Так, в приведенном ниже фраг менте кода переменной ch присваивается символ X.
char ch;
ch = 'X';
Значение типа char можно вывести на экран с помощью метода WriteLine(). Например, в следующей строке кода на экран выводится значение переменной ch.
Console.WriteLine("Значение ch равно: " + ch);
Несмотря на то что тип char определен в C# как целочисленный, его не следует путать со всеми остальными целочисленными типами. Дело в том, что в C# отсутству ет автоматическое преобразование символьных значений в целочисленные и обратно.
Например, следующий фрагмент кода содержит ошибку.
char ch;
ch = 88; // ошибка, не выйдет
Ошибочность приведенного выше фрагмента кода объясняется тем, что 88 — это целое значение, которое не преобразуется автоматически в символьное. При попытке скомпилировать данный фрагмент кода будет выдано соответствующее сообщение об ошибке. Для того чтобы операция присваивания целого значения символьной пере менной оказалась допустимой, необходимо осуществить приведение типа, о котором речь пойдет далее в этой главе.
Тип bool представляет два логических значения: "истина" и "ложь". Эти логиче ские значения обозначаются в C# зарезервированными словами true и false соот ветственно. Следовательно, переменная или выражение типа bool будет принимать одно из этих логических значений. Кроме того, в C# не определено взаимное преобра зование логических и целых значений. Например, 1 не преобразуется в значение true, а 0 — в значение false.
В приведенном ниже примере программы демонстрируется применение типа bool.
// Продемонстрировать применение типа bool.
using System;
class BoolDemo {
static void Main() {
bool b;
b = false;
Console.WriteLine("b равно " + b);
b = true;
Console.WriteLine("b равно " + b);
// Логическое значение может управлять оператором if.
if(b) Console.WriteLine("Выполняется.");
b = false;
if (b) Console.WriteLine("He выполняется.");
// Результатом выполнения оператора отношения
// является логическое значение.
Console.WriteLine("10 > 9 равно " + (10 > 9));
}
}
Эта программа дает следующий результат.
b равно False
b равно True
Выполняется.
10 > 9 равно True
В приведенной выше программе обнаруживаются три интересные особенно сти. Во-первых, при выводе логического значения типа bool с помощью метода WriteLine() на экране появляется значение "True" или "False". Во-вторых, самого значения переменной типа bool достаточно для управления оператором if. Для это го не нужно, например, записывать оператор if следующим образом.
if(b == true) ...
И в-третьих, результатом выполнения оператора отношения является логическое значение. Именно поэтому в результате вычисления выражения 10 > 9 на экран вы водится значение "True." Кроме того, выражение 10 > 9 следует заключить в скобки, поскольку оператор + имеет более высокий приоритет, чем оператор >.
До сих пор при выводе с помощью метода WriteLine() данные отображались в формате, используемом по умолчанию. Но в среде .NET Framework определен до статочно развитый механизм форматирования, позволяющий во всех деталях управ лять выводом данных. Форматированный ввод-вывод подробнее рассматривается далее в этой книге, а до тех пор полезно ознакомиться с некоторыми возможностями форма тирования. Они позволяют указать, в каком именно виде следует выводить значения с помощью метода WriteLine(). Благодаря этому выводимый результат выглядит более привлекательно. Следует, однако, иметь в виду, что механизм форматирования поддерживает намного больше возможностей, а не только те, которые рассматривают ся в этом разделе.
При выводе списков данных в предыдущих примерах программ каждый элемент списка приходилось отделять знаком +, как в следующей строке.
Console.WriteLine("Вы заказали " + 2 +
" предмета по цене $" + 3 + " каждый.");
Конечно, такой способ вывода числовой информации удобен, но он не позволяет управлять внешним видом выводимой информации. Например, при выводе значения с плавающей точкой нельзя определить количество отображаемых десятичных раз рядов. Рассмотрим оператор
Console.WriteLine("Деление 10/3 дает: " + 10.0/3.0);
который выводит следующий результат.
Деление 10/3 дает: 3.33333333333333
В одних случаях такого вывода может оказаться достаточно, а в других — он про сто недопустим. Например, в финансовых расчетах после десятичной точки принято указывать лишь два десятичных разряда.
Для управления форматированием числовых данных служит другая форма метода WriteLine(), позволяющая встраивать информацию форматирования, как показано ниже.
WriteLine("форматирующая строка", arg0, arg1, ... , argN);
В этой форме аргументы метода WriteLine() разделяются запятой, а не знаком +. А форматирующая строка состоит из двух элементов: обычных печатаемых символов, предназначенных для вывода в исходном виде, а также спецификаторов формата. По следние указываются в следующей общей форме:
{argnum, width: fmt}
где argnum — номер выводимого аргумента, начиная с нуля; width — минимальная ширина поля; fmt — формат. Параметры width и fmt являются необязательными. Если во время выполнения в форматирующей строке встречается спецификатор формата, то вместо него подставляется и отображается соответствующий аргумент, обозначаемый параметром argnum. Таким образом, местоположение спецификатора формата в форматирующей строке определяет место отображения соответствующих данных. Параметры width и fmt указывать необязательно. Это означает, что в своей простейшей форме спецификатор формата обозначает конкретный отображаемый аргумент. Например, спецификатор {0} обозначает аргумент arg0, спецификатор {1} — аргумент arg1 и т.д.
Начнем с самого простого примера. При выполнение оператора
Console.WriteLine("В феврале {0} или {1} дней.", 28, 29);
получается следующий результат.
В феврале 28 или 29 дней
Как видите, значение 28 подставляется вместо спецификатора {0}, а значение 29 — вместо спецификатора {1}. Следовательно, спецификаторы формата обозначают ме сто в строке, где отображаются соответствующие аргументы (в данном случае — зна чения 28 и 29). Кроме того, обратите внимание на то, что дополнительные значения разделяются запятой, а не знаком +.
Ниже приведен видоизмененный вариант предыдущего оператора, в котором ука зывается ширина полей.
Console.WriteLine("В феврале {0,10} или {1,5} дней.", 28, 29);
Выполнение этого оператора дает следующий результат. В феврале 28 или 29 дней.
Как видите, неиспользуемые части полей заполнены пробелами. Напомним, что минимальная ширина поля определяется параметром width. Если требуется, она мо жет быть превышена при выводе результата.
Разумеется, аргументы, связанные с командой форматирования, не обязательно должны быть константами. Ниже приведен пример программы, которая выводит та блицу результатов возведения чисел в квадрат и куб. В ней команды форматирования используются для вывода соответствующих значений.
// Применить команды форматирования.
using System;
class DisplayOptions {
static void Main() {
int i;
Console.WriteLine("Число\tКвадрат\tКуб");
for(i = 1; i < 10; i++)
Console.WriteLine("{0}\t{1}\t{2}", i, i*i, i*i*i);
}
}
Результат выполнения этой программы выглядит следующим образом.
Число Квадрат Куб
1 1 1
2 4 8
3 9 27
4 16 64
5 25 125
6 36 216
7 49 343
8 64 512
9 81 729
В приведенных выше примерах сами выводимые значения не форматировались. Но ведь основное назначение спецификаторов формата — управлять внешним видом выводимых данных. Чаще всего форматированию подлежат следующие типы данных: с плавающей точкой и десятичный. Самый простой способ указать формат данных — описать шаблон, который будет использоваться в методе WriteLine(). Для этого ука зывается образец требуемого формата с помощью символов #, обозначающих разряды чисел. Кроме того, можно указать десятичную точку и запятые, разделяющие цифры. Ниже приведен пример более подходящего вывода результата деления 10 на 3.
Console.WriteLine("Деление 10/3 дает: (0:#.##)", 10.0/3.0);
Выполнение этого оператора приводит к следующему результату.
Деление 10/3 дает: 3.33
В данном примере шаблон #.## указывает методу WriteLine() отобразить два десятичных разряда в дробной части числа. Следует, однако, иметь в виду, что метод WriteLine() может отобразить столько цифр слева от десятичной точки, сколько по требуется для правильной интерпретации выводимого значения.
Рассмотрим еще один пример. Оператор
Console.WriteLine("{0:###,###.##}", 123456.56);
дает следующий результат.
123,456.56
Для вывода денежных сумм, например, рекомендуется использовать спецификатор формата С.
decimal balance;
balance = 12323.09m;
Console.WriteLine("Текущий баланс равен {0:С}" , balance);
Результат выполнения этого фрагмента кода выводится в формате денежных сумм, указываемых в долларах США.
Текущий баланс равен $12,323.09
Форматом С можно также воспользоваться, чтобы представить в более подходя щем виде результат выполнения рассматривавшейся ранее программы расчета цены со скидкой.
// Использовать спецификатор формата С для вывода
// результата в местной валюте.
using System;
class UseDecimal {
static void Main() {
decimal price;
decimal discount;
decimal discounted_price;
// рассчитать цену со скидкой,
price = 19.95m;
discount = 0.15m; // норма скидки составляет 15%
discounted_price = price - ( price * discount);
Console.WriteLine("Цена со скидкой: {0:С}", discounted_price);
}
}
Вот как теперь выглядит результат выполнения этой программы.
Цена со скидкой: 16,96 грн.
В C# литералами называются постоянные значения, представленные в удобной для восприятия форме. Например, число 100 является литералом. Сами литералы и их назначение настолько понятны, что они применялись во всех предыдущих примерах программ без всяких пояснений. Но теперь настало время дать им формальное объяс нение.
В С# литералы могут быть любого простого типа. Представление каждого литерала зависит от конкретного типа. Как пояснялось ранее, символьные литералы заключают ся в одинарные кавычки. Например, 'а' и '%' являются символьными литералами. Целочисленные литералы указываются в виде чисел без дробной части. Например, 10 и -100 — это целочисленные литералы. Для обозначения литералов с плавающей точкой требуется указывать десятичную точку и дробную часть числа. Например, 11.123 — это литерал с плавающей точкой. Для вещественных чисел с плавающей точкой в C# допускается также использовать экспоненциальное представление. У литералов должен быть также конкретный тип, поскольку C# является строго ти пизированным языком. В этой связи возникает естественный вопрос: к какому типу следует отнести числовой литерал, например 2,123987 или 0.23? К счастью, для от вета на этот вопрос в C# установлен ряд простых для соблюдения правил. Во-первых, у целочисленных литералов должен быть самый мелкий целочислен ный тип, которым они могут быть представлены, начиная с типа int. Таким образом, у целочисленных литералов может быть один из следующих типов: int, uint, long или ulong в зависимости от значения литерала. И во-вторых, литералы с плавающей точкой относятся к типу double.
Если вас не устраивает используемый по умолчанию тип литерала, вы можете явно указать другой его тип с помощью суффикса. Так, для указания типа long к литералу присоединяется суффикс l или L. Например, 12 — это литерал типа int, a 12L — ли терал типа long. Для указания целочисленного типа без знака к литералу присоединя ется суффикс u или U. Следовательно, 100 — это литерал типа int, a 100U — литерал типа uint. А для указания длинного целочисленного типа без знака к литералу при соединяется суффикс ul или UL. Например, 984375UL — это литерал типа ulong. Кроме того, для указания типа float к литералу присоединяется суффикс F или f. Например, 10.19F — это литерал типа float. Можете даже указать тип double, при соединив к литералу суффикс d или D, хотя это излишне. Ведь, как упоминалось выше, по умолчанию литералы с плавающей точкой относятся к типу double.
И наконец, для указания типа decimal к литералу присоединяется суффикс m или М. Например, 9.95М — это десятичный литерал типа decimal.
Несмотря на то что целочисленные литералы образуют по умолчанию значения типа int, uint, long или ulong, их можно присваивать переменным типа byte, sbyte, short или ushort, при условии, что присваиваемое значение может быть представлено целевым типом.
Вам, вероятно, известно, что в программировании иногда оказывается проще поль зоваться системой счисления по основанию 16, чем по основанию 10. Система счисле ния по основанию 16 называется шестнадцатеричной. В ней используются числа от 0 до 9, а также буквы от А до F, которыми обозначаются десятичные числа 10, 11, 12, 13, 14 и 15. Например, десятичному числу 16 соответствует шестнадцатеричное число 10. Вследствие того что шестнадцатеричные числа применяются в программировании до вольно часто, в C# разрешается указывать целочисленные литералы в шестнадцате ричном формате. Шестнадцатеричные литералы должны начинаться с символов 0х, т.е. нуля и последующей латинской буквы "икс". Ниже приведены некоторые приме ры шестнадцатеричных литералов.
count = 0xFF; // 255 в десятичной системе
incr = 0x1а; // 26 в десятичной системе
Большинство печатаемых символов достаточно заключить в одинарные кавычки, но набор в текстовом редакторе некоторых символов, например возврата каретки, вы зывает особые трудности. Кроме того, ряд других символов, в том числе одинарные и двойные кавычки, имеют специальное назначение в С#, поэтому их нельзя исполь зовать непосредственно. По этим причинам в C# предусмотрены специальные управ ляющие последовательности символов, иногда еще называемые константами с обратной косой чертой (табл. 3.2). Такие последовательности применяются вместо тех символов, которых они представляют.
Таблица 3.2. Управляющие последовательности символов
Управляющая последовательность | Описание |
---|---|
\a | Звуковой сигнал (звонок) |
\b | Возврат на одну позицию |
\f | Перевод страницы (переход на новую страницу) |
\n | Новая строка (перевод строки) |
\r | Возврат каретки |
\t | Горизонтальная табуляция |
\v | Вертикальная табуляция |
\0 | Пустой символ |
' | Одинарная кавычка |
" | Двойная кавычка |
\ | Обратная косая черта |
Например, в следующей строке кода переменной ch присваивается символ табу ляции.
ch = '\t';
А в приведенном ниже примере кода переменной ch присваивается символ оди нарной кавычки.
ch = '\'';
В С# поддерживается еще один тип литералов — строковый. Строковый литерал представляет собой набор символов, заключенных в двойные кавычки. Например сле дующий фрагмент кода:
"это тест"
представляет собой текстовую строку. Образцы подобных строк не раз встречались в приведенных выше примерах программ.
Помимо обычных символов, строковый литерал может содержать одну или не сколько управляющих последовательностей символов, о которых речь шла выше. Рас смотрим для примера программу, в которой используются управляющие последова тельности \n и \t.
// Продемонстрировать применение управляющих
// последовательностей символов в строковых литералах.
using System;
class StrDemo {
static void Main() {
Console.WriteLine("Первая строка\nВторая строка\nТретья строка");
Console.WriteLine("Один\tДва\tТри");
Console.WriteLine("Четыре\tПять\tШесть");
// Вставить кавычки.
Console.WriteLine("\"3ачем?\", спросил он.");
}
}
Результат выполнения этой программы приведен ниже.
Первая строка
Вторая строка
Третья строка
Один Два Три
Четыре Пять Шесть
"Зачем?", спросил он.
В приведенном выше примере программы обратите внимание на то, что для пере хода на новую строку используется управляющая последовательность \n. Для выво да нескольких строк совсем не обязательно вызывать метод WriteLine() несколько раз — достаточно вставить управляющую последовательность \n в тех местах удли ненной текстовой строки (или строкового литерала), где должен происходить переход на новую строку. Обратите также внимание на то, как в текстовой строке формируется знак кавычек.
Помимо описанной выше формы строкового литерала, можно также указать бук вальный строковый литерал. Такой литерал начинается с символа @, после которого следует строка в кавычках. Содержимое строки в кавычках воспринимается без изме нений и может быть расширено до двух и более строк. Это означает, что в буквальный строковый литерал можно включить символы новой строки, табуляции и прочие, не прибегая к управляющим последовательностям. Единственное исключение составля ют двойные кавычки ("), для указания которых необходимо использовать две двой ные кавычки подряд (""). В приведенном ниже примере программы демонстрируется применение буквальных строковых литералов.
// Продемонстрировать применение буквальных строковых литералов.
using System;
class Verbatim {
static void Main() {
Console.WriteLine(@"Это буквальный
строковый литерал,
занимающий несколько строк.
");
Console.WriteLine(@"А это вывод с табуляцией:
1 2 3 4
5 6 7 8
");
Console.WriteLine(@"Отзыв программиста: ""Мне нравится С#.""");
}
}
Результат выполнения этой программы приведен ниже.
Это буквальный
строковый литерал,
занимающий несколько строк.
А это вывод с табуляцией:
1 2 3 4
5 6 7 8
Отзыв программиста: "Мне нравится С#."
Следует особо подчеркнуть, что буквальные строковые литералы выводятся в том же виде, в каком они введены в исходном тексте программы.
Преимущество буквальных строковых литералов заключается в том, что они позво ляют указать в программе выводимый результат именно так, как он должен выглядеть на экране. Но если выводится несколько строк, то переход на новую строку может на рушить порядок набора исходного текста программы с отступами. Именно по этой причине в примерах программ, приведенных в этой книге, применение буквальных строковых литералов ограничено. Тем не менее они приносят немало замечательных выгод во многих случаях, когда требуется форматирование выводимых результатов.
И последнее замечание: не путайте строки с символами. Символьный литерал, на пример 'X', обозначает одиночную букву типа char. А строка, состоящая из одного символа, например "X", по-прежнему остается текстовой строкой.
Переменные объявляются с помощью оператора следующей формы:
тип имя_переменной;
где тип — это тип данных, хранящихся в переменной; а имя_переменной — это ее имя. Объявить можно переменную любого действительного типа, в том числе и опи санных выше типов значений. Важно подчеркнуть, что возможности переменной опре деляются ее типом. Например, переменную типа bool нельзя использовать для хра нения числовых значений с плавающей точкой. Кроме того, тип переменной нельзя изменять в течение срока ее существования. В частности, переменную типа int нельзя преобразовать в переменную типа char.
Все переменные в C# должны быть объявлены до их применения. Это нужно для того, чтобы уведомить компилятор о типе данных, хранящихся в переменной, прежде чем он попытается правильно скомпилировать любой оператор, в котором использу ется переменная. Это позволяет также осуществлять строгий контроль типов в С#. В С# определено несколько различных видов переменных. Так, в предыдущих при мерах программ использовались переменные, называемые локальными, поскольку они объявляются внутри метода.
Задать значение переменной можно, в частности, с помощью оператора присваи вания, как было не раз продемонстрировано ранее. Кроме того, задать начальное зна чение переменной можно при ее объявлении. Для этого после имени переменной ука зывается знак равенства (=) и присваиваемое значение. Ниже приведена общая форма инициализации переменной:
тип имя_переменной = значение;
где значение — это конкретное значение, задаваемое при создании переменной. Оно должно соответствовать указанному типу переменной. Ниже приведены некоторые примеры инициализации переменных.
int count = 10; // задать начальное значение 10 переменной count.
char ch = 'X'; // инициализировать переменную ch буквенным значением X.
float f = 1.2F // переменная f инициализируется числовым значением 1,2.
Если две или более переменные одного и того же типа объявляются списком, разде ляемым запятыми, то этим переменным можно задать, например, начальное значение.
int а, b =8, с = 19, d; // инициализировать переменные b и с
В данном примере инициализируются только переменные b и с.
В приведенных выше примерах в качестве инициализаторов переменных исполь зовались только константы, но в C# допускается также динамическая инициализация переменных с помощью любого выражения, действительного на момент объявления переменной. Ниже приведен пример краткой программы для вычисления гипотенузы прямоугольного треугольника по длине его противоположных сторон.
// Продемонстрировать динамическую инициализацию.
using System;
class DynInit {
static void Main() {
// Длина сторон прямоугольного треугольника,
double s1 = 4.0;
double s2 = 5.0;
// Инициализировать переменную hypot динамически,
double hypot = Math.Sqrt( (s1 * s1) + (s2 * s2) );
Console.Write("Гипотенуза треугольника со сторонами " +
s1 + " и " + s2 + " равна ");
Console.WriteLine("{0:#.###}.", hypot);
}
}
Результат выполнения этой программы выглядит следующим образом.
Гипотенуза треугольника со сторонами 4 и 5 равна 6.403
В данном примере объявляются три локальные переменные: s1, s2 и hypot. Две из них (s1 и s2) инициализируются константами, А третья (hypot) динамически ини циализируется вычисляемой длиной гипотенузы. Для такой инициализации исполь зуется выражение, указываемое в вызываемом методе Math.Sqrt(). Как пояснялось выше, для динамической инициализации пригодно любое выражение, действительное на момент объявления переменной. А поскольку вызов метода Math.Sqrt() (или лю бого другого библиотечного метода) является действительным на данный момент, то его можно использовать для инициализации переменной hypot. Следует особо под черкнуть, что в выражении для инициализации можно использовать любой элемент, действительный на момент самой инициализации переменной, в том числе вызовы методов, другие переменные или литералы.
Как пояснялось выше, все переменные в C# должны быть объявлены. Как прави ло, при объявлении переменной сначала указывается тип, например int или bool, а затем имя переменной. Но начиная с версии C# 3.0, компилятору предоставляется возможность самому определить тип локальной переменной, исходя из значения, ко торым она инициализируется. Такая переменная называется неявно типизированной. Неявно типизированная переменная объявляется с помощью ключевого слова var и должна быть непременно инициализирована. Для определения типа этой перемен ной компилятору служит тип ее инициализатора, т.е. значения, которым она инициа лизируется. Рассмотрим такой пример.
var е = 2.7183;
В данном примере переменная е инициализируется литералом с плавающей точкой, который по умолчанию имеет тип double, и поэтому она относится к типу double. Если бы переменная е была объявлена следующим образом:
var е = 2.7183F;
то она была бы отнесена к типу float.
В приведенном ниже примере программы демонстрируется применение неявно типизированных переменных. Он представляет собой вариант программы из предыду щего раздела, измененной таким образом, чтобы все переменные были типизированы неявно.
// Продемонстрировать применение неявно типизированных переменных.
using System;
class ImplicitlyTypedVar {
static void Main() {
// Эти переменные типизированы неявно. Они отнесены
// к типу double, поскольку инициализирующие их
// выражения сами относятся к типу double.
var s1 = 4.0;
var s2 = 5.0;
// Итак, переменная hypot типизирована неявно и
// относится к типу double, поскольку результат,
// возвращаемый методом Sqrt(), имеет тип double.
var hypot = Math.Sqrt( (s1 * s1) + (s2 * s2) );
Console.Write("Гипотенуза треугольника со сторонами " +
s1 + " by " + s2 + " равна ");
Console.WriteLine("{0:#.###}.", hypot);
// Следующий оператор не может быть скомпилирован,
// поскольку переменная s1 имеет тип double и
// ей нельзя присвоить десятичное значение.
// s1 = 12.2М; // Ошибка!
}
}
Результат выполнения этой программы оказывается таким же, как и прежде.
Важно подчеркнуть, что неявно типизированная переменная по-прежнему остает ся строго типизированной. Обратите внимание на следующую закомментированную строку из приведенной выше программы.
// s1 = 12.2М; // Ошибка!
Эта операция присваивания недействительна, поскольку переменная s1 относится к типу double. Следовательно, ей нельзя присвоить десятичное значение. Единствен ное отличие неявно типизированной переменной от обычной, явно типизированной переменной, — в способе определения ее типа. Как только этот тип будет определен, он закрепляется за переменной до конца ее существования. Это, в частности, означает, что тип переменной s1 не может быть изменен по ходу выполнения программы.
Неявно типизированные переменные внедрены в C# не для того, чтобы заменить собой обычные объявления переменных. Напротив, неявно типизированные перемен ные предназначены для особых случаев, и самый примечательный из них имеет отно шение к языку интегрированных запросов (LINQ), подробно рассматриваемому в главе 19. Таким образом, большинство объявлений переменных должно и впредь оставаться явно типизированными, поскольку они облегчают чтение и понимание исходного тек ста программы.
И последнее замечание: одновременно можно объявить только одну неявно типи зированную переменную. Поэтому объявление
var s1 = 4.0, s2 = 5.0; // Ошибка!
является неверным и не может быть скомпилировано. Ведь в нем предпринимается попытка объявить обе переменные, s1 и s2, одновременно.
Все переменные, использовавшиеся в предыдущих примерах программ, объяв лялись в самом начале метода Main(). Но в C# локальную переменную разрешается объявлять в любом кодовом блоке. Как пояснялось в главе 2, кодовый блок начинает ся открывающей фигурной скобкой и оканчивается закрывающей фигурной скобкой. Этот блок и определяет область действия. Следовательно, всякий раз, когда начинается блок, образуется новая область действия. Прежде всего область действия определяет видимость имен отдельных элементов, в том числе и переменных, в других частях про граммы без дополнительного уточнения. Она определяет также время существования локальных переменных.
В C# к числу наиболее важных относятся области действия, определяемые классом и методом. Рассмотрение области действия класса (и объявляемых в ней переменных) придется отложить до того момента, когда в этой книге будут описываться классы. А до тех пор будут рассматриваться только те области действия, которые определяют ся методом иди же в самом методе.
Область действия, определяемая методом, начинается открывающей фигурной скобкой и оканчивается закрывающей фигурной скобкой. Но если у этого метода име ются параметры, то и они входят в область действия, определяемую данным методом.
Как правило, локальные переменные объявляются в области действия, невидимой для кода, находящегося вне этой области. Поэтому, объявляя переменную в опреде ленной области действия, вы тем самым защищаете ее от доступа иди видоизменения вне данной области. Разумеется, правила области действия служат основанием для ин капсуляции.
Области действия могут быть вложенными. Например, всякий раз, когда создается кодовый блок, одновременно образуется и новая, вложенная область действия. В этом случае внешняя область действия охватывает внутреннюю область. Это означает, что локальные переменные, объявленные во внешней области действия, будут видимы для кода во внутренней области действия. Но обратное не справедливо: локальные перемен ные, объявленные во внутренней области действия, не будут видимы вне этой области.
Для того чтобы стала более понятной сущность вложенных областей действия, рас смотрим следующий пример программы.
// Продемонстрировать область действия кодового блока.
using System;
class ScopeDemo {
static void Main() {
int x; // Эта переменная доступна для всего кода внутри метода Main().
х = 10;
if(x == 10) { // начать новую область действия
int у = 20; // Эта переменная доступна только в данном кодовом блоке.
// Здесь доступны обе переменные, х и у.
Console.WriteLine("х и у: " + х + " " + у);
х = у * 2;
}
// у = 100; // Ошибка! Переменна у здесь недоступна.
// А переменная х здесь по-прежнему доступна.
Console.WriteLine("х равно " + х);
}
}
Как поясняется в комментариях к приведенной выше программе, переменная х объявляется в начале области действия метода Main(), и поэтому она доступна для всего последующего кода в пределах этого метода. В блоке условного оператора if объявляется переменная у. А поскольку этот кодовый блок определяет свою собствен ную область действия, то переменная у видима только для кода в пределах данного блока. Именно поэтому строка line у = 100;, находящаяся за пределами этого блока, закомментирована. Если удалить находящиеся перед ней символы коммента рия (//), то во время компиляции программы произойдет ошибка, поскольку пере менная у невидима за пределами своего кодового блока. В то же время переменная х может использоваться в блоке условного оператора if, поскольку коду из этого блока, находящемуся во вложенной области действия, доступны переменные, объявленные в охватывающей его внешней области действия.
Переменные могут быть объявлены в любом месте кодового блока, но они становят ся действительными только после своего объявления. Так, если объявить переменную в начале метода, то она будет доступна для всего остального кода в пределах этого метода. А если объявить переменную в конце блока, то она окажется, по существу, бес полезной, поскольку не будет доступной ни одному коду.
Если в объявление переменной включается инициализатор, то такая переменная инициализируется повторно при каждом входе в тот блок, в котором она объявлена. Рассмотрим следующий пример программы.
// Продемонстрировать время существования переменной.
using System;
class VarInitDemo {
static void Main() {
int x;
for(x = 0; x < 3; x++) {
int у = -1; // Переменная у инициализируется при каждом входе в блок.
Console.WriteLine("у равно: " + у); // Здесь всегда выводится -1
у = 100;
Console.WriteLine("у теперь равно: " + у);
}
}
}
Ниже приведен результат выполнения этой программы.
у равно: -1
у теперь равно: 100
у равно: -1
у теперь равно: 100
у равно: -1
у теперь равно: 100
Как видите, переменная у повторно инициализируется одним и тем же значением -1 при каждом входе во внутренний цикл for. И несмотря на то, что после этого цик ла ей присваивается значение 100, оно теряется при повторной ее инициализации.
В языке C# имеется еще одна особенность соблюдения правил области действия: не смотря на то, что блоки могут быть вложены, ни у одной из переменных из внутренней области действия не должно быть такое же имя, как и у переменной из внешней об ласти действия. В приведенном ниже примере программы предпринимается попытка объявить две разные переменные с одним и тем же именем, и поэтому программа не может быть скомпилирована.
/*
В этой программе предпринимается попытка объявить во внутренней
области действия переменную с таким же самым именем, как и у
переменной, определенной во внешней области действия.
*** Эта программа не может быть скомпилирована. ***
*/
using System;
class NestVar {
static void Main() {
int count;
for (count = 0; count < 10; count = count+1) {
Console.WriteLine("Это подсчет: " + count);
int count; // Недопустимо!!!
for(count = 0; count < 2; count++)
Console.WriteLine("В этой программе есть ошибка!");
}
}
}
Если у вас имеется некоторый опыт программирования на С или C++, то вам долж
но быть известно, что на присваивание имен переменным, объявляемым во внутренней
области действия, в этих языках не существует никаких ограничений. Следовательно,
в С и C++ объявление переменной count в кодовом блоке, входящем во внешний цикл
for, как в приведенном выше примере, считается вполне допустимым. Но в С и C++ та
кое объявление одновременно означает сокрытие внешней переменной. Разработчики
C# посчитали, что такого рода сокрытие имен может легко привести к программным
ошибкам, и поэтому решили запретить его.
## Преобразование и приведение типов
В программировании нередко значения переменных одного типа присваиваются
переменным другого типа. Например, в приведенном ниже фрагменте кода целое зна
чение типа int присваивается переменной с плавающей точкой типа float.
int i; float f; i = 10; f = i; // присвоить целое значение переменной типа float
Если в одной операции присваивания смешиваются совместимые типы данных,
то значение в правой части оператора присваивания автоматически преобразуется
в тип, указанный в левой его части. Поэтому в приведенном выше фрагменте кода
значение переменной i сначала преобразуется в тип float, а затем присваивается
переменной f. Но вследствие строгого контроля типов далеко не все типы данных
в С# оказываются полностью совместимыми, а следовательно, не все преобразова
ния типов разрешены в неявном виде. Например, типы bool и int несовместимы.
Правда, преобразование несовместимых типов все-таки может быть осуществлено
путем приведения. Приведение типов, по существу, означает явное их преобразова
ние. В этом разделе рассматривается как автоматическое преобразование, так и при
ведение типов.
### Автоматическое преобразование типов
Когда данные одного типа присваиваются переменной другого типа, неявное преоб
разование типов происходит автоматически при следующих условиях:
* оба типа совместимы;
* диапазон представления чисел целевого типа шире, чем у исходного типа.
*
Если оба эти условия удовлетворяются, то происходит расширяющее преобразование.
Например, тип int достаточно крупный, чтобы вмещать в себя все действительные
значения типа byte, а кроме того, оба типа, int и byte, являются совместимыми цело
численными типами, и поэтому для них вполне возможно неявное преобразование.
Числовые типы, как целочисленные, так и с плавающей точкой, вполне совместимы
друг с другом для выполнения расширяющих преобразований. Так, приведенная ниже
программа составлена совершенно правильно, поскольку преобразование типа long
в тип double является расширяющим и выполняется автоматически.
// Продемонстрировать неявное преобразование типа long в тип double. using System;
class LtoD { static void Main() { long L; double D; L = 100123285L; D = L; Console.WriteLine("L и D: " + L + " " + D); } }
Если тип long может быть преобразован в тип double неявно, то обратное пре
образование типа double в тип long неявным образом невозможно, поскольку оно
не является расширяющим. Следовательно, приведенный ниже вариант предыдущей
программы составлен неправильно.
// *** Эта программа не может быть скомпилирована. *** using System; class LtoD { static void Main() { long L; double D; D = 100123285.0; L = D; // Недопустимо!!! Console.WriteLine("L и D: " + L + " " + D); } }
Помимо упомянутых выше ограничений, не допускается неявное взаимное преоб
разование типов decimal и float иди double, а также числовых типов и char или
bool. Кроме того, типы char и bool несовместимы друг с другом.
### Приведение несовместимых типов
Несмотря на всю полезность неявных преобразований типов, они неспособны удо
влетворить все потребности в программировании, поскольку допускают лишь расши
ряющие преобразования совместимых типов. А во всех остальных случаях приходится
обращаться к приведению типов. Приведение — это команда компилятору преобразо
вать результат вычисления выражения в указанный тип. А для этого требуется явное
преобразование типов. Ниже приведена общая форма приведения типов.
(целевой_тип) выражение
Здесь целевой_тип обозначает тот тип, в который желательно преобразовать ука
занное выражение. Рассмотрим для примера следующее объявление переменных.
double х, у;
Если результат вычисления выражения х/у должен быть типа int, то следует за
писать следующее.
(int) (х / у)
Несмотря на то что переменные х и у относятся к типу double, результат вычисле
ния выражения х/у преобразуется в тип int благодаря приведению. В данном приме
ре выражение х/у следует непременно указывать в скобках, иначе приведение к типу
int будет распространяться только на переменную х, а не на результат ее деления на
переменную у. Приведение типов в данном случае требуется потому, что неявное пре
образование типа double в тип int невозможно.
Если приведение типов приводит к сужающему преобразованию, то часть информа
ции может быть потеряна. Например, в результате приведения типа long к типу int
часть информации потеряется, если значение типа long окажется больше диапазона
представления чисел для типа int, поскольку старшие разряды этого числового значе
ния отбрасываются. Когда же значение с плавающей точкой приводится к целочислен
ному, то в результате усечения теряется дробная часть этого числового значения. Так,
если присвоить значение 1,23 целочисленной переменной, то в результате в ней оста
нется лишь целая часть исходного числа (1), а дробная его часть (0,23) будет потеряна.
В следующем примере программы демонстрируется ряд преобразований типов,
требующих приведения. В этом примере показан также ряд ситуаций, в которых при
ведение типов становится причиной потери данных.
// Продемонстрировать приведение типов. using System;
class CastDemo { static void Main() { double x, y; byte b; int i; char ch; uint u; short s; long 1; x = 10.0; у = 3.0; // Приведение типа double к типу int, дробная часть числа теряется. i = (int) (х / у); Console.WriteLine("Целочисленный результат деления х / у: " + i); Console.WriteLine();
// Приведение типа int к типу byte без потери данных,
i = 255;
b = (byte) i;
Console.WriteLine("b после присваивания 255: " + b +
" -- без потери данных.");
// Приведение типа int к типу byte с потерей данных,
i = 257;
b = (byte) i;
Console.WriteLine("b после присваивания 257: " + b +
" -- с потерей данных.");
Console.WriteLine();
// Приведение типа uint к типу short без потери данных.
u = 32000;
s = (short) u;
Console.WriteLine("s после присваивания 32000: " +
s + " -- без потери данных.");
// Приведение типа uint к типу short с потерей данных,
u = 64000;
s = (short) u;
Console.WriteLine("s после присваивания 64000: " +
s + " -- с потерей данных.");
Console.WriteLine();
// Приведение типа long к типу uint без потери данных.
l = 64000;
u = (uint) l;
Console.WriteLine ("u после присваивания 64000: " + u +
" -- без потери данных.");
// Приведение типа long к типу uint с потерей данных.
l = -12;
u = (uint) l;
Console.WriteLine("и после присваивания -12: " + u +
" -- с потерей данных.");
Console.WriteLine();
// Приведение типа int к типу char,
b = 88; // код ASCII символа X
ch = (char) b;
Console.WriteLine("ch после присваивания 88: " + ch);
}
} Вот какой результат дает выполнение этой программы.
Целочисленный результат деления х / у: 3
b после присваивания 255: 255 -- без потери данных.
b после присваивания 257: 1 -- с потерей данных.
s после присваивания 32000: 32000 -- без потери данных.
s после присваивания 64000: -1536 -- с потерей данных.
u после присваивания 64000: 64000 -- без потери данных.
u после присваивания -12: 4294967284 -- с потерей данных.
ch после присваивания 88: X
Рассмотрим каждую операцию присваивания в представленном выше примере программы по отдельности. Вследствие приведения результата деления х/у к типу int отбрасывается дробная часть числа, а следовательно, теряется часть информации.
Когда переменной b присваивается значение 255, то информация не теряется, по скольку это значение входит в диапазон представления чисел для типа byte. Но когда переменной b присваивается значение 257, то часть информации теряется, поскольку это значение превышает диапазон представления чисел для типа byte. Приведение типов требуется в обоих случаях, поскольку неявное преобразование типа int в тип byte невозможно.
Когда переменной s типа short присваивается значение 32 000 переменной и типа uint, потери данных не происходит, поскольку это значение входит в диапазон пред ставления чисел для типа short. Но в следующей операции присваивания перемен ная и имеет значение 64 000, которое оказывается вне диапазона представления чисел для типа short, и поэтому данные теряются. Приведение типов требуется в обоих случаях, поскольку неявное преобразование типа uint в тип short невозможно.
Далее переменной u присваивается значение 64 000 переменной l типа long. В этом случае данные не теряются, поскольку значение 64 000 оказывается вне диапа зона представления чисел для типа uint. Но когда переменной u присваивается зна чение -12, данные теряются, поскольку отрицательные числа также оказываются вне диапазона представления чисел для типа uint. Приведение типов требуется в обоих случаях, так как неявное преобразование типа long в тип uint невозможно.
И наконец, когда переменной char присваивается значение типа byte, информа ция не теряется, но приведение типов все же требуется.
Помимо операций присваивания, преобразование типов происходит и в самих вы ражениях. В выражении можно свободно смешивать два или более типа данных, при условии их совместимости друг с другом. Например, в одном выражении допускает ся применение типов short и long, поскольку оба типа являются числовыми. Когда в выражении смешиваются разные типы данных, они преобразуются в один и тот же тип по порядку следования операций.
Преобразования типов выполняются по принятым в C# правилам продвижения ти пов. Ниже приведен алгоритм, определяемый этими правилами для операций с двумя операндами.
- ЕСЛИ один операнд имеет тип decimal, ТО и второй операнд продвигается к типу decimal (но если второй операнд имеет тип float или double, результат будет ошибочным).
- ЕСЛИ один операнд имеет тип double, ТО и второй операнд продвигается к типу double.
- ЕСЛИ один операнд имеет тип float, ТО и второй операнд продвигается к типу float.
- ЕСЛИ один операнд имеет тип ulong, ТО и второй операнд продвигается к типу ulong (но если второй операнд имеет тип sbyte, short, int или long, результат будет ошибочным).
- ЕСЛИ один операнд имеет тип long, ТО и второй операнд продвигается к типу long.
- ЕСЛИ один операнд имеет тип uint, а второй — тип sbyte, short или int, ТО оба операнда продвигаются к типу long.
- ЕСЛИ один операнд имеет тип uint, ТО и второй операнд продвигается к типу uint.
- ИНАЧЕ оба операнда продвигаются к типу int.
Относительно правил продвижения типов необходимо сделать ряд важных заме чаний. Во-первых, не все типы могут смешиваться в выражении. В частности, неявное преобразование типа float или double в тип decimal невозможно, как, впрочем, и смешение типа ulong с любым целочисленным типом со знаком. Для смешения этих типов требуется явное их приведение.
Во-вторых, особого внимания требует последнее из приведенных выше правил. Оно гласит: если ни одно из предыдущих правил не применяется, то все операнды про двигаются к типу int. Следовательно, все значения типа char, sbyte, byte, ushort и short продвигаются к типу int в целях вычисления выражения. Такое продвижение типов называется целочисленным. Это также означает, что результат выполнения всех арифметических операций будет иметь тип не ниже int.
Следует иметь в виду, что правила продвижения типов применяются только к зна чениям, которыми оперируют при вычислении выражения. Так, если значение пере менной типа byte продвигается к типу int внутри выражения, то вне выражения эта переменная по-прежнему относится к типу byte. Продвижение типов затрагивает только вычисление выражения.
Но продвижение типов может иногда привести к неожиданным результатам. Если, например, в арифметической операции используются два значения типа byte, то про исходит следующее. Сначала операнды типа byte продвигаются к типу int. А затем выполняется операция, дающая результат типа int. Следовательно, результат выпол нения операции, в которой участвуют два значения типа byte, будет иметь тип int. Но ведь это не тот результат, который можно было бы с очевидностью предположить. Рассмотрим следующий пример программы.
// Пример неожиданного результата продвижения типов!
using System;
class PromDemo {
static void Main() {
byte b;
b = 10;
b = (byte) (b * b); // Необходимо приведение типов!!
Console.WriteLine("b: "+ b);
}
}
Как ни странно, но когда результат вычисления выражения bb присваивается обрат но переменной b, то возникает потребность в приведении к типу byte! Объясняется это тем, что в выражении bb значение переменной b продвигается к типу int и поэтому не может быть присвоено переменной типа byte без приведения типов. Имейте это обсто ятельство в виду, если получите неожиданное сообщение об ошибке несовместимости типов в выражениях, которые, на первый взгляд, кажутся совершенно правильными.
Аналогичная ситуация возникает при выполнении операций с символьными опе рандами. Например, в следующем фрагменте кода требуется обратное приведение к типу char, поскольку операнды ch1 и ch2 в выражении продвигаются к типу int.
char ch1 = 'a', ch2 = 'b';
ch1 = (char) (ch1 + ch2);
Без приведения типов результат сложения операндов ch1 и ch2 будет иметь тип int, и поэтому его нельзя присвоить переменной типа char.
Продвижение типов происходит и при выполнении унарных операций, например с унарным минусом. Операнды унарных операций более мелкого типа, чем int(byte, sbyte, short и ushort), т.е. с более узким диапазоном представления чисел, про двигаются к типу int. То же самое происходит и с операндом типа char. Кроме того, если выполняется унарная операция отрицания значения типа uint, то результат про двигается к типу long.
Приведение типов можно применять и к отдельным частям крупного выражения. Это позволяет точнее управлять преобразованиями типов при вычислении выраже ния. Рассмотрим следующий пример программы, в которой выводятся квадратные корни чисел от 1 до 10 и отдельно целые и дробные части каждого числового результа та. Для этого в данной программе применяется приведение типов, благодаря которо му результат, возвращаемый методом Math.Sqrt(), преобразуется в тип int.
// Пример приведения типов в выражениях.
using System;
class CastExpr {
static void Main() {
double n;
for(n = 1.0; n <= 10; n++) {
Console.WriteLine("Квадратный корень из {0} равен {1}",
n, Math.Sqrt(n));
Console.WriteLine("Целая часть числа: (0)",
(int) Math.Sqrt(n));
Console.WriteLine("Дробная часть числа: (0)",
Math.Sqrt(n) - (int) Math.Sqrt(n) );
Console.WriteLine();
}
}
}
Вот как выглядит результат выполнения этой программы.
Квадратный корень из 1 равен 1
Целая часть числа: 1
Дробная часть числа: 0
Квадратный корень из 2 равен 1.4142135623731
Целая часть числа: 1
Дробная часть числа: 0.414213562373095
Квадратный корень из 3 равен 1.73205080756888
Целая часть числа: 1
Дробная часть числа: 0.732050807568877
Квадратный корень из 4 равен 2
Целая часть числа: 2
Дробная часть числа: 0
Квадратный корень из 5 равен 2.23606797749979
Целая часть числа: 2
Дробная часть числа: 0.23606797749979
Квадратный корень из 6 равен 2.44948974278318
Целая часть числа: 2
Дробная часть числа: 0.449489742783178
Квадратный корень из 7 равен 2.64575131106459
Целая часть числа: 2
Дробная часть числа: 0.645751311064591
Квадратный корень из 8 равен 2.82842712474619
Целая часть числа: 2
Дробная часть числа: 0.82842712474619
Квадратный корень из 9 равен 3
Целая часть числа: 3
Дробная часть числа: 0
Квадратный корень из 10 равен 3.16227766016838
Целая часть числа: 3
Дробная часть числа: 0.16227766016838
Как видите, приведение результата, возвращаемого методом Math.Sqrt(), к типу int позволяет получить целую часть числа. Так, в выражении
Math.Sqrt(n) - (int) Math.Sqrt(n)
приведение к типу int дает целую часть числа, которая затем вычитается из всего числа, а в итоге получается дробная его часть. Следовательно, результат вычисления данного выражения имеет тип double. Но к типу int приводится только значение, возвращаемое вторым методом Math.Sqrt().