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

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

В программировании нередко значения переменных одного типа присваиваются переменным другого типа. Например, в приведенном ниже фрагменте кода целое значение типа int присваивается переменной с плавающей точкой типа float:

Int i; float f; i = 10; f = i; // присвоить целое значение переменной типа float

Если в одной операции присваивания смешиваются совместимые типы данных, то значение в правой части оператора присваивания автоматически преобразуется в тип, указанный в левой его части. Поэтому в приведенном выше фрагменте кода значение переменной i сначала преобразуется в тип float, а затем присваивается переменной f. Но вследствие строгого контроля типов далеко не все типы данных в C# оказываются полностью совместимыми, а следовательно, не все преобразования типов разрешены в неявном виде. Например, типы bool и int несовместимы. Правда, преобразование несовместимых типов все-таки может быть осуществлено путем приведения . Приведение типов, по существу, означает явное их преобразование.

Автоматическое преобразование типов

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

  • оба типа совместимы
  • диапазон представления чисел целевого типа шире, чем у исходного типа

Если оба эти условия удовлетворяются, то происходит расширяющее преобразование . Например, тип int достаточно крупный, чтобы вмещать в себя все действительные значения типа byte, а кроме того, оба типа, int и byte, являются совместимыми целочисленными типами, и поэтому для них вполне возможно неявное преобразование.

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

Using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 { class Program { static void Main(string args) { short num1, num2; num1 = 10; num2 = 15; Console.WriteLine("{0} + {1} = {2}",num1,num2,Sum(num1,num2)); Console.ReadLine(); } static int Sum(int x, int y) { return x + y; } } }

Обратите внимание на то, что метод Sum() ожидает поступления двух параметров типа int. Тем не менее, в методе Main() ему на самом деле передаются две переменных типа short. Хотя это может показаться несоответствием типов, программа будет компилироваться и выполняться без ошибок и возвращать в результате, как и ожидалось, значение 25.

Причина, по которой компилятор будет считать данный код синтаксически корректным, связана с тем, что потеря данных здесь невозможна. Поскольку максимальное значение (32767), которое может содержать тип short, вполне вписывается в рамки диапазона типа int (максимальное значение которого составляет 2147483647), компилятор будет неявным образом расширять каждую переменную типа short до типа int. Формально термин "расширение" применяется для обозначения неявного восходящего приведения (upward cast), которое не приводит к потере данных.

Приведение несовместимых типов

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

(целевой_тип) выражение

Здесь целевой_тип обозначает тот тип, в который желательно преобразовать указанное выражение.

Если приведение типов приводит к сужающему преобразованию , то часть информации может быть потеряна. Например, в результате приведения типа long к типу int часть информации потеряется, если значение типа long окажется больше диапазона представления чисел для типа int, поскольку старшие разряды этого числового значения отбрасываются. Когда же значение с плавающей точкой приводится к целочисленному, то в результате усечения теряется дробная часть этого числового значения. Так, если присвоить значение 1,23 целочисленной переменной, то в результате в ней останется лишь целая часть исходного числа (1), а дробная его часть (0,23) будет потеряна. Давайте рассмотрим пример:

Using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 { class Program { static void Main(string args) { int i1 = 455, i2 = 84500; decimal dec = 7.98845m; // Приводим два числа типа int // к типу short Console.WriteLine((short)i1); Console.WriteLine((short)i2); // Приводим число типа decimal // к типу int Console.WriteLine((int)dec); Console.ReadLine(); } } }

Результатом работы данной программы будет:

Обратите внимание, что переменная i1 корректно преобразовалась в тип short, т.к. ее значение входит в диапазон этого типа данных. Преобразование переменной dec в тип int вернуло целую часть этого числа. Преобразование переменной i2 вернуло значение переполнения 18964 (т.е. 84500 - 2*32768).

Перехват сужающих преобразований данных

В предыдущем примере приведение переменной i2 к типу short не является приемлемым, т.к. возникает потеря данных . Для создания приложений, в которых потеря данных должна быть недопустимой, в C# предлагаются такие ключевые слова, как checked и unchecked , которые позволяют гарантировать, что потеря данных не окажется незамеченной.

По умолчанию, в случае, когда не предпринимается никаких соответствующих исправительных мер, условия переполнения (overflow) и потери значимости (underflow) происходят без выдачи ошибки. Обрабатывать условия переполнения и потери значимости в приложении можно двумя способами. Это можно делать вручную, полагаясь на свои знания и навыки в области программирования.

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

К счастью, в C# предусмотрено ключевое слово checked. Если оператор (или блок операторов) заключен в контекст checked, компилятор C# генерирует дополнительные CIL-инструкции, обеспечивающие проверку на предмет условий переполнения, которые могут возникать в результате сложения, умножения, вычитания или деления двух числовых типов данных.

В случае возникновения условия переполнения во время выполнения будет генерироваться исключение System.OverflowException . Давайте рассмотрим пример, в котором будем передавать в консоль значение исключения:

Using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 { class Program { static void Main(string args) { byte var1 = 250; byte var2 = 150; try { byte sum = checked((byte)(var1+var2)); Console.WriteLine("Сумма: {0}", sum); } catch (OverflowException ex) { Console.WriteLine(ex.Message); Console.ReadLine(); } } } }

Результат работы данной программы:

Настройка проверки на предмет возникновения условий переполнения в масштабах проекта

Если создается приложение, в котором переполнение никогда не должно проходить незаметно, может выясниться, что обрамлять ключевым словом checked приходится раздражающе много строк кода. На такой случай в качестве альтернативного варианта в компиляторе C# поддерживается флаг /checked . При активизации этого флага проверки на предмет возможного переполнения будут автоматически подвергаться все имеющиеся в коде арифметические операции, без применения для каждой из них ключевого слова checked. Обнаружение переполнения точно так же приводит к генерации соответствующего исключения во время выполнения.

Для активизации этого флага в Visual Studio 2010 необходимо открыть страницу свойств проекта, перейти на вкладку Build (Построение), щелкнуть на кнопке Advanced (Дополнительно) и в открывшемся диалоговом окне отметить флажок Check for arithmetic overflow/underflow (Проверять арифметические переполнения и потери точности) :

Важно отметить, что в C# предусмотрено ключевое слово unchecked , которое позволяет отключить выдачу связанного с переполнением исключения в отдельных случаях.

Итак, чтобы подвести итог по использованию в C# ключевых слов checked и unchecked, следует отметить, что по умолчанию арифметическое переполнение в исполняющей среде.NET игнорируется. Если необходимо обработать отдельные операторы, то должно использоваться ключевое слово checked, а если нужно перехватывать все связанные с переполнением ошибки в приложении, то понадобится активизировать флаг /checked. Что касается ключевого слова unchecked, то его можно применять при наличии блока кода, в котором переполнение является допустимым (и, следовательно, не должно приводить к генерации исключения во время выполнения).

Неявное преобразование типов данных выполняет компилятор С++, ну а явное преобразование данных выполняет сам программист. О преобразовании типов данных скажу следующее: «Результат любого вычисления будет преобразовываться к наиболее точному типу данных, из тех типов данных, которые участвуют в вычислении». Для наглядного примера представлю таблицу с преобразованиями типов данных. В таблице рассмотрим операцию деления. В качестве целочисленного типа данных возьмем int , ну и вещественный тип данных у нас будет float .

Таблица 1 — Явное и неявное преобразование типов данных в С++
x y Результат деления Пример
делимое делитель частное x = 15 y = 2
int int int 15/2=7
int float float 15/2=7.5
float int float 15/2=7.5

Из таблицы видно, что меняя переменные различных местами, результат остается тот же (в нашем случае это делимое и делитель). О неявном преобразовании типов данных все. Что же касается явного преобразования, то оно необходимо для того чтобы выполнять некоторые манипуляции, тем самым меняя результат вычисления. Самый простой способ явного преобразования типов данных, пример: допустим нам необходимо разделить такие числа 15 и 2, делим! 15/2=7 . Результат тот же, что и в таблице. Но если сделать незначительные преобразования, например: 15.0/2=7.5 при таком делении число 15 является вещественным, значит и результат будет вещественный. Само число 15 с точки зрения математики не изменилось, ведь 15=15.0. Этот же прием можно было применить к двойке, результат был бы тем же, а можно было сразу к двум числам, но зачем,если хватает одного из двух.

Еще один способ явного преобразования типов данных:

Float(15) / 2 // результат равен 7.5, число 15 преобразуется в вещественный тип данных float. double(15) / 2 // результат равен 7.5 – тоже самое!!!

В С++ также предусмотрена унарная операция приведения типа:

Static_cast(/*переменная или число*/)

пример: static_cast(15)/2 результат равен 7.5
Пример с переменной:

Int ret=15; static_cast(ret)/2 //результат равен 7.5

В случае с переменной надо понимать, что в строке 2 переменная ret не преобразуется в тип данных float , а всего лишь на всего создается временная копия переменной ret с типом данных float . Рассмотрим на практике все способы явного и неявного преобразования типов данных.

// pryeobrazovanie.cpp: определяет точку входа для консольного приложения. #include "stdafx.h" #include #include using namespace std; int _tmain(int argc, _TCHAR* argv) { int int_value15 = 15, int_value2 = 2; // объявляем две переменные типа int float float_value15 = 15, float_value2 = 2; // объявляем две переменные типа float cout << fixed << setprecision(2) // определяем, при выводе чисел с плавающей точкой, два знака после запятой << "15 / 2 = " << int_value15 / int_value2 << endl //неявное преобразование типов данных << "15 / 2 = " << int_value15 / float_value2 << endl //неявное преобразование типов данных << "15 / 2 = " << float_value15 / int_value2 << endl //неявное преобразование типов данных << "15 / 2 = " << float_value15 / float_value2 << endl; //неявное преобразование типов данных cout << "15.0 / 2 = " << 15.0 / 2 << endl // явное преобразование типа данных, число 15.0 - число с плавающей точкой << "15 / 2.0 = " << 15 / 2.0 << endl; // явное преобразование типа данных, число 2.0 - число с плавающей точкой cout << "float(int_value15) / int_value2 = " << float(int_value15) / int_value2 << endl // явное преобразование типа данных << "15 / double(2) = " << 15 / double(2) << endl; // используя приводимый тип как функцию cout << "static_cast(15) / 2 = " << static_cast(15) / 2 << endl // унарная операция приведения типа << "static_cast(15) = " << static_cast(15) << endl // можно печатать различные символы из таблицы ASCII, << "static_cast(20) = " << static_cast(20) << endl; // в скобочках прописываем код символа, который находим в таблице ASCII system("pause"); return 0; }

В строке 5 подключена , эта библиотека нужна для использования различных манипуляторов, в нашем случае — fixed setprecision() . В строке 10 специально создал две переменные типа int , аналогично создал две переменный типа float в строке 11 , эти переменные нужны будут для преобразования их значений в другие типы данных. В строке 12 после оператора cout и операции сдвига в поток вывода << стоят два манипулятора fixed и setprecision() . Манипулятор fixed — это не параметризированный манипулятор, так как никаких параметров не принимает, пишется без круглых скобок. Данный манипулятор применяется в паре с параметризированным манипулятором setprecision() и выполняет фиксированное отображение разрядов после запятой. А манипулятор setprecision() отображает количество знаков после запятой, причём то, которое указано в скобочках. В строках 13, 14, 15, 16 показаны примеры неявного преобразования типов данных, эти примеры взяты из таблицы 1 . В строках 17, 18 показан один из способов явного преобразования данных. Суть такого способа заключается в том, что нужно дописать запятую и нуль к целому числу. В строках 19, 20 явное преобразование выполняется посредством использования приводимых типов как функций, внутри скобочек которых, нужно указать значение или переменную, которую необходимо преобразовать. В строках 21, 22, 23 выполняется явное преобразование типов данных с помощью унарной операции преобразования данных. В круглых скобочках указывается, переменная или значение, которое нужно преобразовать, а в обрамлении знаков < > тип данных, к которому нужно преобразовать. Пример работы программы показан ниже (см. Рисунок 1).

Рисунок 1 — Явное и неявное преобразование типов данных C++

В строках 22, 23 выполняется унарная операция преобразования данных, причём преобразуются числа 15 и 20 к char . Этот тип данных пока вам не известен, но запомните, что char — тип данных для хранения . Так вот, из рисунка 1 видно, что в конце появились символы. Эти символы получились путём преобразования чисел в char . Числами являлись коды из . Таким образом, если необходимо вывести какой-нибудь символ из таблицы ASCII, это можно сделать как показано в строках 22, 23 , при этом подставив только нужный код.

Перейдем к примерам. КлассTesting , содержащий примеры, представляет собой набор данных разного типа, над которыми выполняются операции, иллюстрирующие преобразования типов. Вот описание класса

Testing: using System;
namespace TypesProject
{
public class Testing {
/// < summary >
/// набор скалярных полей разного типа.
///
private byte b = 255;
private int x = 11 ;
private uint ux = 1111 ;
private float y = 5.5f;
private double dy = 5.55;
private string s = "Hello!";
private string si = "25";
private object obj = new Object();
// Далее идут методы класса, приводимые по ходу
// описания примеров
}
}

В набор данных класса входят скалярные данные арифметического типа, относящиеся к значимым типам, переменные строкового типа и типаobject , принадлежащие ссылочным типам. Рассмотрим закрытый(private ) метод этого класса - процедуруWholsWho с формальным аргументом классаObject . Процедура выводит на консоль переданное ей имя аргумента, его тип и значение.

Вот ее текст:

/// < summary >
/// Метод выводит на консоль информацию о типе и
/// значении фактического аргумента. Формальный
/// аргумент имеет тип object . Фактический аргумент
/// может иметь любой тип, поскольку всегда
/// допустимо неявное преобразование в тип object .
///
/// Имя второго аргумента
/// Допустим аргумент любого типа
private void WhoIsWho(string name, object any) {
Console.WriteLine("type {0} is {1} , value is {2}",
name, any.GetType(), any.ToString());
}

Вот открытый(public ) метод классаTesting , в котором многократно вызывается методWholsWho с аргументами разного типа:

/// < summary >
/// получаем информацию о типе и значении
/// переданного аргумента - переменной или выражения
/// summary >
public void WhoTest() {
WholsWho("x", x);
WholsWho("ux", ux);
WhoIsWho("y", y);
WhoIsWho("dy", dy);
WhoIsWho("s", s);
WhoIsWho("11 + 5.55 + 5.5f", 11 + 5.55 + 5.5f);
obj = 11 + 5.55 + 5.5f;
WhoIsWho (" obj ", obj );
}

Отметим, что сущностьany - формальный аргумент классаObject , который при каждом вызове динамически изменяет тип, связываясь с объектом, заданным фактическим аргументом. Поэтому тип аргумента, выдаваемый на консоль, - это тип фактического аргумента. Отметим также, что наследуемый от классаObject методGetType возвращает тип FCL, то есть тот тип, на который отражается тип языка и с которым реально идет работа при выполнении модуля. В большинстве вызовов фактическим аргументом является переменная - соответствующее свойство классаTesting , но в одном случае передается обычное арифметическое выражение, автоматически преобразуемое в объект. Аналогичная ситуация имеет место и при выполнении присваивания в рассматриваемой процедуре.

На рис. 11 показаны результаты вывода на консоль, полученные при вызове методаWhoTest в приведенной выше процедуреMain классаClassi .

Рисунок 11. Вывод на печать результатов теста WhoTest

Где, как и когда выполняются преобразования типов?

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

Если при вычислении выражения операнды операции имеют разные типы, то возникает необходимость приведения их к одному типу. Такая необходимость возникает и тогда, когда операнды имеют один тип, но он несогласован с типом операции. Например, при выполнении сложения операнды типаbyte должны быть приведены к типуint , поскольку сложение не определено над байтами. При выполнении присваиванияx = e тип источникаe и тип целиx должны быть согласованы. Аналогично, при вызове метода также должны быть согласованы типы источника и цели - фактического и формального аргументов.

Преобразования ссылочных типов

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

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

В C# такие преобразования делятся на неявные и явные. К неявным относятся те преобразования, результат выполнения которых всегда успешен и не приводит к потере точности данных. Неявные преобразования выполняются автоматически. Для арифметических данных это означает, что в неявных преобразованиях диапазон типа назначения содержит в себе диапазон исходного типа. Например, преобразование из типаbyte в типint относится к неявным, поскольку диапазон типаbyte является подмножеством диапазонаint . Это преобразование всегда успешно и не может приводить к потере точности.

К явным относятся разрешенные преобразования, успех выполнения которых не гарантируется или может приводить к потере точности. Такие потенциально опасные преобразования должны быть явно заданы программистом. Преобразование из типаint в типbyte относится к явным, поскольку оно небезопасно и может приводить к потере значащих цифр. Отметим, не для всех типов существуют явные преобразования. В этом случае требуются другие механизмы преобразования типов, которые будут рассмотрены позже.

Преобразования внутри арифметического типа

Арифметический тип, как показано в таблице типов данных, распадается на 11 подтипов. На рис. 12 показана схема преобразований внутри арифметического типа.

Рисунок 12. Иерархия преобразований внутри арифметического типа

Диаграмма, приведенная на рисунке, позволяет ответить на ряд важных вопросов, связанных с существованием преобразований между типами. Если на диаграмме задан путь (стрелками) от типа А к типу В, то это означает существование неявного преобразования из типа А в тип В. Все остальные преобразования между подтипами арифметического типа существуют, но являются явными. Отметим, что циклов на диаграмме нет, все стрелки односторонние, так что преобразование, обратное к неявному, всегда должно быть задано явным образом.

Путь, указанный на диаграмме, может быть достаточно длинным, но это вовсе не означает, что выполняется вся последовательность преобразований на данном пути. Наличие пути говорит лишь о существовании неявного преобразования, а само преобразование выполняется только один раз, - из типа источника А в тип назначения В.

Иногда возникает ситуация, при которой для одного типа источника может одновременно существовать несколько типов назначений и необходимо осуществить выбор цели - типа назначения. Такие проблемы выбора возникают, например, при работе с перегруженными методами в классах.

Правило выбора реализации при вызове метода таково: выбирается та реализация, для которой путь преобразований, заданный на диаграмме, короче. Если есть точное соответствие параметров по типу (путь длины 0), то, естественно, именно эта реализация и будет выбрана.

Рассмотрим еще один тестовый пример. В классTesting включена группа перегруженных методовOLoad с одним и двумя аргументами. Вот эти методы:

/// < summary >
/// Группа перегруженных методов OLoad
/// с одним или двумя аргументами арифметического типа.
/// Если фактический аргумент один, то будет вызван один из
/// методов, наиболее близко подходящий по типу аргумента.
/// При вызове метода с двумя аргументами возможен
/// конфликт выбора подходящего метода, приводящий
/// к ошибке периода компиляции.
///
private void OLoad(float par) {
Console.WriteLine("float value {0}", par);
}
/// < summary >
/// Перегруженный метод OLoad с одним параметром типа long
///
///
private void OLoad(long par) {
Console.WriteLine("long value {0}", par);
}
/// < summary >
/// Перегруженный метод OnLoad с одним параметром типа ulong
///
///
private void OLoad(ulong par) {
Console.WriteLine("ulong value {0}", par);
}
/// < summary >
/// Перегруженный метод OLoad с одним параметром типа double
///
///
private void OnLoad(double par) {
Console.WriteLine("double value {0}", par);
}
/// < summary >
/// Перегруженный метод OLoad с двумя параметрами типа long и long
///
///
///
private void OLoad(long par1, long par2) {
Console.WriteLine("long par1 {0}, long par2 {1}", par1, par2);
}
/// < summary >
/// Перегруженный метод OLoad с двумя параметрами типа
/// double и double
///
///
///
private void OLoad(double par1, double par2) {
Console.WriteLine("double par1 {0}, double par2 {1}", par1, par2);
}
/// < summary >
/// Перегруженный метод OLoad с двумя параметрами типа
/// int и float
///
///
///
private void OLoad(int par1, float par2) {
Console.WriteLine("int par1 {0}, float par2 {1}", par1, par2);
}

Все эти методы устроены достаточно просто. Они сообщают информацию о типе и значении переданных аргументов.

Вот тестирующая процедура, вызывающая методOLoad с разным числом и типами аргументов:

/// < summary >
/// Вызов перегруженного метода OLoad . В зависимости от
/// типа и числа аргументов вызывается один из методов группы.
///
public void OLoadTest() {
OLoad(x);
OLoad(ux);
OLoad(y);
OLoad(dy);
// OLoad(x,ux);
// conflict: (int, float) и (long,long)
OLoad(x, (float) ux);
OLoad(y, dy);
OLoad (x , dy );
}

Отметим, что один из вызовов закомментирован, так как он приводит к конфликту на этапе трансляции. Для устранения конфликта при вызове метода пришлось задать явное преобразование аргумента, что показано в строке, следующей за строкой-комментарием. Результат работы тестаOLoadTest представлен на рис. 13.

Рисунок 13. Вывод на печать результатов теста OLoadTest

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

Как уже говорилось, явные преобразования могут быть опасными из-за потери точности. Поэтому они выполняются по указанию программиста, - на нем лежит вся ответственность за результаты.

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

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

/// < summary >
/// Демонстрация преобразования в строку данных различного типа.
///
public void ToStringTest()
{
s = " Владимир Петров ";
s1 = " Возраст : ";
ux = 27;
s = s + s1 + ux.ToString();
s1 = " Зарплата : ";
dy = 2700.50;
s = s + s1 + dy;
WhoIsWho (" s ", s );
}

Результат работы этой процедуры показан на рис. 14.

Рисунок 14. Вывод на печать результатов теста ToStringTest

Преобразования из строкового типа в другие типы, например, в арифметический, должны выполняться явно. Но явных преобразований между арифметикой и строками не существуют. Необходимы другие механизмы, и они в C# имеются. Для этой цели можно использовать соответствующие методы классаConvert библиотеки FCL, встроенного в пространство имен System . Приведем соответствующий пример:

/// < summary >
/// Демонстрация преобразования строки в данные различного типа.
///
public void FromStringTest() {
s = " Введите возраст ";
Console.WriteLine(s);
s1 = Console.ReadLine();
ux = Convert.ToUInt32(s1);
WhoIsWho ("Возраст: ", ux );
s = "Введите зарплату ";
Console.WriteLine(s);
s1 = Console.ReadLine();
dy = Convert.ToDouble(s1);
WhoIsWho(" Зарплата : ", dy);
}

Этот пример демонстрирует ввод с консоли данных разных типов. Данные, читаемые с консоли методомReadLine илиRead , всегда представляют собой строку, которую затем необходимо преобразовать в нужный тип. Для этого вызываются соответствующие методы классаConvert . Естественно, для успеха преобразования строка должна содержать значение в формате, допускающем подобное преобразование. Отметим, например, что при записи значения числа для выделения дробной части должна использоваться запятая, а не точка; в противном случае возникнет ошибка периода выполнения.

На рис. 15 показаны результаты вывода и ввода данных с консоли при работе этой процедуры.

Рисунок 15. Вывод на печать результатов теста FromStringTest

В программировании нередко значения переменных одного типа присваиваются переменным другого типа. Например, в приведенном ниже фрагменте кода целое значение типа int присваивается переменной с плавающей точкой типа float :
int i; float f; i = 10; f = i;

Если в одной операции присваивания смешиваются совместимые типы данных, то значение в правой части оператора присваивания автоматически преобразуется в тип, указанный в левой его части. Поэтому в приведенном выше фрагменте кода значение переменной i сначала преобразуется в тип float , а затем присваивается переменной f .

Зададимся вопросом, всегда ли возможно преобразование типов? Когда будут возникать сообщения об ошибках, как это повлияет на надежность разрабатываемых программ?

Вследствие строгого контроля типов далеко не все типы данных в C# оказываются полностью совместимыми, и, следовательно, не все преобразования типов разрешены в неявном виде.

Например, типы bool и int несовместимы. Правда, преобразование несовместимых типов все-таки может быть осуществлено путем приведения . Приведение типов , по существу, означает явное их преобразование.

Автоматическое преобразование типов

Когда данные одного типа присваиваются переменной другого типа, неявное преобразование типов происходит автоматически при следующих условиях:
1) оба типа совместимы;
2) диапазон представления чисел целевого типа шире, чем у исходного типа.
Если оба эти условия удовлетворяются, то происходит расширяющее преобразование .
Например, тип int достаточно крупный, чтобы вмещать в себя все действительные значения типа byte , а кроме того, оба типа, int и byte , являются совместимыми целочисленными типами, и поэтому для них вполне возможно неявное преобразование.
Числовые типы, как целочисленные, так и с плавающей точкой, вполне совместимы друг с другом для выполнения расширяющих преобразований.

Рассмотрим пример:

using System; namespace ConsoleApplication1 { classProgram { static void Main(string args) { short num1, num2; num1 =10; num2 =15; Console.WriteLine("{0} + {1} = {2}",num1,num2,Sum(num1,num2)); Console.ReadLine(); } staticintSum(int x,int y) { return x + y; } } }

Обратите внимание на то, что метод Sum() ожидает поступления двух параметров типа int. Тем не менее, в методе Main() ему на самом деле передаются две переменных типа short. Хотя это может показаться несоответствием типов, программа будет компилироваться и выполняться без ошибок и возвращать в результате, как и ожидалось, значение 25.
Причина, по которой компилятор будет считать данный код синтаксически корректным, связана с тем, что потеря данных здесь невозможна.
Поскольку максимальное значение (32767), которое может содержать тип short, вполне вписывается в рамки диапазона типа int (максимальное значение которого составляет 2147483647), компилятор будет неявным образом расширять каждую переменную типа short до типа int.
Формально термин «расширение» применяется для обозначения неявного восходящего приведения (upward cast ), которое не приводит к потере данных.

Приведение несовместимых типов

Несмотря на всю полезность неявных преобразований типов, они неспособны удовлетворить все потребности в программировании, поскольку допускают лишь расширяющие преобразования совместимых типов. А во всех остальных случаях приходится обращаться к приведению типов. Приведение - это команда компилятору преобразовать результат вычисления выражения в указанный тип. А для этого требуется явное преобразование типов.

Ниже приведена общая форма приведения типов:
(целевой_тип) выражение
Здесь целевой_тип обозначает тот тип, в который желательно преобразовать указанное выражение.

Если приведение типов приводит к сужающему преобразованию , то часть информации может быть потеряна. Например, в результате приведения типа long к типу int часть информации потеряется, если значение типа long окажется больше диапазона представления чисел для типа int, поскольку старшие разряды этого числового значения отбрасываются.
Когда же значение с плавающей точкой приводится к целочисленному типу, то в результате усечения теряется дробная часть этого числового значения. Так, если присвоить значение 1,23 целочисленной переменной, то в результате в ней останется лишь целая часть исходного числа (1), а дробная его часть (0,23) будет потеряна.

Давайте рассмотрим пример:

using System; namespace ConsoleApplication1 { classProgram { static void Main(string args) { int i1 =455,i2 =84500; decimal dec =7.98845m; // Приводим два числа типа int к типу short Console.WriteLine((short) i1); Console.WriteLine((short) i2); // Приводим число типа decimal к типу int Console.WriteLine((int) dec); Console.ReadLine(); } } }

Результатом работы данной программы будет:
455
18964
7

Обратите внимание, что переменная i1 корректно преобразовалась в тип short, т.к. ее значение входит в диапазон этого типа данных. Преобразование переменной dec в тип int вернуло целую часть этого числа. Преобразование переменной i2 вернуло значение переполнения 18964 (т.е. 84500 — 2*32768).

Роль класса System.Convert

В завершении темы преобразования типов данных стоит отметить, что в пространстве имен System имеется класс Convert , который тоже может применяться для расширения и сужения данных:
byte sum = Convert.ToByte(var1 + var2);

Одно из преимуществ подхода с применением класса System.Convert связано с тем, что он позволяет выполнять преобразования между типами данных нейтральным к языку образом.

Однако, поскольку в C# есть операция явного преобразования, использование класса Convert для преобразования типов данных обычно является делом вкуса.

Другое полезное назначение методов класса Convert состоит в преобразовании строковой переменной в переменную числового типа. Я его использую достаточно часто! Ведь в консольном приложении возможен только ввод строк, и поскольку оператор Console.ReadLine() возвращает строку, то далее ее можно преобразовать в число, используя соответствующий метод, например:
int k = Convert.ToInt32(Console.ReadLine());
При невозможности преобразования строки в число возникает исключительная ситуация (далее исключение – exception ), которое может быть также обработано.

Перейдем к изучению операторов языка С#, и начнем мы с .