Типы или классы? и типы, и классы

Язык C# в большей степени, чем язык C++, является языком объектного программирования. В чем это выражается? В языке C# сглажено различие между типом и классом. Все типы - встроенные и пользовательские - одновременно являются классами, связанными отношением наследования. Родительским, базовым классом является класс Object. Все остальные типы или, точнее, классы являются его потомками, наследуя методы этого класса. У класса Object есть четыре наследуемых метода:

  1. bool Equals (object obj) - проверяет эквивалентность текущего объекта и объекта, переданного в качестве аргумента;
  2. System.Type GetType () - возвращает системный тип текущего объекта;
  3. string ToString () - возвращает строку, связанную с объектом. Для арифметических типов возвращается значение, преобразованное в строку;
  4. int GetHashCode() - служит как хэш-функция в соответствующих алгоритмах поиска по ключу при хранении данных в хэш-таблицах.

Естественно, что все встроенные типы нужным образом переопределяют методы родителя и добавляют собственные методы и свойства. Учитывая, что и типы, создаваемые пользователем, также являются потомками класса Object, то для них необходимо переопределить методы родителя, если предполагается использование этих методов; реализация родителя, предоставляемая по умолчанию, не обеспечивает нужного эффекта.

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

int x=11;int v = new Int32();v = 007;string s1 = "Agent";s1 = s1 + v.ToString() +x.ToString();

В этом примере переменная x объявляется как обычная переменная типа int. В то же время для объявления переменной v того же типа int используется стиль, принятый для объектов. В объявлении применяется конструкция new и вызов конструктора класса. В операторе присваивания, записанном в последней строке фрагмента, для обеих переменных вызывается метод ToString, как это делается при работе с объектами. Этот метод, наследуемый от родительского класса Object, переопределенный в классе int, возвращает строку с записью целого. Сообщу еще, что класс int не только наследует методы родителя - класса Object, - но и дополнительно определяет метод CompareTo, выполняющий сравнение целых, и метод GetTypeCode, возвращающий системный код типа. Для класса Int определены также статические методы и поля, о которых расскажу чуть позже.

Так что же такое после этого int, спросите Вы: тип или класс? Ведь ранее говорилось, что int относится к value-типам, следовательно, он хранит в стеке значения своих переменных, в то время как объекты должны задаваться ссылками. С другой стороны, создание экземпляра с помощью конструктора, вызов методов, наконец, существование родительского класса Object, - все это указывает на то, что int - это настоящий класс. Правильный ответ состоит в том, что int - это и тип, и класс. В зависимости от контекста x может восприниматься как переменная типа int или как объект класса int. Это же верно и для всех остальных value- типов. Замечу еще, что все значимые типы фактически реализованы как структуры, представляющие частный случай класса.

Остается понять, для чего в языке C# введена такая двойственность. Для int и других значимых типов сохранена концепция типа не только из-за ностальгических воспоминаний о типах. Дело в том, что значимые типы эффективнее в реализации, им проще отводить память, так что именно соображения эффективности реализации заставили авторов языка сохранить значимые типы. Более важно, что зачастую необходимо оперировать значениями, а не ссылками на них, хотя бы из-за различий в семантике присваивания для переменных ссылочных и значимых типов.

С другой стороны, в определенном контексте крайне полезно рассматривать переменные типа int как настоящие объекты и обращаться с ними как с объектами. В частности, полезно иметь возможность создавать и работать со списками, чьи элементы являются разнородными объектами, в том числе и принадлежащими к значимым типам.

Дальнейшие примеры работы с типами и проект Types

Обсуждение особенностей тех или иных конструкций языка невозможно без приведения примеров. Для каждой лекции я строю один или несколько проектов, сохраняя по возможности одну и ту же схему и реально выполняя проекты в среде Visual Studio .Net. Для работы с примерами данной лекции построен консольный проект с именем Types, содержащий два класса: Class1 и Testing. Расскажу чуть подробнее о той схеме, по которой выстраиваются проекты. Класс Class1 строится автоматически при начальном создании проекта. Он содержит процедуру Main - точку входа в проект. В процедуре Main создается объект класса Testing и вызываются методы этого класса, тестирующие те или иные ситуации. Для решения специальных задач, помимо всегда создаваемого класса Testing, создаются один или несколько классов. Добавление нового класса в проект я осуществляю выбором пункта меню Project/Add Class. В этом случае автоматически строится заготовка для нового класса, содержащая конструктор без параметров. Дальнейшая работа над классом ведется над этой заготовкой. Создаваемые таким образом классы хранятся в проекте в отдельных файлах. Это особенно удобно, если классы используются в разных проектах. Функционально связанную группу классов удобнее хранить в одном файле, что не возбраняется.

Все проекты в книге являются самодокументируемыми. Классы и их методы сопровождаются тегами <summary>. В результате появляются подсказки при вызове методов и возможность построения XML-отчета, играющего роль спецификации проекта.

Приведу текст класса Class1:

using System;namespace Types{ /// <summary> /// Проект Types содержит примеры, иллюстрирующие работу /// со встроенными скалярными типами языка С#. /// Проект содержит классы: Testing, Class1. /// /// </summary> class Class1 { /// <summary> /// Точка входа проекта. /// В ней создается объект класса Testing /// и вызываются его методы. /// </summary> [STAThread] static void Main() { Testing tm = new Testing(); Console.WriteLine("Testing.Who Test"); tm.WhoTest(); Console.WriteLine("Testing.Back Test"); tm.BackTest(); Console.WriteLine("Testing.OLoad Test"); tm.OLoadTest(); Console.WriteLine("Testing.ToString Test"); tm.ToStringTest(); Console.WriteLine("Testing.FromString Test"); tm.FromStringTest(); Console.WriteLine("Testing.CheckUncheck Test"); tm.CheckUncheckTest(); } }}

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

типы или классы? и типы, и классы - student2.ru 3. Лекция: Система типов языка С#

Семантика присваивания

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

x = e.

Чтобы присваивание было допустимым, типы переменной x и выражения e должны быть согласованными. Пусть сущность x согласно объявлению принадлежит классу T. Будем говорить, что тип T основан на классе T и является базовым типом x, так что базовый тип определяется классом объявления. Пусть теперь в рассматриваемом нами присваивании выражение e связано с объектом типа T1.

Определение: тип T1 согласован по присваиванию с базовым типом T переменной x, если класс T1 является потомком класса T.

Присваивание допустимо, если и только если имеет место согласование типов. Так как все классы в языке C# - встроенные и определенные пользователем - по определению являются потомками класса Object, то отсюда и следует наш частный случай - переменным класса Object можно присваивать выражения любого типа.

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

Например, пусть задан некоторый класс Parent, а класс Child - его потомок, объявленный следующим образом:

class Child:Parent {...}

Пусть теперь в некотором классе, являющемся клиентом классов Parent и Child, объявлены переменные этих классов и созданы связанные с ними объекты:

Parent p1 = new Parent(), p2 = new Parent();Child ch1 = new Child(), ch2 = new Child();

Тогда допустимы присваивания:

p1 = p2; p2= p1; ch1=ch2; ch2 = ch1; p1 = ch1; p1 = ch2;

Но недопустимы присваивания:

ch1 = p1; ch2 = p1; ch2 = p2;

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

p1 = ch1; ... ch1 = (Child)p1;

Семантика присваивания справедлива и для другого важного случая - при рассмотрении соответствия между формальными и фактическими аргументами процедур и функций. Если формальный аргумент согласно объявлению имеет тип T, а выражение, задающее фактический аргумент, имеет тип T1, то имеет место согласование типов формального и фактического аргумента, если и только если класс T1 является потомком класса T. Отсюда незамедлительно следует, что если формальный параметр процедуры принадлежит классу Object, то фактический аргумент может быть выражением любого типа.

Преобразование к типу object

Рассмотрим частный случай присваивания x = e; когда x имеет тип object. В этом случае гарантируется полная согласованность по присваиванию - выражение e может иметь любой тип. В результате присваивания значением переменной x становится ссылка на объект, заданный выражением e. Заметьте, текущим типом x становится тип объекта, заданного выражением e. Уже здесь проявляется одно из важных различий между классом и типом. Переменная, лучше сказать сущность x, согласно объявлению принадлежит классу Object, но ее тип - тип того объекта, с которым она связана в текущий момент, - может динамически изменяться.

Примеры преобразований

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

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

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

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

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

/// <summary>/// получаем информацию о типе и значении/// переданного аргумента - переменной или выражения/// </summary>public void WhoTest(){ WhoIsWho("x",x); WhoIsWho("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, но в одном случае передается обычное арифметическое выражение, автоматически преобразуемое в объект. Аналогичная ситуация имеет место и при выполнении присваивания в рассматриваемой процедуре.

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

типы или классы? и типы, и классы - student2.ru
Рис. 3.1. Вывод на печать результатов теста WhoTest

типы или классы? и типы, и классы - student2.ru 3. Лекция. Система типов языка С#

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

Рассматривая семантику присваивания и передачи аргументов, мы обошли молчанием один важный вопрос. Будем называть целью левую часть оператора присваивания, а также формальный аргумент при передаче аргументов в процедуру или функцию. Будем называть источником правую часть оператора присваивания, а также фактический аргумент при передаче аргументов в процедуру или функцию. Поскольку источник и цель могут быть как значимого, так и ссылочного типа, то возможны четыре различные комбинации. Рассмотрим их подробнее.

  • Цель и источник значимого типа. Здесь наличествует семантика значимого присваивания. В этом случае источник и цель имеют собственную память для хранения значений. Значения источника заменяют значения соответствующих полей цели. Источник и цель после этого продолжают жить независимо. У них своя память, хранящая после присваивания одинаковые значения.
  • Цель и источник ссылочного типа. Здесь имеет место семантика ссылочного присваивания. В этом случае значениями источника и цели являются ссылки на объекты, хранящиеся в памяти ("куче"). При ссылочном присваивании цель разрывает связь с тем объектом, на который она ссылалась до присваивания, и становится ссылкой на объект, связанный с источником. Результат ссылочного присваивания двоякий. Объект, на который ссылалась цель, теряет одну из своих ссылок и может стать висячим, так что его дальнейшую судьбу определит сборщик мусора. С объектом в памяти, на который ссылался источник, теперь связываются, по меньшей мере, две ссылки, рассматриваемые как различные имена одного объекта. Ссылочное присваивание приводит к созданию псевдонимов - к появлению разных имен у одного объекта. Особо следует учитывать ситуацию, когда цель и/или источник имеет значение void. Если такое значение имеет источник, то в результате присваивания цель получает это значение и более не ссылается ни на какой объект. Если же цель имела значение void, а источник - нет, то в результате присваивания ранее "висячая" цель становится ссылкой на объект, связанный с источником.
  • Цель ссылочного типа, источник значимого типа. В этом случае "на лету" значимый тип преобразуется в ссылочный. Как обеспечивается двойственность существования значимого и ссылочного типа - переменной и объекта? Ответ прост: за счет специальных, эффективно реализованных операций, преобразующих переменную значимого типа в объект и обратно. Операция "упаковать" (boxing) выполняется автоматически и неявно в тот момент, когда по контексту требуется объект, а не переменная. Например, при вызове процедуры WhoIsWho требуется, чтобы аргумент any был объектом. Если фактический аргумент является переменной значимого типа, то автоматически выполняется операция "упаковать". При ее выполнении создается настоящий объект, хранящий значение переменной. Можно считать, что происходит упаковка переменной в объект. Необходимость в упаковке возникает достаточно часто. Примером может служить и процедура консольного вывода WriteLine класса Console, которой требуются объекты, а передаются зачастую переменные значимого типа.
  • Цель значимого типа, источник ссылочного типа. В этом случае "на лету" ссылочный тип преобразуется в значимый. Операция "распаковать" (unboxing) выполняет обратную операцию, - она "сдирает" объектную упаковку и извлекает хранимое значение. Заметьте, операция "распаковать" не является обратной к операции "упаковать" в строгом смысле этого слова. Оператор obj = x корректен, но выполняемый следом оператор x = obj приведет к ошибке. Недостаточно, чтобы хранимое значение в упакованном объекте точно совпадало по типу с переменной, которой присваивается объект. Необходимо явно заданное преобразование к нужному типу.

Операции "упаковать" и "распаковать" (boxing и unboxing).

Примеры

В нашем следующем примере демонстрируется применение обеих операций - упаковки и распаковки. Поскольку формальный аргумент процедуры Back принадлежит классу Object, то при передаче фактического аргумента значимого типа происходит упаковка значения в объект. Этот объект и возвращается процедурой. Его динамический тип определяется тем объектом памяти, на который указывает ссылка. Когда возвращаемый результат присваивается переменной значимого типа, то, несмотря на совпадение типа переменной с динамическим типом объекта, необходимо выполнить распаковку, "содрать" объектную упаковку и вернуть непосредственное значение. Вот как выглядит процедура Back и тестирующая ее процедура BackTest из класса Testing:

/// <summary>/// Возвращает переданный ему аргумент./// Фактический аргумент может иметь произвольный тип./// Возвращается всегда объект класса object./// Клиент, вызывающий метод, должен при необходимости /// задать явное преобразование получаемого результата/// </summary>/// <param name="any"> Допустим любой аргумент</param>/// <returns></returns>object Back(object any){ return(any);}/// <summary>/// Неявное преобразование аргумента в тип object/// Явное приведение типа результата./// </summary>public void BackTest(){ ux = (uint)Back(ux); WhoIsWho("ux",ux); s1 = (string)Back(s); WhoIsWho("s1",s1); x =(int)(uint)Back(ux); WhoIsWho("x",x); y = (float)(double)Back(11 + 5.55 + 5.5f); WhoIsWho("y",y);}

Обратите внимание, что если значимый тип в левой части оператора присваивания не совпадает с динамическим типом объекта, то могут потребоваться две операции приведения. Вначале нужно распаковать значение, а затем привести его к нужному типу, что и происходит в двух последних операторах присваивания. Приведу результаты вывода на консоль, полученные при вызове процедуры BackTest в процедуре Main.

типы или классы? и типы, и классы - student2.ru
Рис. 3.2. Вывод на печать результатов теста BackTest

Две двойственные операции "упаковать" и "распаковать" позволяют, в зависимости от контекста, рассматривать значимые типы как ссылочные, переменные как объекты, и наоборот.

4. Лекция: Преобразования типов

типы или классы? и типы, и классы - student2.ru типы или классы? и типы, и классы - student2.ru

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

типы или классы? и типы, и классы - student2.ru

типы или классы? и типы, и классы - student2.ru

типы или классы? и типы, и классы - student2.ru

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

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

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

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

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

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

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

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

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

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

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

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

типы или классы? и типы, и классы - student2.ru
Рис. 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);}

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

типы или классы? и типы, и классы - student2.ru
Рис. 4.2. Вывод на печать результатов теста OLoadTest

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

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

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

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

4. Лекция: Преобразования типов

типы или классы? и типы, и классы - student2.ru

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

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

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

Важным классом преобразований являются преобразования в строковый тип и наоборот. Преобразования в строковый тип всегда определены, поскольку, напомню, все типы являются потомками базового класса 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);}

типы или классы? и типы, и классы - student2.ru
Рис. 4.3. Вывод на печать результатов теста ToStringTest

Здесь для переменной 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 показаны результаты вывода и ввода данных с консоли при работе этой процедуры.

типы или классы? и типы, и классы - student2.ru
Рис. 4.4. Вывод на печать результатов теста FromStringTest

Преобразования и класс 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: DateTimeSystem.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# позволяет создать проверяемый блок, в котором будет осуществляться проверка результата вычисления арифметических выражений. Если результат вычисления значения источника выходит за диапазон возможных значений целевой переменной, то возникнет исключение (говорят также: "будет выброшено исключение") соответствующего типа. Если предусмотрена обработка исключения, то дальнейшее зависит от обработчика исключения. В лучшем случае, программа сможет продолжить корректное выполнение. В худшем, - она остановится и выдаст информацию об ошибке. Заметьте, не произойдет самого опасного - продолжения работы программы с неверными данными.

Синтаксически прове

Наши рекомендации