Введение в, программирование, язык программирования, основы, операционная система
учебники, программирование, основы, введение в,

 

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

Где, как и когда выполняются преобразования типов?
Необходимость в преобразовании типов возникает в выражениях, присваиваниях, замене формальных аргументов метода фактическими.
Если при вычислении выражения операнды операции имеют разные типы, то возникает необходимость приведения их к одному типу. Такая необходимость возникает и тогда, когда операнды имеют один тип, но он несогласован с типом операции. Например, при выполнении сложения операнды типа byte должны быть приведены к типу int, поскольку сложение не определено над байтами. При выполнении присваивания x=e тип источника e и тип цели x должны быть согласованы. Аналогично, при вызове метода также должны быть согласованы типы источника и цели - фактического и формального аргументов.
Преобразования ссылочных типов
Поскольку операции над ссылочными типами не определены (исключением являются строки, но операции над ними, в том числе и присваивание, выполняются как над значимыми типами), то необходимость в них возникает только при присваиваниях и вызовах методов. Семантика таких преобразований рассмотрена в предыдущей лекции 3, где подробно обсуждалась семантика присваивания и совпадающая с ней семантика замены формальных аргументов фактическими. Там же много говорилось о преобразованиях между ссылочными и значимыми типами, выполняемых при этом операциях упаковки значений в объекты и обратной их распаковки.
Коротко повторю основные положения, связанные с преобразованиями ссылочных типов. При присваиваниях (замене аргументов) тип источника должен быть согласован с типом цели, то есть объект, связанный с источником, должен принадлежать классу, являющемуся потомком класса цели. В случае согласования типов, ссылочная переменная цели связывается с объектом источника и ее тип динамически изменяется, становясь типом источника. Это преобразование выполняется автоматически и неявно, не требуя от программиста никаких дополнительных указаний. Если же тип цели является потомком типа источника, то неявное преобразование отсутствует, даже если объект, связанный с источником, принадлежит типу цели. Явное преобразование, заданное программистом, позволяет справиться с этим случаем. Ответственность за корректность явных преобразований лежит на программисте, так что может возникнуть ошибка на этапе выполнения, если связываемый объект реально не является объектом класса цели. За примерами следует обратиться к лекции 3, еще раз обратив внимание на присваивания объектов классов Parent и Child.
Преобразования типов в выражениях
Продолжая тему преобразований типов, рассмотрим привычные для программистов преобразования между значимыми типами и, прежде всего, преобразования внутри арифметического типа.
В C# такие преобразования делятся на неявные и явные. К неявным относятся те преобразования, результат выполнения которых всегда успешен и не приводит к потере точности данных. Неявные преобразования выполняются автоматически. Для арифметических данных это означает, что в неявных преобразованиях диапазон типа назначения содержит в себе диапазон исходного типа. Например, преобразование из типа byte в тип int относится к неявным, поскольку диапазон типа byte является подмножеством диапазона int. Это преобразование всегда успешно и не может приводить к потере точности. Заметьте, преобразования из целочисленных типов к типам с плавающей точкой относятся к неявным. Хотя здесь и может происходить некоторое искажение значения, но точность представления значения сохраняется, например, при преобразовании из long в double порядок значения остается неизменным.
К явным относятся разрешенные преобразования, успех выполнения которых не гарантируется или может приводить к потере точности. Такие потенциально опасные преобразования должны быть явно заданы программистом. Преобразование из типа int в тип byte относится к явным, поскольку оно небезопасно и может приводить к потере значащих цифр. Заметьте, не для всех типов существуют явные преобразования. В этом случае требуются другие механизмы преобразования типов, которые будут рассмотрены позже.
Преобразования внутри арифметического типа
Арифметический тип, как показано в таблице 3.1, распадается на 11 подтипов. На рис. 4.1 показана схема преобразований внутри арифметического типа.
Диаграмма, приведенная на рисунке, позволяет ответить на ряд важных вопросов, связанных с существованием преобразований между типами. Если на диаграмме задан путь (стрелками) от типа А к типу В, то это означает существование неявного преобразования из типа А в тип В. Все остальные преобразования между подтипами арифметического типа существуют, но являются явными. Заметьте, что циклов на диаграмме нет, все стрелки односторонние, так что преобразование, обратное к неявному, всегда должно быть задано явным образом.
Путь, указанный на диаграмме, может быть достаточно длинным, но это вовсе не означает, что выполняется вся последовательность преобразований на данном пути. Наличие пути говорит лишь о существовании неявного преобразования, а само преобразование выполняется только один раз, - из типа источника А в тип назначения В.
Иногда возникает ситуация, при которой для одного типа источника может одновременно существовать несколько типов назначений и необходимо осуществить выбор цели - типа назначения. Такие проблемы выбора возникают, например, при работе с перегруженными методами в классах.


Понятие перегрузки методов и операций подробно будет рассмотрено в последующих лекциях (см. лекцию 8).

Диаграмма, приведенная на рис. 4.1, и в этом случае помогает понять, как делается выбор. Пусть существует две или более реализации перегруженного метода, отличающиеся типом формального аргумента. Тогда при вызове этого метода с аргументом типа T может возникнуть проблема, какую реализацию выбрать, поскольку для нескольких реализаций может быть допустимым преобразование аргумента типа T в тип, заданный формальным аргументом данной реализации метода. Правило выбора реализации при вызове метода таково: выбирается та реализация, для которой путь преобразований, заданный на диаграмме, короче. Если есть точное соответствие параметров по типу (путь длины 0), то, естественно, именно эта реализация и будет выбрана.
Давайте рассмотрим еще один тестовый пример. В класс Testing включена группа перегруженных методов OLoad с одним и двумя аргументами. Вот эти методы:
/// <summary>
/// Группа перегруженных методов OLoad
/// с одним или двумя аргументами арифметического типа.
/// Если фактический аргумент один, то будет вызван один из
/// методов, наиболее близко подходящий по типу аргумента.
/// При вызове метода с двумя аргументами возможен
/// конфликт выбора подходящего метода, приводящий
/// к ошибке периода компиляции.
/// </summary>
void OLoad(float par)
{
Console.WriteLine("float value {0}", par);
}
/// <summary>
/// Перегруженный метод OLoad с одним параметром типа long
/// </summary>
/// <param name="par"></param>
void OLoad(long par)
{
Console.WriteLine("long value {0}", par);
}
/// <summary>
/// Перегруженный метод OLoad с одним параметром типа ulong
/// </summary>
/// <param name="par"></param>
void OLoad(ulong par)
{
Console.WriteLine("ulong value {0}", par);
}
/// <summary>
/// Перегруженный метод OLoad с одним параметром типа double
/// </summary>
/// <param name="par"></param>
void OLoad(double par)
{
Console.WriteLine("double value {0}", par);
}
/// <summary>
/// Перегруженный метод OLoad с двумя параметрами типа long и long
/// </summary>
/// <param name="par1"></param>
/// <param name="par2"></param>
void OLoad(long par1, long par2)
{
Console.WriteLine("long par1 {0}, long par2 {1}", par1, par2);
}
/// <summary>
/// Перегруженный метод OLoad с двумя параметрами типа
/// double и double
/// </summary>
/// <param name="par1"></param>
/// <param name="par2"></param>
void OLoad(double par1, double par2)
{
Console.WriteLine("double par1 {0}, double par2 {1}",par1, par2);
}
/// <summary>
/// Перегруженный метод OLoad с двумя параметрами типа
/// int и float
/// </summary>
/// <param name="par1"></param>
/// <param name="par2"></param>
void OLoad(int par1, float par2)
{
Console.WriteLine("int par1 {0}, float par2 {1}",par1, par2);
}
Все эти методы устроены достаточно просто. Они сообщают информацию о типе и значении переданных аргументов. Вот тестирующая процедура, вызывающая метод OLoad с разным числом и типами аргументов:
/// <summary>
/// Вызов перегруженного метода OLoad. В зависимости от
/// типа и числа аргументов вызывается один из методов группы.
/// </summary>
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);
}
Заметьте, один из вызовов закомментирован, так как он приводит к конфликту на этапе трансляции. Для устранения конфликта при вызове метода пришлось задать явное преобразование аргумента, что показано в строке, следующей за строкой-комментарием.
Прежде чем посмотреть на результаты работы тестирующей процедуры, попробуйте понять, какой из перегруженных методов вызывается для каждого из вызовов. В случае каких-либо сомнений используйте схему, приведенную на 4.1.
Приведу все-таки некоторые комментарии. При первом вызове метода тип источника - int, а тип аргумента у четырех возможных реализаций соответственно float, long, ulong, double. Явного соответствия нет, поэтому нужно искать самый короткий путь на схеме. Так как не существует неявного преобразования из типа int в тип ulong (на диаграмме нет пути), то остаются возможными три реализации. Но путь из int в long короче, чем остальные пути, поэтому будет выбрана long-реализация метода.
Следующий вызов демонстрирует еще одну возможную ситуацию. Для типа источника uint существуют две возможные реализации, и пути преобразований для них имеют одинаковую длину. В этом случае выбирается та реализация, для которой на диаграмме путь показан сплошной, а не пунктирной стрелкой, потому будет выбрана реализация с параметром long.
Рассмотрим еще ситуацию, приводящую к конфликту. Первый аргумент в соответствии с правилами требует вызова одной реализации, а второй аргумент будет настаивать на вызове другой реализации. Возникнет коллизия, не разрешимая правилами C# и приводящая к ошибке периода компиляции. Коллизию требуется устранить, например, как это сделано в примере. Обратите внимание - обе реализации допустимы, и существуй даже только одна из них, ошибки бы не возникало.

http://localhost:3232/img/empty.gifЯвные преобразования

Как уже говорилось, явные преобразования могут быть опасными из-за потери точности. Поэтому они выполняются по указанию программиста, - на нем лежит вся ответственность за результаты.
Преобразования строкового типа
Важным классом преобразований являются преобразования в строковый тип и наоборот. Преобразования в строковый тип всегда определены, поскольку, напомню, все типы являются потомками базового класса Object, а, следовательно, обладают методом ToString(). Для встроенных типов определена подходящая реализация этого метода. В частности, для всех подтипов арифметического типа метод ToString() возвращает в подходящей форме строку, задающую соответствующее значение арифметического типа. Заметьте, метод ToString можно вызывать явно, но, если явный вызов не указан, то он будет вызываться неявно, всякий раз, когда по контексту требуется преобразование к строковому типу. Вот соответствующий пример:
/// <summary>
/// Демонстрация преобразования в строку данных различного типа.
/// </summary>
public void ToStringTest()
{
s ="Владимир Петров ";
s1 =" Возраст: "; ux = 27;
s = s + s1 + ux.ToString();
s1 =" Зарплата: "; dy = 2700.50;
s = s + s1 + dy;
WhoIsWho("s",s);
}
Здесь для переменной ux метод был вызван явно, а для переменной dy он вызывается автоматически. Результат работы этой процедуры показан на рис. 4.3.
Преобразования из строкового типа в другие типы, например, в арифметический, должны выполняться явно. Но явных преобразований между арифметикой и строками не существуют. Необходимы другие механизмы, и они в C# имеются. Для этой цели можно использовать соответствующие методы класса Convert библиотеки FCL, встроенного в пространство имен System. Приведу соответствующий пример:
/// <summary>
/// Демонстрация преобразования строки в данные различного типа.
/// </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. Естественно, для успеха преобразования строка должна содержать значение в формате, допускающем подобное преобразование. Заметьте, например, что при записи значения числа для выделения дробной части должна использоваться запятая, а не точка; в противном случае возникнет ошибка периода выполнения.
На рис. 4.4 показаны результаты вывода и ввода данных с консоли при работе этой процедуры.
Преобразования и класс Convert
Класс Convert, определенный в пространстве имен System, играет важную роль, обеспечивая необходимые преобразования между различными типами. Напомню, что внутри арифметического типа можно использовать более простой, скобочный способ приведения к нужному типу. Но таким способом нельзя привести, например, переменную типа string к типу int, оператор присваивания: ux = (int)s1; приведет к ошибке периода компиляции. Здесь необходим вызов метода ToInt32 класса Convert, как это сделано в последнем примере предыдущего раздела.
Методы класса Convert поддерживают общий способ выполнения преобразований между типами. Класс Convert содержит 15 статических методов вида To <Type> (ToBoolean(),...ToUInt64()), где Type может принимать значения от Boolean до UInt64 для всех встроенных типов, перечисленных в таблице 3.1. Единственным исключением является тип object, - метода ToObject нет по понятным причинам, поскольку для всех типов существует неявное преобразование к типу object. Среди других методов отмечу общий статический метод ChangeType, позволяющий преобразование объекта к некоторому заданному типу.
Существует возможность преобразования к системному типу DateTime, который хотя и не является встроенным типом языка C#, но допустим в программах, как и любой другой системный тип. Приведу простейший пример работы с этим типом:
// System type: DateTime
System.DateTime dat = Convert.ToDateTime("15.03.2003");
Console.WriteLine("Date = {0}", dat);
Результатом вывода будет строка:
Date = 15.03.2003      0:00:00
Все методы To <Type> класса Convert перегружены и каждый из них имеет, как правило, более десятка реализаций с аргументами разного типа. Так что фактически эти методы задают все возможные преобразования между всеми встроенными типами языка C#.
Кроме методов, задающих преобразования типов, в классе Convert имеются и другие методы, например, задающие преобразования символов Unicode в однобайтную кодировку ASCII, преобразования значений объектов и другие методы. Подробности можно посмотреть в справочной системе.
Проверяемые преобразования
Уже упоминалось о том, что при выполнении явных преобразований могут возникать нежелательные явления, например, потеря точности. Я говорил, что вся ответственность за это ложится на программиста, и легче ему от этого не становится. А какую часть этого бремени может взять на себя язык программирования? Что можно предусмотреть для обнаружения ситуаций, когда такие явления все-таки возникают? В языке C# имеются необходимые для этого средства.
Язык C# позволяет создать проверяемый блок, в котором будет осуществляться проверка результата вычисления арифметических выражений. Если результат вычисления значения источника выходит за диапазон возможных значений целевой переменной, то возникнет исключение (говорят также: "будет выброшено исключение") соответствующего типа. Если предусмотрена обработка исключения, то дальнейшее зависит от обработчика исключения. В лучшем случае, программа сможет продолжить корректное выполнение. В худшем, - она остановится и выдаст информацию об ошибке. Заметьте, не произойдет самого опасного - продолжения работы программы с неверными данными.
Синтаксически проверяемый блок предваряется ключевым словом checked. В теле такого блока арифметические преобразования проверяются на допустимость. Естественно, подобная проверка требует дополнительных временных затрат. Если группа операторов в теле такого блока нам кажется безопасной, то их можно выделить в непроверяемый блок, используя ключевое слово unchecked. Замечу еще, что и в непроверяемом блоке при работе методов Convert все опасные преобразования проверяются и приводят к выбрасыванию исключений. Приведу пример, демонстрирующий все описанные ситуации:
/// <summary>
/// Демонстрация проверяемых и непроверяемых преобразований.
/// Опасные проверяемые преобразования приводят к исключениям.
/// Опасные непроверяемые преобразования приводят к неверным
/// результатам, что совсем плохо.
/// </summary>
public void CheckUncheckTest()
{
x = -25^2;
WhoIsWho ("x", x);
b= 255;
WhoIsWho("b",b);
// Проверяемые опасные преобразования.
// Возникают исключения, перехватываемые catch-блоком.
checked
{
try
{
b += 1;
}
catch (Exception e)
{
Console.WriteLine("Переполнение при вычислении b");
Console.WriteLine(e);
}
try
{
b = (byte)x;
}
catch (Exception e)
{
Console.WriteLine("Переполнение при преобразовании к byte");
Console.WriteLine(e);
}
// непроверяемые опасные преобразования
unchecked
{
try
{
b +=1;
WhoIsWho ("b", b);
b = (byte)x;
WhoIsWho ("b", b);
ux= (uint)x;
WhoIsWho ("ux", x);
Console.WriteLine("Исключений нет, но результаты не верны!");
}
catch (Exception e)
{
Console.WriteLine("Этот текст не должен появляться");
Console.WriteLine(e);
}
// автоматическая проверка преобразований в Convert
// исключения возникают, несмотря на unchecked
try
{
b = Convert.ToByte(x);
}
catch (Exception e)
{
Console.WriteLine("Переполнение при
преобразовании к byte!");
Console.WriteLine(e);
}
try
{
ux= Convert.ToUInt32(x);
}
catch (Exception e)
{
Console.WriteLine("Потеря знака при
преобразовании к uint!");
Console.WriteLine(e);
}
}
}
}

http://localhost:3232/img/empty.gifИсключения и охраняемые блоки. Первое знакомство

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


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

В состав библиотеки FCL входит класс Exception, свойства и методы которого позволяют работать с исключениями как с объектами, получать нужную информацию, дополнять объект собственной информацией. У класса Exception - большое число потомков, каждый из которых описывает определенный тип исключения. При проектировании собственных классов можно параллельно проектировать и классы, задающие собственный тип исключений, который может выбрасываться в случае ошибок при работе методов класса. Создаваемый класс исключений должен быть потомком класса Exception.
Если в некотором модуле предполагается возможность появления исключений, то разумно предусмотреть и их обработку. В этом случае в модуле создается охраняемый try-блок, предваряемый ключевым словом try. Вслед за этим блоком следуют один или несколько блоков, обрабатывающих исключения, - catch-блоков. Каждый catch-блок имеет формальный параметр класса Exception или одного из его потомков. Если в try-блоке возникает исключение типа T, то catch-блоки начинают конкурировать в борьбе за перехват исключения. Первый по порядку catch-блок, тип формального аргумента которого согласован с типом T - совпадает с ним или является его потомком - захватывает исключение и начинает выполняться; поэтому порядок написания catch-блоков небезразличен. Вначале должны идти специализированные обработчики. Универсальным обработчиком является catch-блок с формальным параметром родового класса Exception, согласованным с исключением любого типа T. Универсальный обработчик, если он есть, стоит последним, поскольку захватывает исключение любого типа.
Конечно, плохо, когда в процессе работы той или иной процедуры возникает исключение. Однако его появление еще не означает, что процедура не сможет выполнить свой контракт. Исключение может быть нужным образом обработано, после чего продолжится нормальный ход вычислений приложения. Гораздо хуже, когда возникают ошибки в работе процедуры, не приводящие к исключениям. Тогда работа продолжается с неверными данными без исправления ситуации и даже без уведомления о возникновении ошибки. Наш пример показывает, что вычисления в C# могут быть небезопасными и следует применять специальные средства языка, такие как, например, checked-блоки, чтобы избежать появления подобных ситуаций.
Вернемся к обсуждению нашего примера. Здесь как в проверяемых, так и в непроверяемых блоках находятся охраняемые блоки с соответствующими обработчиками исключительных ситуаций. Во всех случаях применяется универсальный обработчик, захватывающий любое исключение в случае его возникновения в try-блоке. Сами обработчики являются простыми уведомителями, они лишь сообщают об ошибочной ситуации, не пытаясь исправить ее.
Опасные вычисления в охраняемых проверяемых блоках
Такая ситуация возникает в первых двух try-блоках нашего примера. Эти блоки встроены в проверяемый checked-блок. В каждом из них используются опасные вычисления, приводящие к неверным результатам. Так, при присваивании невинного выражения b+1 из-за переполнения переменная b получает значение 0, а не 256. Поскольку вычисление находится в проверяемом блоке, то ошибка обнаруживается и результатом является вызов исключения. Далее, поскольку все это происходит в охраняемом блоке, то управление перехватывается и обрабатывается в соответствующем catch-блоке. Эту ситуацию следует отнести к нормальному, разумно построенному процессу вычислений.
Опасные вычисления в охраняемых непроверяемых блоках
Такую ситуацию демонстрирует третий try-блок нашего примера, встроенный в непроверяемый unchecked-блок. Здесь участвуют те же самые опасные вычисления, но теперь их корректность не проверяется, они не вызывают исключений, и как следствие, соответствующий catch-блок не вызывается. Результаты вычислений при этом неверны, но никаких уведомлений об этом нет. Это самая плохая ситуация, которая может случиться при работе наших программ.
Заметьте, проверку переполнения в арифметических вычислениях можно включить не только с помощью создания checked-блоков, но и задав свойство checked проекта (по умолчанию, оно выключено). Как правило, это свойство проекта всегда включается в процессе разработки и отладки. В законченной версии проекта свойство вновь отключается, поскольку полная проверка всех преобразований требует определенных накладных расходов, увеличивая время работы; а проверяемые блоки остаются лишь там, где такой контроль действительно необходим.
Область действия проверки или ее отключения можно распространить и на отдельное выражение. В этом случае спецификаторы checked и unchecked предшествуют выражению, заключенному в круглые скобки. Такое выражение называется проверяемым (непроверяемым) выражением, а checked и unchecked рассматриваются как операции, допустимые в выражениях.
Опасные преобразования и методы класса Convert
Явно выполняемые преобразования по определению относятся к опасным. Явные преобразования можно выполнять по-разному. Синтаксически наиболее просто выполнить приведение типа - кастинг, явно указав тип приведения, как это сделано в только что рассмотренном примере. Но если это делается в непроверяемом блоке, последствия могут быть самыми печальными. Поэтому такой способ приведения типов следует применять с большой осторожностью. Надежнее выполнять преобразования типов более универсальным способом, используя стандартный встроенный класс Convert, специально спроектированный для этих целей.
В нашем примере четвертый и пятый try-блоки встроены в непроверяемый unchecked-блок. Но опасные преобразования реализуются методами класса Convert, которые сами проводят проверку и при необходимости выбрасывают исключения, что и происходит в нашем случае.
На рис. 4.5 показаны результаты работы процедуры CheckUncheckTest. Их анализ способствует лучшему пониманию рассмотренных нами ситуаций.
На этом, пожалуй, пора поставить точку в обсуждении системы типов языка C#. За получением тех или иных подробностей, как всегда, следует обращаться к справочной системе.
http://localhost:3232/img/empty.gifhttp://localhost:3232/img/empty.gif

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г.Яндекс.Метрика