Тверской государственный университет
Опубликован: 02.12.2009 | Доступ: свободный | Студентов: 3450 / 677 | Оценка: 4.41 / 4.23 | Длительность: 09:18:00
ISBN: 978-5-9963-0259-8
Лекция 3:

Выражения и операции

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

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

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

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

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

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

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

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

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

/// <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);
}

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

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

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

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

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

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

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

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

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

Выражения над строками. Преобразования строк

Начнем с символьного типа. Давайте уточним, какие выражения можно строить над операндами этого типа. На алфавите символов определен порядок, задаваемый Unicode кодировкой символов. Знать, как кодируется тот или иной символ, не обязательно, но следует помнить, что кодировка буквенных символов таких алфавитов, как кириллица, латиница и других языковых алфавитов, являющихся частью Unicode алфавита, является плотной, так что, например, код буквы "а" на единицу меньше кода буквы "б". Исключение составляет буква "Ё", выпадающая из плотной кодировки. Большие буквы (заглавные) в кодировке предшествуют малым буквам (строчным). Для цифр также используется плотная кодировка.

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

char sym = 'Ё';
     int code_Sym = sym;
    Console.WriteLine("sym = {0}, code = {1}",
    sym, code_Sym);
code_Sym++;
sym = (char)code_Sym;
Console.WriteLine("sym = {0}, code = {1}",
    sym, code_Sym);

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

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

/// <summary>
  /// Соответствует ли s требованиям,
  /// предъявляемым к именам в русском языке:
  /// Первый символ - большая буква кириллицы
  /// Остальные символы - малые буквы кириллицы
  /// </summary>
  /// <param name="s">входная строка</param>
  /// <returns>
  /// true, если s соответствует правилам,
  /// false -  в противном случае
  /// </returns>
  public bool IsName(string s)
  {      
      if (s == "") return false;
      char letter = s[0];
      if(!(letter >= 'А' && letter <= 'Я'))return false;
      for(int i=1; i< s.Length; i++)
      {
    letter = s[i];
    if (!(letter >= 'а' && letter <= 'я')) return false;
      }
      return true; 
  }

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

По этой причине над операндами строкового типа из множества операций, задаваемых знаками логических, арифметических и операций отношения, определены только три операции. Две операции позволяют сравнивать строки на эквивалентность (==, !=). Третья операция, задаваемая знаком операции "+", называется операцией конкатенации или сцепления строк и позволяет вторую строку присоединить к концу первой строки. Вот пример:

string s1 = "Мир";
  if (s1 == "Мир" | s1 == "мир") s1 += " Вам";
  Console.WriteLine(s1);

Операций над строками немного, но методов вполне достаточно. Сравнивать две строки, используя знаки операций " >, < ", нельзя, но есть методы сравнения Compare, решающие эту задачу. О работе со строками более подробно поговорим в отдельной лекции.

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

Хотя неявных преобразований строкового типа в другие типы нет, необходимость в преобразованиях существует. Когда пользователь вводит данные различных типов, он задает эти данные как строки текста, поскольку текстовое представление наиболее естественно для человека. Поэтому при вводе практически всегда возникает задача преобразования данных, заданных текстом, в "настоящий" тип данных.

Классы библиотеки FCL предоставляют два способа явного выполнения таких преобразований:

  • метод Parse ;
  • методы класса Convert.
Метод Parse

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

static void InputVars()
  {
      string strInput;
      Console.WriteLine(INPUT_BYTE);
      strInput = Console.ReadLine();
      byte b1;
      b1 = byte.Parse(strInput);

      Console.WriteLine(INPUT_INT);
      strInput = Console.ReadLine();
      int n;
      n = int.Parse(strInput);

      Console.WriteLine(INPUT_FLOAT);
      strInput = Console.ReadLine();
      float x;
      x = float.Parse(strInput);

      Console.WriteLine(INPUT_CHAR);
      strInput = Console.ReadLine();
      char ch;
      ch = char.Parse(strInput);
  }

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

Преобразование в строковый тип

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

/// <summary>
/// Демонстрация преобразования в строку
/// данных различного типа.
/// </summary>
public void ToStringTest()
{
    string name;
    uint age;
    double salary;
    name = "Владимир Петров";
    age = 27;
    salary = 27000;
    string s = "Имя: " + name +
       ". Возраст: " + age.ToString() +
       ". Зарплата: " + salary;            
    Console.WriteLine(s);
}

Здесь для переменной age метод был вызван явно, а для переменной salary он вызывается автоматически.

Федор Антонов
Федор Антонов

Здравствуйте!

Записался на ваш курс, но не понимаю как произвести оплату.

Надо ли писать заявление и, если да, то куда отправлять?

как я получу диплом о профессиональной переподготовке?

Илья Ардов
Илья Ардов

Добрый день!

Я записан на программу. Куда высылать договор и диплом?