Справочник по C# Герберт Шилдт ббк 32. 973. 26018 75 Ш57 удк 681 07 Издательский дом "Вильямс" Зав редакцией
Скачать 5.05 Mb.
|
62 Часть I. Язык C# О некоторых вариантах вывода данных До сих пор при выводе данных с помощью метода WriteLine() они отображались с использованием стандартного формата, определенного в C#. Однако в C# предусмотрен и более высокоорганизованный механизм форматирования, который позволяет более тонко управлять отображением данных. Несмотря на то, что форматированный ввод-вывод подробно описывается далее, нам не обойтись без рассмотрения некоторых деталей уже сейчас. Они позволят сделать результаты, выводимые программой, более читабельными и привлекательными. Однако не забывайте, что в этом разделе описана только малая часть средств форматирования, поддерживаемых в C#. При выводе списка данных его элементы необходимо разделять знаками “плюс”. Вот пример: Console.WriteLine( "Вы заказали " + 2 + " предмета по $" + 3 + " каждый."); Несмотря на определенные достоинства такого способа вывода данных, он не дает никаких “рычагов” управления их форматированием. Например, выводя значение с плавающей точкой, вы не сможете управлять количеством отображаемых десятичных разрядов. Рассмотрим следующую инструкцию: Console.WriteLine( "При делении 10/3 получаем: " + 10.0/3.0); При ее выполнении увидим на экране такой результат: при делении 10/3 получаем: 3,33333333333333 Результат, представленный с таким количеством десятичных разрядов, годится при решении одних задач и совершенно неприемлем в других случаях. Например, в денежных расчетах обычно ограничиваются отображением только двух десятичных разрядов. Для управления форматированием числовых данных необходимо использовать вторую форму метода WriteLine() , которая позволяет ввести информацию о форматировании. WriteLine(" строка_форматирования ", arg0 , arg1 , ... , argN ); В этой версии метода WriteLine() передаваемые ему аргументы разделяются запятыми, а не знаками “ + ”. Элемент строка_форматирования содержит две составляющие: “постоянную” и “переменную”. Постоянная составляющая представляет собой печатные символы, отображаемые “как есть”, а переменная состоит из спецификаторов формата. Общая форма записи спецификатора формата имеет следующий вид: { номер_аргумента , ширина : формат } Здесь элемент номер_аргумента определяет порядковый номер отображаемого аргумента (начиная с нулевого). С помощью элемента ширина указывается минимальная ширина поля, а формат задается элементом формат . Если при выполнении метода WriteLine() в строке форматирования встречается спецификатор формата, вместо него подставляется (и отображается) аргумент, соответствующий заданному элементу номер_аргумента . Таким образом, элементы номер_аргумента указывают позицию спецификации в строке форматирования, которая определяет, где именно должны быть отображены соответствующие данные. Элементы ширина и формат указывать необязательно. Следовательно, спецификатор формата {0} означает arg0 , {1} означает arg1 и т.д. Теперь рассмотрим простой пример. При выполнении инструкции Глава 3. Типы данных, литералы и переменные 63 Console.WriteLine("В феврале {0} или {1} дней.", 28, 29); будет сгенерирован следующий результат: В феврале 28 или 29 дней. Как видите, вместо спецификатора {0} было подставлено значение 28, а вместо спецификатора {1} — значение 29. Таким образом, внутри строки форматирования спецификаторы формата идентифицируют местоположение последовательно заданных аргументов (в данном случае это числа 28 и 29). Обратите также внимание на то, что составные части выводимого результата разделены не знаками “ + ”, а запятыми. А теперь “сыграем” вариацию на тему предыдущей инструкции, указав в спецификаторах формата минимальную ширину поля. Console.WriteLine( "В феврале {0,10} или {1,5} дней.", 28, 29); Вот как будет выглядеть результат ее выполнения: В феврале 28 или 29 дней. Нетрудно убедиться, что при выводе значений аргументов были добавлены пробелы, заполняющие неиспользуемые части полей. Обратите внимание на то, что второй элемент спецификатора формата означает минимальную ширину поля. Другими словами, при необходимости это значение может быть превышено. Безусловно, аргументы, связанные с командой форматирования, необязательно должны быть константами. Например, в следующей программе отображается таблица результатов возведения ряда чисел в квадрат и куб. // Использование команд форматирования. using System; class DisplayOptions { public 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() . Для этого рассмотрим пример форматирования с помощью символов “ # ”, отмечающих позиции цифр. При этом можно указать расположение десятичной 64 Часть I. Язык C# точки и запятых, которые используются в качестве разделителей групп разрядов. Выше мы приводили пример отображения частного от деления числа 10 на 3. Теперь рассмотрим еще один вариант вывода результата выполнения этой арифметической операции. Console.WriteLine( "При делении 10/3 получаем: {0:#.##}", 10.0/3.0); Теперь результат выглядит по-другому: При делении 10/3 получаем: 3.33 В этом примере шаблон имеет вид #.## , что для метода WriteLine() служит указанием отобразить лишь два десятичных разряда. Но важно понимать, что при необходимости слева от десятичной точки будет отображено столько цифр, сколько потребуется, чтобы не исказить значение. А вот еще пример. При выполнении инструкции Console.WriteLine("{0:###,###.##}", 123456.56); будет сгенерирован следующий результат: 123,456.56 Если нужно отобразить значение в формате представления долларов и центов, используйте спецификатор формата С . Вот пример: decimal balance; balance = 12323.09m; Console.WriteLine("Текущий баланс равен {0:C}, balance); Результат выполнения этой последовательности инструкций выглядит так: Текущий баланс равен $12,323.09 Формат с можно использовать для улучшения представления результата выполнения программы вычисления цены со скидкой, которая рассматривалась выше. /* Использование спецификатора формата C для вывода значений в виде долларов и центов. */ using System; class UseDecimal { public 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:C}", discounted_price); } } Посмотрите, как теперь выглядит результат выполнения программы, и сравните его с предыдущим: Цена со скидкой: 16,96р. Глава 3. Типы данных, литералы и переменные 65 Литералы В C# литералами называются фиксированные значения, представленные в понятной форме. Например, число 100 — это литерал. Литералы также называют константами. По большей части применение литералов происходит на интуитивном уровне, и поэтому мы без особых пояснений использовали их в той или иной форме во всех предыдущих примерах программ. Теперь настало время объяснить их формально. C#-литералы могут иметь любой тип значений. Способ их представления зависит от их типа. Как упоминалось выше, символьные константы заключаются между двумя одинарными кавычками. Например, как ‘ а ’, так и ‘ % ’ — символьные константы. Целочисленные литералы задаются как числа без дробной части. Например, 10 и - 100 — это целочисленные константы. Константы с плавающей точкой должны обязательно иметь десятичную точку, а за ней — дробную часть числа. Примером константы с плавающей точкой может служить число 11.123 . Для вещественных чисел C# позволяет также использовать экспоненциальное представление (в виде мантиссы и порядка). Поскольку C# — строго типизированный язык, литералы в нем также имеют тип. Отсюда сразу возникает вопрос: как определить тип литерала? Например, какой тип имеют такие литералы, как 12 , 123987 или 0.23 ? К счастью, C# определяет несколько простых правил, позволяющих ответить на эти вопросы. Во-первых, что касается целочисленных литералов, то им присваивается наименьший целочисленный тип, который сможет его хранить, начиная с типа int . Таким образом, целочисленный литерал, в зависимости от конкретного значения, может иметь тип int , uint , long или ulong . Во-вторых, все литералы с плавающей точкой имеют тип double Если тип, задаваемый по умолчанию в языке C#, не соответствует вашим намерениям в отношении типа конкретного литерала, вы можете явно определить его с помощью нужного суффикса. Чтобы задать литерал типа long , присоедините к его концу букву l или L . Например, если значение 12 автоматически приобретает тип int , но значение 12L имеет тип long . Чтобы определить целочисленное значение без знака, используйте суффикс u или U . Так, если значение 100 имеет тип int , но значение 100U — тип uint Для задания длинного целого без знака используйте суффикс ul или UL (например, значение 987654UL будет иметь тип ulong ). Чтобы задать литерал типа float , используйте суффикс f или F (например, 10.19F ). Чтобы задать литерал типа decimal , используйте суффикс m или М (например, 9. 95М ). Несмотря на то что целочисленные литералы создают int -, uint -, long - или ulong -значения по умолчанию, их тем не менее можно присваивать переменным типа byte , sbyte , short или ushort , если, конечно, они могут быть представлены соответствующим типом. Шестнадцатеричные литералы Вероятно, вам известно, что в программировании вместо десятичной иногда удобнее использовать систему счисления по основанию 16, которая называется шестнадцатеричной. В ней используются цифры от 0 до 9 и буквы от А до F, которые служат для обозначения шестнадцатеричных “цифр” 10, 11, 12, 13, 14 и 15. Например, число 10 в шестнадцатеричной системе равно десятичному числу 16. Язык C#, как и многие другие языки программирования, позволяет задавать целочисленные константы в шестнадцатеричном формате. Шестнадцатеричный литерал должен начинаться с пары символов 0x (нуля и буквы “ x ”). Приведем несколько примеров. count = 0xFF; // 255 в десятичной системе 66 Часть I. Язык C# incr = 0x1a; // 26 в десятичной системе Управляющие последовательности символов Среди множества символьных констант, образующихся в результате заключения символов в одинарные кавычки, помимо печатных символов есть такие (например, символ возврата каретки), которые создают проблему при использовании текстовых редакторов. Некоторые символы, например одинарная или двойная кавычка, имеют в C# специальное значение, поэтому их нельзя использовать непосредственно. По этим причинам в C# предусмотрено несколько управляющих последовательностей символов (ESC- последовательностей), перечисленных в табл. 3.3. Эти последовательности используются вместо символов, которых они представляют. Например, следующая инструкция присваивает переменной ch символ табуляции: ch = ‘\t’; А эта инструкция присваивает переменной ch символ одинарной кавычки: ch = ‘\’’; Таблица 3.3. Управляющие последовательности символов ESC-последовательность Описание \а Звуковой сигнал (звонок) \b Возврат на одну позицию \f Подача страницы (для перехода к началу следующей страницы) \n Новая строка \r Возврат каретки \t Горизонтальная табуляция \v Вертикальная табуляция \0 Нуль-символ \’ Одинарная кавычка (апостроф) \" Двойная кавычка \\ Обратная косая черта Строковые литералы Язык C# поддерживает еще один тип литерала: строковый. Строка — это набор символов, заключенных в двойные кавычки. Например, фрагмент кода "Это тест" представляет собой строку. В предыдущих фрагментах программ (а именно в инструкциях вызова метода WriteLine() ) вы видели другие примеры строк. Помимо обычных символов, строковый литерал может содержать одну или несколько управляющих последовательностей. Рассмотрим, например, следующую программу. В ней используются такие ESC-последовательности, как \n , \t и \" // Использование ESC-последовательностей в строках. using System; Глава 3. Типы данных, литералы и переменные 67 class StrDemo { public static void Main() { Console.WriteLine( "Первая строка\nВторая строка\nТретья строка"); Console.WriteLine("Один\tДва\tТри"); Console.WriteLine("Четыре\tПять\tШесть"); // Вставляем кавычки. Console.WriteLine("\"Зачем?\", спросил он."); } } Вот что получаем в результате: Первая строка Вторая строка Третья строка Один Два Три Четыре Пять Шесть "Зачем?", спросил он. Обратите внимание на то, как управляющая последовательность \n используется для перехода на новую строку, благодаря чему не нужно многократно вызывать метод WriteLine() для организации выводимых данных на нескольких строках. В те позиции, где необходимо сделать переход на новую строку, достаточно вставить ESC- последовательность \n . Обратите также внимание на то, как в выводимых строках обеспечивается наличие двойных кавычек (с помощью ESC-последовательности \" ). Помимо формы только что описанного строкового литерала можно также определить буквальный (verbatim) строковый литерал. Буквальный строковый литерал начинается с символа @ , за которым следует строка, заключенная в кавычки. Содержимое строки в кавычках принимается без какой бы то ни было модификации и может занимать две или более строк. Таким образом, можно переходить на новую строку, использовать табуляцию и пр., не прибегая к помощи управляющих последовательностей. Единственное исключение составляет двойная кавычка ( " ). Чтобы получить в выходных данных двойную кавычку, в буквальном строковом литерале необходимо использовать две подряд двойные кавычки ( "" ). А теперь обратимся к программе, в которой демонстрируется использование буквального строкового литерала. // Демонстрация буквальных строковых литералов. using System; class Verbatim { public static void Main() { Console.WriteLine(@"Это буквальный строковый литерал, который занимает несколько строк. "); Console.WriteLine(@"А теперь воспользуемся табуляцией: 1 2 3 4 5 6 7 8 "); Console.WriteLine( @"Отзыв программиста: ""Мне нравится C#."""); } } Вот что сгенерирует эта программа: 68 Часть I. Язык C# Это буквальный строковый литерал, который занимает несколько строк А теперь воспользуемся табуляцией: 1 2 3 4 5 6 7 8 Отзыв программиста: "Мне нравится C#." Здесь важно отметить, что буквальные строковые литералы отображаются точно так, как они введены в программе. Они позволяют программисту так формировать выходные данные, как они будут отображены на экране. Но в случае многострочного вывода переход на следующую строку нарушит систему формирования отступов в программе. Поэтому буквальные строковые литералы не слишком часто используются в программах этой книги, хотя во многих случаях форматирования данных они оказываются хорошим подспорьем. И последнее. Не путайте строки с символами. Символьный литерал (например, 'X' ) представляет одиночную букву типа char . А строка, хотя и содержащая всего одну букву (например, “ X ”), это все-таки строка. Рассмотрим переменные поближе Как вы узнали в главе 2, для объявления переменной необходимо использовать инструкцию следующего формата: тип имя_переменной ; Здесь с помощью элемента тип задается тип объявляемой переменной, а с помощью элемента имя_переменной — ее имя. Можно объявить переменную любого допустимого типа. При создании переменной создается экземпляр соответствующего типа. Таким образом, возможности переменной определяются ее типом. Например, переменную типа bool нельзя использовать для хранения значений с плавающей точкой. Более того, тип переменной невозможно изменить во время ее существования. Например, переменную типа int нельзя преобразовать в переменную типа char Все переменные в C# должны быть объявлены до их использования. Это — требование компилятора, поскольку, прежде чем скомпилировать надлежащим образом инструкцию, в которой используется переменная, он должен “знать” тип содержащейся в ней информации. “Знание” типа также позволяет C# осуществлять строгий контроль типов. Помимо типов переменные различаются и другими качествами. Например, переменные, которые мы использовали в примерах программ до сих пор, называются локальными, поскольку они объявляются внутри метода. Инициализация переменной Переменная до использования должна получить значение. Это можно сделать с помощью инструкции присваивания. Можно также присвоить переменной начальное значение одновременно с ее объявлением. Для этого достаточно после имени переменной поставить знак равенства и указать присваиваемое значение. Общий формат инициализации переменной имеет такой вид: тип имя_переменной = значение ; Глава 3. Типы данных, литералы и переменные 69 Здесь, как нетрудно догадаться, элемент значение — это начальное значение, которая получает переменная при создании. Значение инициализации должно соответствовать заданному типу переменной. Вот несколько примеров: int count = 10; // Присваиваем переменной count // начальное значение 10. char ch = 'X'; // Инициализируем ch буквой X. float f = 1.2F // Переменная f инициализируется // числом 1.2. При объявлении двух или более переменных одного типа с помощью списка (с разделением элементов списка запятыми) одной или нескольким из этих переменных можно присвоить начальные значения. Например, в инструкции int a, b = 8, c = 19, d; //Переменные b и c // инициализируются числами. Динамическая инициализация Хотя в предыдущих примерах в качестве инициализаторов были использованы только константы, C# позволяет инициализировать переменные динамически, с помощью любого выражения, действительного на момент объявления переменной. Рассмотрим, например, короткую программу, которая вычисляет гипотенузу прямоугольного треугольника, заданного длинами двух противоположных сторон. // Демонстрация динамической инициализации. using System; class DynInit { public static void Main() { double s1 = 4.0, 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 . Здесь важно то, что в выражении инициализации можно использовать любой элемент, действительный на момент инициализации, включая вызовы методов, другие переменные или литералы. 70 Часть I. Язык C# Область видимости и время существования переменных До сих пор все переменные, с которыми мы имели дело, объявлялись в начале метода Main() . Однако в C# разрешается объявлять переменные внутри любого блока. Блок начинается открывающей, а завершается закрывающей фигурными скобками. Любой блок определяет область объявления, или область видимости (scope) объектов. Таким образом, при создании блока создается и новая область видимости, которая определяет, какие объекты видимы для других частей программы. Область видимости также определяет время существования этих объектов. Самыми важными в C# являются области видимости, которые определены классом и методом. Область видимости класса (и переменные, объявленные внутри нее) мы рассмотрим позже, когда доберемся до описания классов, а пока затронем области видимости, определяемые методами. Область видимости, определяемая методом, начинается с открывающей фигурной скобки. Но если метод имеет параметры, они также относятся к области видимости метода. Как правило, переменные, объявленные в некоторой области видимости, невидимы (т.е. недоступны) для кода, который определяется вне этой области видимости. Таким образом, при объявлении переменной внутри области видимости вы локализируете ее и защищаете от неправомочного доступа и/или модификации. Эти правила области видимости обеспечивают основу для инкапсуляции. Области видимости могут быть вложенными. Например, при каждом создании программного блока создается новая вложенная область видимости. В этом случае внешняя область включает внутреннюю. Это означает, что объекты, объявленные внутри внешней области, будут видимы для кода внутренней области. Но обратное утверждение неверно: объекты, объявленные во внутренней области, невидимы вне ее. Чтобы лучше понять суть вложенных областей видимости, рассмотрим следующую программу: // Демонстрация области видимости блока. using System; class ScopeDemo { public static void Main() { int x; // Переменная x известна всему коду в пределах // метода Main(). x = 10; if(x == 10) { // Начало новой области видимости. int y = 20; // Переменная y известна только // этому блоку. // Здесь известны обе переменные x и y. Console.WriteLine("x и y: " + x + " " + y); x = y * 2; } // y = 100; // Ошибка! Переменная y здесь неизвестна. // Переменная x здесь известна. Console.WriteLine("Значение x равно " + x); } } Глава 3. Типы данных, литералы и переменные 71 Как утверждается в комментариях, переменная x объявляется в начале области видимости метода Main() и потому доступна всему последующему коду метода. Внутри блока инструкции if объявляется переменная y . А поскольку блок определяет область видимости, то переменная y видима только коду внутри этого блока. Поэтому, находясь вне этого блока, программная строка // y = 100; //Ошибка! Переменная y здесь неизвестна. оформлена как комментарий. Если убрать символ комментария, компилятор выдаст сообщение об ошибке, поскольку переменная y невидима вне if -блока. Переменную x можно свободно использовать и внутри if -блока, поскольку внутренний код этого блока (т.е. код во вложенной области видимости) имеет доступ к переменным, объявленным вне его. Внутри блока переменные можно объявлять в любой точке, но действительными они становятся только после объявления. Таким образом, если объявить переменную в начале метода, она будет доступна всему коду этого метода. И наоборот, если объявить переменную в конце метода, она будет попросту бесполезной ввиду отсутствия кода, который мог бы ее использовать. Переменные создаются после входа в их область видимости, а разрушаются при выходе из нее. Это означает, что переменная не будет хранить значение за пределами области видимости. Таким образом, переменная, объявленная внутри некоторого метода, не будет хранить значение между вызовами этого метода. И точно так же переменная, объявленная внутри некоторого блока, потеряет свое значение по завершении этого блока. Следовательно, время существования переменной ограничивается ее областью видимости. Если объявление переменной включает инициализатор, такая переменная будет повторно инициализироваться при каждом входе в блок, в котором она объявляется. Рассмотрим, например, следующую программу: // Демонстрация времени существования переменной. using System; class VarInitDemo { public static void Main() { int x; for(x = 0; x < 3; x++) { int y = -1; // Переменная y инициализируется при // каждом входе в программный блок Console.WriteLine("Значение y равно: " + y); // Здесь // всегда выводится -1. y = 100; Console.WriteLine("Теперь значение y равно: " + y); } } } Вот какие результаты генерирует эта программа: Значение y равно: -1 Теперь значение y равно: 100 Значение y равно: -1 Теперь значение y равно: 100 Значение y равно: -1 Теперь значение y равно: 100 72 Часть I. Язык C# Как видите, при каждом входе в цикл for переменная y неизменно принимает значение —1. Несмотря на последующее присваивание ей значения 100, она это значение теряет. В правилах действия областей видимости есть одна деталь: хотя блоки могут быть вложенными, ни одна переменная, объявленная во внутренней области видимости, не может иметь имя, совпадающее с именем переменной, объявленной во внешней области видимости. Например, следующая программа из-за попытки объявить две отдельные переменные с одинаковыми именами скомпилирована не будет. /* Здесь делается попытка объявить переменную во внутренней области видимости с таким же именем, как у переменной, определенной во внешней области видимости. *** Эта программа не будет скомпилирована. *** */ using System; class NestVar { public static void Main() { int count; for(count = 0; count < 10; count = count+1) { Console.WriteLine("This is count: " + count); int count; // Неверно!!! for(count = 0; count < 2; count++) Console.WriteLine("В этой программе есть ошибка!"); } } } Если вы до этого программировали на C/C++, вам должно быть известно, что на имена, объявляемые во внутренней области видимости, никакие ограничения не накладываются. Таким образом, в языках C/C++ объявление переменной count внутри блока внешнего цикла for было бы совершенно законным. Однако при всей своей законности такое объявление скрывает внешнюю переменную. Поэтому разработчики C#, зная, что подобное сокрытие имен может легко привести к ошибкам программирования, решили запретить его. Преобразование и приведение типов В программировании переменной одного типа часто присваивается значение переменной другого типа. Например, как показано в следующем фрагменте программы, мы могли бы присвоить переменной типа float значение типа int int i; float f; i = 10; f = i; //float-переменной присваивается int-значение. Если в инструкции присваивания смешиваются совместимые типы, значение с правой стороны (от оператора присваивания) автоматически преобразуется в значение “левостороннего” типа. Таким образом, в предыдущем фрагменте программы значение, Глава 3. Типы данных, литералы и переменные 73 хранимое в int -переменной i , преобразуется в значение типа float , а затем присваивается переменной f . Но, поскольку в C# не все типы совместимы и действует строгий контроль типов, не все преобразования типов разрешены в неявном виде. Например, типы bool и int несовместимы. Тем не менее, с помощью операции приведения типов все-таки возможно выполнить преобразование между несовместимыми типами. Приведение типов — это выполнение преобразования типов в явном виде. Автоматическое преобразование типов При присвоении значения одного типа данных переменной другого типа будет выполнено автоматическое преобразование типов, если ■ эти два типа совместимы; ■ тип приемника больше (т.е. имеет больший диапазон представления чисел), чем тип источника. При соблюдении этих двух условий выполняется преобразование с расширением, или расширяющее преобразование. Например, тип int — достаточно “большой” тип, чтобы сохранить любое допустимое значение типа byte , а поскольку как int , так и byte — целочисленные типы, здесь может быть применено автоматические преобразование. Для расширяющих преобразований числовые типы, включая целочисленные и с плавающей точкой, совместимы один с другим. Например, следующая программа совершенно легальна, поскольку преобразование типов из long в double является расширяющим, которое выполняется автоматически. // Демонстрация автоматического преобразования типов // из long в double. using System; class LtoD { public 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 { public static void Main() { long L; double D; D = 100123285.0; L = D; // Неверно!!! 74 Часть I. Язык C# Console.WriteLine("L и D: " + L + " " + D); } } Помимо только что описанных ограничений не существует автоматического преобразования между типом decimal и float (или double ), а также из числовых типов в тип char (или bool ). Кроме того, несовместимы и типы char и bool Приведение несовместимых типов Несмотря на большую пользу автоматического преобразования типов оно не в состоянии удовлетворить все нужды программирования, поскольку реализуется только при расширяющем преобразовании между совместимыми типами. Во всех остальных случаях приходится применять приведение к типу. Приведение к типу — это явно заданная инструкция компилятору преобразовать один тип в другой. Инструкция приведения записывается в следующей общей форме: ( тип_приемника ) выражение Здесь элемент тип_приемника определяет тип для преобразования заданного выражения. Например, если вам нужно, чтобы выражение x/y имело тип int , напишите следующие программные инструкции: double x, y; // ... (int) (x / y); В этом фрагменте кода, несмотря на то, что переменные x и y имеют тип double , результат вычисления заданного выражения приводится к типу int . Круглые скобки, в которые заключено выражение x / y , обязательны. В противном случае (без круглых скобок) операция приведения к типу int была бы применена только к значению переменной x , а не к результату деления. Для получения результата желаемого типа здесь не обойтись без операции приведения, поскольку автоматического преобразования из типа double в int не существует. Если приведение приводит к сужающему преобразованию, возможна потеря информации. Например, в случае приведения типа long к типу int информация будет утеряна, если значение типа long больше максимально возможного числа, которое способен представить тип int , поскольку будут “усечены” старшие разряды long - значения. При выполнении операции приведения типа с плавающей точкой к целочисленному будет утеряна дробная часть простым ее отбрасыванием. Например, при присвоении переменной целочисленного типа числа 1 , 23 в действительности будет присвоено число 1 . Дробная часть ( 0,23 ) будет утеряна. В следующей программе демонстрируется ряд преобразований типов, которые требуют приведения типов, причем в некоторых ситуациях приведение вызывает потерю данных. // Демонстрация приведения типов. using System; class CastDemo { public static void Main() { double x, y; byte b; int i; char ch; uint u; Глава 3. Типы данных, литералы и переменные 75 short s; long l; x = 10.0; y = 3.0; // Приведение типа double к типу int. i = (int) (x / y); // Дробная часть теряется. Console.WriteLine( "Целочисленный результат деления x / y: " + 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("u после присваивания -12: " + u + " -- с потерей данных."); Console.WriteLine(); // Приведение типа byte к типу char. b = 88; // ASCII-код для буквы X. ch = (char) b; Console.WriteLine("ch после присваивания 88: " + ch); } } 76 Часть I. Язык C# Результаты выполнения этой демонстрационной программы имеют такой вид: Целочисленный результат деления x/y: 3 b после присваивания 255: 255 -- без потери данных. b после присваивания 257: 1 -- с потерей данных. s после присваивания 32000: 32000 -- без потери данных. s после присваивания 64000: -1536 -- с потерей данных. u после присваивания 64000: 64000 -- без потери данных. u после присваивания -12: 4294 967284 -- с потерей данных. ch после присваивания 88: X Теперь рассмотрим каждую инструкцию присваивания отдельно. Приведение результата деления ( x / y ) к типу int приводит к усечению дробной части, т.е. к потере информации. Однако никакой потери информации не происходит, если переменной b присваивается значение 255, поскольку переменная типа byte в состоянии хранить число 255. Но при попытке присвоить переменной b число 257 информация будет потеряна, поскольку число 257 находится за пределами диапазона представления чисел для типа byte . В обоих этих случаях без операции приведения типов не обойтись, поскольку автоматическое преобразование типа int в тип byte невозможно. В случае присвоения переменной s типа short значения 32 000 (с помощью переменной u типа uint ) данные не теряются, потому что short -переменная может хранить число 32 000. Но следующее присвоение уже не такое успешное, поскольку число 64 000 находится за пределами диапазона представления чисел для типа short , и эта ситуация сопровождается потерей данных. В обоих этих случаях без операции приведения типов также не обойтись, поскольку автоматическое преобразование типа uint в тип short невозможно. Затем в программе переменной u (типа uint ) присваивалось значение 64 000 (с помощью переменной l типа long ). Эта инструкция была выполнена без потери данных, поскольку число 64 000 находится в пределах uint -диапазона. Но попытка присвоить той же переменной u число -12, конечно, привела к потере данных, так как тип uint не предназначен для хранения отрицательных чисел. И в этих обоих случаях без операции приведения типов не обойтись, поскольку автоматическое преобразование типа long в тип uint невозможно. Наконец, присваивание byte -значения переменной типу char обходится “без жертв”, т.е. без потери информации, но здесь также необходимо применять операцию приведения типов. Преобразование типов в выражениях Преобразование типов встречается не только в инструкциях присваивания, но и в выражениях. В выражениях можно смешивать различные типы данных, если они совместимы. Например, можно смешивать типы short и long , поскольку это числовые типы. При смешении различных типов в одном выражении все его составляющие преобразуются к одному типу, причем это происходит по мере перехода от одной операции к другой. Преобразование типов выполняется на основе правил продвижения по “типовой” лестнице. Для бинарных операций действует следующий алгоритм. Глава 3. Типы данных, литералы и переменные 77 ЕСЛИ один операнд имеет тип decimal , TO и второй “возводится в ранг”, т.е. “в тип” decimal (но если второй операнд имеет тип float или double , результат будет ошибочным). ЕСЛИ один операнд имеет тип double , TO и второй преобразуется в значение типа double ЕСЛИ один операнд имеет тип float , TO и второй преобразуется в значение типа float ЕСЛИ один операнд имеет тип ulong , TO и второй преобразуется в значение типа ulong (но если второй операнд имеет тип sbyte , short , int или long , результат будет ошибочным). ЕСЛИ один операнд имеет тип long , TO и второй преобразуется в значение типа long ЕСЛИ один операнд имеет тип uint , а второй имеет тип sbyte , short или int , ТО оба операнда преобразуются в значения типа long ЕСЛИ один операнд имеет тип uint , TO и второй преобразуется в значение типа uint ИНАЧЕ оба операнда преобразуются в значения типа int Относительно правил продвижения по “типовой” лестнице необходимо сделать несколько замечаний. Во-первых не все типы можно смешивать в одном выражении. Например, не выполняется неявное преобразование значения типа float или double в значение типа decimal . Нельзя также смешивать тип ulong и целочисленный тип со знаком. Чтобы все-таки объединить эти несовместимые типы в одном выражении, необходимо использовать в явном виде операцию приведения типов. Во-вторых, уделите особое внимание последнему правилу. Оно утверждает, что все операнды будут преобразованы в значения типа int , если не было применено ни одно их предыдущих правил. Следовательно, в выражении все char -, sbyte -, byte -, ushort - и short -значения будут в процессе вычислений преобразованы в значения типа int . Такое “поголовное” int -преобразование называют целочисленным продвижением типа (integer promotion). Следствием этого алгоритма является то, что результат всех арифметических операций будет иметь тип по “званию” не ниже int Важно понимать, что продвижение типов применяется только к значениям, используемым при вычислении выражения. Например, хотя значение переменной типа byte внутри выражения будет “подтянуто” до типа int , вне выражения эта переменная по-прежнему имеет тип byte . Правило продвижения типов действует только при вычислении выражения. Однако продвижение типов может иметь неожиданные последствия. Например, предположим, что арифметическая операция включает два byte -значения. Тогда выполняется следующая последовательность действий. Сначала byte -операнды “подтягиваются” до типа int , затем вычисляется результат операции, который имеет тип int . Выходит, после выполнения операции над двумя byte -операндами вместо ожидаемого byte -результата мы получим int -значение. Именно такая неожиданность и может иметь место. А теперь рассмотрим такую программу: // Сюрприз в результате продвижения типов! using System; class PromDemo { public static void Main() { byte b; 78 Часть I. Язык C# b = 10; b = (byte) (b * b); // Необходимо приведение типов)!! Console.WriteLine("b: "+ b); } } Кажется странным, что при присвоении результата произведения b * b переменной b необходимо выполнять операцию приведения типов. Дело в том, что в выражении b * b значение переменной b “подтягивается” до типа int , т.е. результат выражения b * b представляет собой int -значение, которое нельзя присвоить byte -переменной без приведения типов. Имейте это в виду, если вдруг получите сообщение об ошибке, где сказано о несовместимости типов для выражений, в которых, казалось бы, все в полном порядке. Ситуация подобного рода встречается также при выполнении операций над операндами типа char . Например, в следующем фрагменте кода также необходимо “возвратить” результат к исходному типу из-за автоматического преобразования char - операндов к типу int внутри вычисляемого выражения. char ch1 = 'a', ch2 = 'b'; ch1 = (char) (ch1 + ch2); Без приведения типов результат сложения операндов ch1 и ch2 имел бы тип int , а int -значение невозможно присвоить char -переменной. Продвижение типов также имеет место при выполнении унарных операций (например, с унарным минусом). Операнды унарных операций, тип которых по диапазону меньше типа int (т.е. sbyte -, byte -, short - и ushort -значения), “подтягиваются” к типу int . To же происходит с операндом типа char . Более того, при выполнении операции отрицания uint -значения результат приобретает тип long Приведение типов в выражениях Операцию приведения типов можно применить не ко всему выражению, а к конкретной его части. Это позволяет более тонко управлять преобразованием типов при вычислении выражения. Рассмотрим, например, следующую программу. Она отображает значения квадратных корней из чисел от 1 до 10. Она также выводит по отдельности целую и дробную части каждого результата. Для этого в программе используется операция приведения типов, которая позволяет преобразовать результат вызова метода Math.Sqrt() в значение типа int // Приведение типов в выражениях. using System; class CastExpr { public 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( Глава 3. Типы данных, литералы и переменные 79 "Дробная часть числа: {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,16227766016938 Целая часть числа: 3 Дробная часть числа: 0,16227766016838 Как видно из результатов выполнения программы, приведение значения, возвращаемого методом Math.Sqrt() , к типу int , позволяет получить целую часть значения. А его дробную часть мы получаем в результате вычисления следующего выражения (если из вещественного числа вычесть его целую часть, то результат даст дробную часть исходного числа): Math.Sqrt(n) - (int) Math.Sqrt(n) Результат этого выражения имеет тип double . Здесь к типу int приводится только результат второго вызова метода Math.Sqrt() |