Тверской государственный университет
Опубликован: 22.11.2005 | Доступ: свободный | Студентов: 30399 / 1845 | Оценка: 4.31 / 3.69 | Длительность: 28:26:00
ISBN: 978-5-9556-0050-5
Лекция 4:

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

< Лекция 3 || Лекция 4: 123 || Лекция 5 >
Аннотация: Преобразования типов. Преобразования внутри арифметического типа. Преобразования строкового типа. Класс Convert и его методы. Проверяемые преобразования. Управление проверкой арифметических преобразований.

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

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

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

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

Поскольку операции над ссылочными типами не определены ( исключением являются строки, но операции над ними, в том числе и присваивание, выполняются как над значимыми типами), то необходимость в них возникает только при присваиваниях и вызовах методов. Семантика таких преобразований рассмотрена в предыдущей лекции 3, где подробно обсуждалась семантика присваивания и совпадающая с ней семантика замены формальных аргументов фактическими. Там же много говорилось о преобразованиях между ссылочными и значимыми типами, выполняемых при этом операциях упаковки значений в объекты и обратной их распаковки.

Коротко повторю основные положения, связанные с преобразованиями ссылочных типов. При присваиваниях (замене аргументов) тип источника должен быть согласован с типом цели, то есть объект, связанный с источником, должен принадлежать классу, являющемуся потомком класса цели. В случае согласования типов, ссылочная переменная цели связывается с объектом источника и ее тип динамически изменяется, становясь типом источника. Это преобразование выполняется автоматически и неявно, не требуя от программиста никаких дополнительных указаний. Если же тип цели является потомком типа источника, то неявное преобразование отсутствует, даже если объект, связанный с источником, принадлежит типу цели. Явное преобразование, заданное программистом, позволяет справиться с этим случаем. Ответственность за корректность явных преобразований лежит на программисте, так что может возникнуть ошибка на этапе выполнения, если связываемый объект реально не является объектом класса цели. За примерами следует обратиться к лекции 3, еще раз обратив внимание на присваивания объектов классов Parent и Child.

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

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

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

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

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

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

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

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

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

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

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

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

Диаграмма, приведенная на рис. 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);
}

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

Вывод на печать результатов теста OLoadTest

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

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

Приведу все-таки некоторые комментарии. При первом вызове метода тип источника - int, а тип аргумента у четырех возможных реализаций соответственно float, long, ulong, double. Явного соответствия нет, поэтому нужно искать самый короткий путь на схеме. Так как не существует неявного преобразования из типа int в тип ulong (на диаграмме нет пути), то остаются возможными три реализации. Но путь из int в long короче, чем остальные пути, поэтому будет выбрана long-реализация метода.

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

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

< Лекция 3 || Лекция 4: 123 || Лекция 5 >
Александр Галабудник
Александр Галабудник

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

Александра Гусева
Александра Гусева