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

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

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

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

Приоритет и порядок выполнения операций

Большинство операций в языке C#, их приоритет и порядок наследованы из языка C++. Однако есть и различия, например, нет операции « , », позволяющей вычислять список выражений, добавлены операции checked и unchecked, применимые к выражениям.

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

Таблица 3-1. Приоритеты операций языка C#

Приоритет Категория Операции Порядок
0 Первичные (expr),  x.y, x->y, f(x), a[x], x++,  x—, new,typeof(t), checked(expr),   unchecked(expr) Слева направо
1 Унарные +, -, !, ~,   ++x, —x, (T)x, sizeof(t) Слева направо
2 Мультипликативные(Умножение) *, /, % Слева направо
3 Аддитивные(Сложение) +, - Слева направо
4 Сдвиг << ,>> Слева направо
5 Отношения, проверка типов <,   >, <=, >=, is, as Слева направо
6 Эквивалентность ==,   != Слева направо
7 Логическое И (AND) & Слева направо
8 Логическое исключающее ИЛИ (XOR) ^ Слева направо
9 Логическое ИЛИ (OR) | Слева направо
10 Условное логическое И && Слева направо
11 Условное логическое ИЛИ || Слева направо
12 Условное выражение ? : Справа налево
13 ПрисваиваниеСклеивание   с null =,   *=, /=, %=, +=, -=,   <<=, >>=, &=, ^=, |=?? Справа налево
14 Лямбда оператор => Справа налево

Перегрузка операций и методов

Под перегрузкой операции понимается существование нескольких реализаций одной и той же операции.  Например, операция со знаком «+» выполняется по-разному в зависимости от того, являются ли ее операнды целыми числами, длинными целыми, целыми с фиксированной или плавающей точкой или являются строками текста.

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

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

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

Каждый объект (переменная), каждый операнд при вычислении выражения, само выражение характеризуется парой <v, T>, задающей значение выражения и его тип. В процессе вычислений зачастую возникает необходимость преобразования типов – необходимость преобразовать пару <vs, Ts>  к паре <vg, Tg>. Исходная пара называется источником преобразования, заключительная  — целью преобразования.

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

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

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

Преобразование, для которого не выполняется хотя бы одно из этих условий, называется опасным. Достаточным условием существования безопасного преобразования является, например, условие того, что тип Ts является подтипом типа Tg. Действительно, в этом случае любое значение источника является одновременно и допустимым  значением цели. Так преобразование от типа int  к типу double является безопасным. Обратное преобразование естественно будет опасным.

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

Существуют разные способы выполнения явных преобразований – операция кастинга (приведение к типу), методы специального класса Convert, специальные методы ToString, Parse. Все эти способы будут рассмотрены в данной главе.

Поясним, как выполняются неявные преобразования при вычислении выражения. Пусть при вычислении некоторого выражения необходимо выполнить сложение  x + n, где x имеет тип double, а n – int. Среди многочисленных реализаций сложения есть операции, выполняющие сложение операндов типа int и сложение операндов типа double, так что при выборе любой из этих реализаций сложения потребуется преобразование типа одного из операндов. Поскольку преобразование типа от int к double является безопасным, а в другую сторону это преобразование опасно, то выбирается безопасное преобразование, выполняемое автоматически, второй операнд неявно преобразуется к типу double, выполняется сложение операндов этого типа, и результат сложения будет иметь тип double.

Организация программного проекта ConsoleExpressions

Как обычно, все примеры программного кода, появляющиеся в тексте, являются частью программного проекта. Опишу структуру используемого в этой главе консольного проекта, названного ConsoleExpressions. Помимо созданного по умолчанию класса Program в проект добавлены два класса с именами TestingExpressions  и Scales. Каждый из методов класса TestingExpressions представляет тест, позволяющий анализировать особенности операций, используемых при построении выражений. Так что этот класс представляет собой сборник тестов. Класс Scale носит содержательный характер, демонстрируя работу со шкалами, о которых пойдет речь в этой главе. Чтобы иметь возможность вызывать методы этих классов, в процедуре Main класса Program объявляются и создаются объекты этих классов. Затем эти объекты используются в качестве цели вызова соответствующих методов. Общая схема процедуры Main и вызова методов класса такова:

static void Main(string[] args)

{

string answer = «Да»;

do

{

try

{

TestingExpressions test = new TestingExpressions();

test.Casting();

//Вызов других методов

}

catch (Exception e)

{

Console.WriteLine(

«Невозможно нормально продолжить работу!»);

Console.WriteLine(e.Message);

}

Console.WriteLine(«Продолжим работу? (Да/нет)»);

answer = Console.ReadLine();

} while (answer == «Да» || answer == «да» || answer == «yes»);

}

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

Операции высшего приоритета

Рассмотрим подробнее операции из таблицы 3_1, отнесенные к высшему приоритету и выполняемые в первую очередь.

Выражения в скобках

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

Вот классический пример выражения со скобками:

result = (x1 + x2) * (x1 — x2);

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

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

bool temp = x1 + x2 > x1 — x2 && x1 — 2 < x2 + 1;

result = temp? 1 : 2;

Однако «лишние» скобки в записи выражения явно не помешают:

bool temp = ((x1 + x2) > (x1 — x2)) &&

((x1 — 2) < (x2 + 1));

result = temp? 1 : 2;

Операция вызова «точка» x.y, вызов функций F(x), вызов, инициируемый указателем x -> y

Несмотря на то, что точка «малозаметный»  символ, операция вызова  x.y является одной из основных и важнейших операций в объектном программировании. Здесь x является целью вызова и представляет некоторый уже созданный объект, а y является свойством или методом этого объекта. Поскольку свойство объекта может задавать новый объект, то может порождаться достаточно длинная цепочка вызовов (x.y1.y2.y3.y4), заканчивающаяся, как правило, терминальным свойством.

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

x.M(a1, … ak)

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

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

Console.WriteLine(INPUT_FLOAT);

strInput = Console.ReadLine();

x1 = Convert.ToSingle(strInput);

Здесь в качестве цели вызовов выступают классы Console и Convert, вызывающие статические методы этих классов.

Если цель вызова указана, то такой вызов называется квалифицированным. Когда целью вызова является текущий объект, то ее (цель) можно опускать, делая вызов неквалифицированным. Такой вызов всегда можно сделать квалифицированным, указав this в качестве имени текущего объекта:

result += this.n * this.m;

В данном случае можно было бы опустить имя текущего объекта и записать выражение следующим образом:

result += n * m;

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

result += x2 * x2 + F(x1) — x1 * x1;

Здесь все вызовы свойств x1, x2, метода F(x) записаны без квалификации, но можно превратить их в квалифицированные, показав явным образом, что реально при вычислении выражения используется операция вызова «точка».  В последних примерах предполагается, что n, m, x1, x2 являются полями класса, а F – методом класса.

В неуправляемом коде, который появляется в блоках, объявленных как небезопасные, разрешена работа с указателями. Вызов полей и методов объекта, когда целью является указатель, задается операцией «стрелка» x -> y, где x – это указатель, а y – поле объекта, на который указывает указатель. Переходя от указателей к объекту, операцию «стрелка» можно заменить операцией «точка» следующим образом: (*x).y

В нашем курсе работа с указателями рассматриваться не будет.

Операция индексации a[i, j]

О массивах подробно поговорим в одной из ближайших глав этого курса. Сейчас скажем, что если уже объявлен массив, то в выражении можно использовать элемент этого массива, задав индексы этого элемента. Так, например, если объявлен одномерный массив w, содержащий n элементов, то выражение w[i] будет определять i-й элемент этого массива, где индекс принимает значения от 0 до n-1.

Операция new

Ключевое слово «new» в языке C# в зависимости от контекста используется по-разному. Оно может задавать модификатор метода или операцию в выражениях. Операция new предназначена для создания объектов. Поскольку каждая реальная программа немыслима без объектов, то операция new встречается практически во всех программах, хотя зачастую в неявной форме. Синтаксически эта операция имеет вид:

new <вызов конструктора объекта>

Чаще всего эта операция встречается в инициализаторах объекта в момент его объявления. Но допустимы и другие способы применения этой операции, например, в качестве фактического аргумента при вызове метода класса. Приведу совсем экзотический пример, где new встречается в арифметическом выражении:

Type tip = (n + new double()).GetType();

Рассмотрим обычное объявление скалярной переменной значимого типа:

int x = 77;

Это объявление можно рассматривать как краткую форму записи следующих операторов:

int x = new int(); x = 77;

 

Операции sizeof и typeof

Операция sizeof возвращает размер памяти, заданный в байтах, отводимой для хранения экземпляра класса. Ее единственным аргументом является имя класса. Существенное ограничение состоит в том, что она не применима к классам, создаваемым программистом.

Операция typeof возвращает объект класса Type, характеризующий тип класса, заданного в качестве аргумента операции. В отличие от операции sizeof она применима к классам, создаваемым программистом. Тот же результат, что и операция typeof дает метод GetType, вызванный объектом – экземпляром класса. Этот метод наследуется от родительского класса object и существует у всех классов, в том числе и создаваемых программистом.

Приведу пример использования этих операций:

/// <summary>

/// определение размеров и типов

/// </summary>

public  void SizeMethod()

{

Console.WriteLine(«Размер типа Boolean = » + sizeof(bool));

Console.WriteLine(«Размер типа double = » + sizeof(double));

Console.WriteLine(«Размер типа char = » + sizeof(System.Char));

//Console.WriteLine(«Размер класса TestingExpressoins = » +

//    sizeof(TestingExpressions));

int b1 = 1;

Console.WriteLine(«Тип переменной int b1: {0}, {1}»,

b1.GetType(), typeof(int));

Console.WriteLine(«Тип класса TestingExpressoins = {0}»,

typeof(TestingExpressions));

}//SizeMethod

 

В этом примере операция применяется к трем встроенным типам –bool, double, char. Попытка применить эту операцию к собственному классу приводит к ошибке компиляции и потому закомментирована.

Операция typeof с успехом применена как к собственному классу TestingExpressions, так и к встроенному классу int.

На рис. 3_1 приведены результаты вывода на консоль, полученные при вызове этого метода:

Рис. 3_1 Результаты выполнения операций sizeof и typeof

Операции «увеличить» и «уменьшить» (increment, decrement)

Операции увеличить на единицу и уменьшить на единицу могут быть префиксными и постфиксными. В справочной системе утверждается, что к высшему приоритету относятся постфиксные операции x++ и x—, что нашло отражение в таблице 3_1.Префиксные операции имеют на единицу меньший приоритет.

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

public void IncDec()

{

int n = 1, m = 0;

Console.WriteLine(«n = {0}», n);

m = n++ + ++n;

Console.WriteLine(«m = n++ + ++n = {0},n = {1}»,

m, n);

m = n++ + n + ++n;

Console.WriteLine(«m = n++ + n + ++n = {0},n = {1}»,

m, n);

m = ++n + n + n++;

Console.WriteLine(«m = ++n + n + n++ = {0},n = {1}»,

m, n);

}

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

Консольный вывод выполнения этого метода дает результат, показанный на рис 3_2:

Рис.3_2 Результат выполнения метода IncDec

Следует также заметить, что рассматриваемые операции применимы только к переменным, свойствам и индексаторам класса, то есть к выражениям, которым отведена область памяти. В языках C++ и C# такие выражения называются l-value, поскольку они могут встречаться в левых частях оператора присваивания. Как следствие, запись в C# выражения < —x++ > приведет к ошибке. Как только к x слева или справа приписана одна из операций, выражение перестает принадлежать к классу l-value выражений и вторую операцию приписать уже невозможно.

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

x++;   y—;

В этом случае фактически исчезает побочный эффект, являющийся опасным средством, и операции используются как краткая запись операторов:

x = x + 1; y = y – 1;

Унарные операции приоритета 1

Следующий по важности приоритет имеют унарные операции. Префиксные операции ++x и –x уже подробно рассмотрены. Арифметические унарные операции + и – не требуют особых пояснений. О логических унарных операциях отрицания, задаваемых знаками ! и ~ скажем чуть позже. А сейчас рассмотрим оставшуюся унарную операцию.

Операция кастинга – приведения к типу

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

(T)x

Здесь в скобках указывается тип, к которому следует привести выражение x. Нужно понимать, что не всегда существует явное приведение типа источника к типу цели T. Операция кастинга применима только для приведения типов внутри арифметического типа. С ее помощью один арифметический подтип можно привести к другому подтипу, но нельзя, например, целочисленные типы  привести к логическому типу bool.

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

byte b1 = 1, b2 = 2, b3;

//b3 = b1 + b2;

b3 = (byte)(b1 + b2);

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

В следующем фрагменте кода демонстрируется еще один пример приведения типа:

int tempFar, tempCels;

tempCels = -40;

tempFar = (int)(1.8 * tempCels) + 32;

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

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

//if ((bool)1) b3 = 100;

if (Convert.ToBoolean(1)) b3 = 100;

В этом примере показана попытка применить кастинг для приведения типа int  к типу bool. Такое преобразование типа с помощью операции кастинга не разрешается и приводит к ошибке на этапе компиляции. Но, заметьте, это преобразование можно выполнить более мощными методами класса Convert .

Проверяемые и непроверяемые блоки и выражения

У нас остались еще нерассмотренными две операции высшего приоритета – checked и unchecked. Начну с определения. Блок или выражение называется проверяемым (непроверяемым), если ему предшествует ключевое слово checked (unchecked). В проверяемых блоках контролируется вычисление арифметических операций и возникает исключительная ситуация, если, например, при вычислениях происходит переполнение разрядной сетки числа. В непроверяемых блоках такая исключительная ситуация будет проигнорирована, и вычисления продолжатся с неверным результатом.

Слегка модифицируем выше приведенный пример:

byte b1 = 100, b2 = 200, b3;

//b3 = b1 + b2;

b3 = (byte)(b1 + b2);

Если в предыдущем примере с байтами все вычисления были корректны, то теперь результат вычисления b3 просто не верен. При сложении теряется старший разряд со значением 256, и b3 вместо 300 получит значение 44 из диапазона, допустимого для типа byte.  Плохо, когда при выполнении программы возникает исключительная ситуация и программа не может далее нормально выполняться. Но еще хуже, когда программа завершает свою работу, выдавая неправильные результаты. Ложь хуже отказа. Кто виноват в возникшей ситуации? Программист, поскольку именно он разрешил опасную операцию, не позаботился о ее контроле и обработке исключительной ситуации в случае ее возникновения. Программист должен знать, что по умолчанию вычисления выполняются в режиме unchecked.  А потому, если нет полной уверенности в  возможности проведения преобразования, то запись опасных преобразований должна сопровождаться введением проверяемых выражений, охраняемых блоков и сопровождающих их обработчиков исключительных ситуаций. Вот как может выглядеть корректно построенный код:

public void Days()

{

byte hotDays = 0, coldDays = 0, hotAndCold = 0;

const string HOW_HOT =

«Сколько жарких дней в этом году? (выше +25 градусов)»;

const string HOW_COLD =

«Сколько холодных дней в этом году? (ниже -25 градусов)»;

const string HOW_HOT_AND_COLD =

«В этом году жарких и холодных дней было «;

const string MESSAGE_ERROR =

«Данные не соответствуют типу в методе Days!»;

 

try

{

Console.WriteLine(HOW_HOT);

hotDays = byte.Parse(Console.ReadLine());

Console.WriteLine(HOW_COLD);

coldDays = byte.Parse(Console.ReadLine());

hotAndCold = checked((byte)(hotDays + coldDays));

Console.WriteLine(HOW_HOT_AND_COLD +

hotAndCold.ToString());

}

catch (OverflowException)

{

Console.WriteLine(MESSAGE_ERROR);

}

}

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

  Арифметические операции

В языке C# имеются обычные для всех языков арифметические операции – «+, -, *, /, %». Все они перегружены. Операции «+» и «-» могут быть унарными и бинарными. Унарные операции приписывания знака арифметическому выражению имеют наивысший приоритет среди арифметических операций. К следующему приоритету относятся арифметические операции типа умножения, к которому относятся три операции  - умножения, деления и взятия остатка. Все эти операции перегружены и определены для разных подтипов арифметического типа. Следует однако помнить, что арифметические операции не определены над короткими числами (byte, short) и начинаются с типа int.

Операция деления «/» над целыми типами осуществляет деление нацело, для типов с плавающей и фиксированной точкой обычное деление. Операция «%» возвращает остаток от деления нацело и определена не только над целыми типами, но и над типами с плавающей точкой. Тип результата зависит от типов операндов. Приведу пример вычислений с различными арифметическими типами:

/// <summary>

/// Арифметические операции

/// </summary>

public void Ariphmetica()

{

byte b1 = 7, b2 = 3, b3;

b3 = (byte)(b1 / b2);

int n = -7, m = 3, p, q, r;

p = n / m; q = n % m; r = p*m + q;

Console.WriteLine(«Операции над типом int»);

Console.WriteLine(

«n = {0}, m = {1}, p = n/m = {2}, » +

«q = n % m = {3}, r = p*m + q = {4}»,

n, m, p, q, r);

 

Console.WriteLine(«Операции над типом double»);

double x = 7.5, y = 3.5, u, v, w;

u = x / y; v = u * y;

w = x % y;

Console.WriteLine(

«x = {0}, y = {1}, u = x/y = {2}, » +

«v = u*y = {3}, w = x % y = {4}»,

x, y, u, v, w);

 

Console.WriteLine(«Операции над типом decimal»);

decimal d1 = 7.5M, d2 = 3.5M, d3, d4, d5;

d3 = d1 / d2; d4 = d3 * d2;

d5 = d1 % d2;

Console.WriteLine(

«d1 = {0}, d2 = {1}, d3 = d1/d2 = {2}, » +

«d4 = d3*d2 = {3}, d5 = d1 % d2 = {4}»,

d1, d2, d3, d4, d5);

}//Ariphmetica

Результаты вычислений при вызове этого метода показаны на рис. 3_3.

Рис. 3_3 Результаты работы метода Ariphmetica

Для целых типов можно исходить из того, что равенство n = (n/m)*m+n%m истинно. Для типов с плавающей точкой выполнение точного равенства x = (x/y)*y следует считать скорее случайным, а не закономерным событием. Законно невыполнение этого равенства, как это имеет место при вычислениях с фиксированной точкой.

Вычисление выражений

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

Память и время – два основных ресурса.

В распоряжении программиста при решении задач есть два основных ресурса – это память компьютера и его быстродействие. Кажется, что оба эти ресурса практически безграничны, и потому можно не задумываться о том, как  они тратятся. Эти представления иллюзорны. Многие задачи, возникающие на практике, таковы, что имеющихся ресурсов не хватает и требуется жесткая их экономия. Вот два простых примера. Если в программе есть трехмерный массив A: double[,,]; A = new double[n,n,n],то уже при n =1000 оперативной памяти современных компьютеров не хватит для хранения элементов этого массива. Если приходится решать задачу, подобную задаче о «ханойской башне», где время решения задачи T = O(2n), то уже при n = 64 никакого быстродействия всех современных компьютеров не хватит для решения этой задачи в сколь либо допустимые сроки. Программист обязан уметь оценивать объем ресурсов, требуемых программе.

Говоря о ресурсах, требуемых программе P, часто используют термины – временная и емкостная сложность – T(P) и V(P). Выражения представляют хорошую начальную базу для оценивания этих характеристик.

Характеристики T(P) и V(P) обычно взаимосвязаны. Увеличивая расходы памяти, можно уменьшить время решения задачи, или, выбирая другое решение, сократить расходы памяти, увеличивая время работы. Одна из реальных задач, стоящих перед профессиональным программистом – это нахождение нужного компромисса между памятью и временем. Помните:

«Выбора тяжко бремя – память или время!»

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

Именованные константы

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

«Каждой константе имя давайте,

Числа без имени из программ изгоняйте!»

Исключением могут быть простые константы – 0, 1, 2, 3. Если, как это часто бывает, изменяется значение константы, то это изменение должно делаться только в одном месте, там, где эта константа определяется. Введение констант уменьшает время вычислений, поскольку константы, заданные выражениями, вычисляются еще на этапе компиляции.

Рассмотрим в качестве примера вычисление значений переменных x и y, заданных следующими выражениями:

 

 

Вычислять эти выражения, точно следуя приведенной записи, не следует. Вот как можно организовать эти вычисления:

public void EvalXY(double a, out double x, out double y)

{

const double C1 = 53.5 * 33 / (37 * 37);

const double C2 = 133 + C1, C3 = 1.0 / 3;

double t1 = a + C1, t2 = a — C1;

x = t1 * t2 / Math.Pow(C2, C3);

y = t1 / t2;

}

Заметьте, константы будут вычислены еще на этапе компиляции, так что для вычисления выражений потребуется 5 арифметических операций и один вызов стандартной функции. Выигрыш кажется незначительным при тех скоростях, которыми обладают компьютеры. Но стоит учесть, что метод EvalXY может вызываться многократно. Но главное даже не выигрыш во времени вычислений. Более важно, что запись выражения становится простой и позволяет легко обнаруживать ошибки записи.

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

x = t1 * t2 / Math.Pow(133 + C1, 1 / 3)

Надеюсь, что читатель ошибку видит, но на всякий случай поясню, что ошибка связана с  вычислением второго аргумента  функции возведения в степень Pow. Здесь применяется операция деления, операнды которой целые числа, потому результат деления нацело будет равен нулю. Обнаружить ошибку студенты могут далеко не сразу. В процедуре EvalXY ошибка обнаруживается мгновенно, стоит только взглянуть на значения констант, вычисленных еще на этапе компиляции.

Операции отношения

Операции отношения стоит просто перечислить, в объяснениях они не нуждаются. Всего операций 6  (==, !=, <, >, <=, >= ), все они возвращают результат логического типа bool.  Операции перегружены, так что их операнды могут быть разных типов. Понятно, что перед  вычислением отношения может потребоваться преобразование типа одного из операндов. Понятно, что  не всегда возможны неявные преобразования, гарантирующие возможность выполнения сравнения.  Возникнет ошибка на этапе компиляции в выражении:

1 > «1″

Задав явное преобразование типа для одного из операндов, это отношение можно вычислить. Следует обратить внимание на запись отношения эквивалентности, задаваемое двумя знаками равенства. Типичной ошибкой является привычная для математики запись:

if(a = b)

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

Операции проверки типов

Операции проверки типов is и as будут рассмотрены в последующих лекциях.

Операции сдвига

Операции сдвига вправо “>>” и сдвига влево “<<” в обычных вычислениях применяются редко. Они особенно полезны, если данные рассматриваются, как строка битов. Результатом операции является сдвиг строки битов влево или вправо на K разрядов. В применении к обычным целым положительным числам сдвиг вправо равносилен делению нацело на 2K, а сдвиг влево – умножению на 2K. Для отрицательных чисел сдвиг вправо и деление дают разные результаты, отличающиеся на 1. В языке C# операции сдвига определены только для некоторых целочисленных типов – int, uint, long, ulong. Величина сдвига должна иметь тип int. Вот пример применения этих операций:

/// <summary>

///операции сдвига

/// </summary>

public void Shift()

{

int n = 17,m =3, p,q;

p= n>>2; q = m<<2;

Console.WriteLine(«n= » + n + «; m= » +

m + «; p=n>>2 = «+p + «; q=m<<2 » + q);

long x=-75, y =-333, u,v,w;

u = x>>2; v = y<<2; w = x/4;

Console.WriteLine(«x= » + x + «; y= » +

y + «; u=x>>2 = «+u + «; v=y<<2 » + v +

«; w = x/4 = » + w);

}//Shift

Логические операции

Логические операции в языке C# делятся на две категории, — одни выполняются только над операндами типа bool, другие – как над булевскими, так и целочисленными операндами.

Логические операции над булевскими операндами

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

Следующая по приоритету бинарная операция (x && y) называется конъюнкцией, операцией «И» или логическим умножением. Она возвращает значение true в том и только в том случае, когда оба операнда имеют значение true. В остальных случаях – возвращается значение false .

Следующая по приоритету бинарная операция (x || y) называется дизъюнкцией, операцией «ИЛИ» или логическим сложением. Она возвращает значение false в том и только в том случае, когда оба операнда имеют значение false. В остальных случаях – возвращается значение true.

Когда описывается семантика операций, то молчаливо предполагается, что операнды операции определены. Подразумевается, что результат операции не определен, если не определен хотя бы один из ее операндов. Это утверждение верно почти для всех операций языка C#. К исключениям относятся рассматриваемые нами логические операции && и ||. Эти операции называются условными логическими операциями. Если первый операнд операции конъюнкции && ложен, то второй операнд не вычисляется и результат операции равен false, даже если второй операнд не определен. Аналогично, если первый операнд операции дизъюнкции || истинен, то при выполнении этого условия второй операнд не вычисляется и результат операции равен true, даже если второй операнд не определен.

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

//Условное And — &&

public int SearchPattern(int[] arr, int pattern)

{

int result = -1, index = 0;

int n = arr.Length;

while (index < n && arr[index] != pattern) index++;

if (index != n) result = index;

return (result);

}

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

  Логические операции над булевскими операндами и целыми числами. Работа со шкалами

Рассмотрим логические операции, которые могут выполняться не только над булевскими значениями, но и над целыми числами. Высший приоритет среди этих операций имеет унарная операция отрицания ( ~x). Заметьте, есть две операции отрицания, одна из них (!x) определена только над операндами булевского типа, другая (~x) – только над целочисленными операндами.

Говоря о логических операциях над целыми числами, следует понимать, что целые числа можно рассматривать как последовательность битов (разрядов). Каждый бит, имеющий значение 0 или 1, можно интерпретировать как логическое значение обычным образом: 0 соответствует false, 1 – true. Логическая операция, применяемая к операндам одного и того же целочисленноготипа, выполняется над соответствующими парами битов, создавая результат в виде последовательности битов и интерпретируемый как целое число. По этой причине такие логические операции называются побитовыми или поразрядными операциями.

Бинарных побитовых логических операций три  (& , ^ , |). В порядке следования приоритетов это конъюнкция (операция «И»), исключающее ИЛИ, дизъюнкция (операция «ИЛИ»). Они определены как над целыми типами выше int, так и над булевыми типами. В первом случае они используются как побитовые операции, во втором – как обычные логические операции.  Когда эти операции выполняются над булевскими операндами, то оба операнда вычисляются в любом случае, и, если хотя бы один из операндов не определен, то и результат операции будет не определен. Когда необходима такая семантика логических операций, тогда без этих операций не обойтись.

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

Шкалы

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

Шкала позволяет экономно задавать информацию об объекте, а побитовые операции позволяют весьма эффективно эту информацию обрабатывать. Поскольку эти операции определены над типами int, uint, long, ulong, то это означает, что C# позволяет работать со шкалами длины 32 и 64.

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

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

// <summary>

/// Свойства претендентов на должность программиста,

/// описывающие знание технологий и языков программирования

/// </summary>

public enum Prog_Properties

{

VB = 1, C_sharp = 2, C_plus_plus = 4,

Web = 8, Prog_1C = 16

}

Заметьте, при определении перечисления можно указать, на какое значение целого типа проецируется значение из перечисления. Если проектировать i-е значение на i-й разряд целого числа (2i-1), как это сделано в примере, то переменные перечисления будут задавать шкалу свойств.

Свойства каждого претендента на должность характеризуются своей шкалой, которую можно рассматривать как переменную типа Prog_Properties. Задать шкалу претендента можно целым числом в интервале от 0 до 2n -1, приведя значение к нужному типу. Например, так:

Prog_Properties candidate1 = (Prog_Properties)18;

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

Prog_Properties candidate2 = Prog_Properties.C_sharp |

Prog_Properties.C_plus_plus | Prog_Properties.Web;

Логические операции над шкалами позволяют эффективно реализовывать различные запросы, отбирая из массива кандидатов тех, кто соответствует заданным требованиям. Пусть например cand[i] – шкала i-го кандидата, а pattern – шкала, задающая набор требований, предъявляемых к кандидатам. Рассмотрим условие:

(cand[i] & pattern) == pattern

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

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

Я написал отдельный класс Scales для работы со шкалами и перечислением Prog_Properties. Приведу несколько методов этого класса, позволяющие выполнять различные запросы к кандидатам.

/// <summary>

/// Список кандидатов, обладающих

/// свойствами, заданных образцом.

/// </summary>

public ArrayList CandsHavePat()

{

ArrayList temp = new ArrayList();

for (int i = 0; i < n; i++)

if ((cand[i] & pattern) == pattern)

temp.Add(«cand[" + i + "]«);

return temp;

}

/// <summary>

/// Список кандидатов, не обладающих

/// всеми свойствами, заданными образцом.

/// </summary>

public ArrayList CandsHaveNotAllPat()

{

ArrayList temp = new ArrayList();

for (int i = 0; i < n; i++)

if ((~cand[i] & pattern) == pattern)

temp.Add(«cand[" + i + "]«);

return temp;

}

/// <summary>

/// Список кандидатов, обладающих

/// некоторыми свойствами, заданными образцом.

/// </summary>

public ArrayList CandsHaveSomePat()

{

ArrayList temp = new ArrayList();

for (int i = 0; i < n; i++)

{

currentScale = cand[i] & pattern;

if (currentScale > 0 && currentScale < pattern)

temp.Add(«cand[" + i + "]«);

}

return temp;

}

/// <summary>

/// Список кандидатов, обладающих

/// только свойствами, заданными образцом.

/// </summary>

public ArrayList CandsHaveOnlyPat()

{

ArrayList temp = new ArrayList();

for (int i = 0; i < n; i++)

if (((cand[i] & pattern) == pattern) &&

((cand[i] & ~pattern) == 0))

temp.Add(«cand[" + i + "]«);

return temp;

}

Все эти методы устроены одинаково. Они отличаются условием отбора в операторе if, которое включает побитовые логические операции, выполняемые над шкалами cand и pattern, объявленными как массив переменных и простая переменная перечислимого типа Prog_Properties.  В качестве результата выполнения запроса возвращается массив  типа ArraList, содержащий кандидатов, удовлетворяющих условиям запроса. На рис. 3_4 показаны результаты работы консольного приложения, в котором используется созданный класс, и вызываются приведенные выше методы этого класса.

Рис. 3_4 Результаты запросов над шкалами

 

Условное выражение

В C#, как и в C++, разрешены условные выражения. Конечно, без них можно обойтись, заменив их условным оператором. Вот простой пример их использования, поясняющий синтаксис их записи:

//Условное выражение

int a = 7, b = 9, max;

max = (a > b) ? a : b;

Условное выражение начинается с условия, заключенного в круглые скобки, после которого следует знак вопроса и пара выражений, разделенных двоеточием « : ». Условие задается выражением типа bool. Если оно истинно, то из пары выражений выбирается первое, в противном случае результатом является значение второго выражения. В данном примере переменная max получит значение 9.

Заметьте, условное выражение является примером тернарного выражения – выражения с тремя операндами. И здесь, как и в случае условных логических операций не требуется, чтобы все операнды были определены. Если булевский операнд определен и имеет значение true, то вычисляется второй операнд, а третий операнд не вычисляется и может быть в этот момент не определен.

Операция присваивания

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

Начнем с формального определения синтаксиса выражения присваивания:

<переменная> <знак присваивания> <выражение>

Знаков присваивания много, они перечислены в таблице 3_1. Чаще всего используется знак равенства, но иногда ему могут предшествовать и другие знаки операций. С чем связано наличие многих знаков у одной операции? Языку C# это досталось в наследство от языка С++, где авторы языка были большими любителями краткости записи в ущерб ее понимаемости. Поэтому в языке допустимы такие выражения как x++, x+=y, мало понятные обычному математику. Второе из этих выражений является выражением присваивания и удовлетворяет выше приведенному синтаксису со знаком присваивания +=. Его можно рассматривать как краткую запись выражения x = x + y. Аналогичный смысл имеют и другие знаки присваивания – (*=,  /=  и другие).

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

/// <summary>

/// Множественное присваивание

/// </summary>

static void AssignTest()

{

double x = 1, y = 2, z = 3, w =9, u = 7, v = 5;

if((x += y -= z += w *= (u + v) / (u — v)) < 0)

Console.WriteLine(«x = {0}, y = {1}, z = {2},» +

«w = {3}, u = {{4}, v = {5}», x, y, z, w, u, v);

}

В операторе if записано выражение, задающее множественное присваивание. Какова семантика, как вычисляются выражения присваивания?

Операция присваивания является правосторонней операцией, и особенностью вычисления выражения присваивания является то, что оно вычисляется справа налево.  В нашем примере вначале будет вычислено самое правое выражение (u + v) / (u – v), значение которого будет равно 6. Двигаясь налево по ходу присваивания, значение выражения будет изменяться. Последним будет вычислено выражение, которое получит переменная x. Значение этого выражения равно -54, и именно оно является окончательным значением выражения множественного присваивания и будет участвовать в сравнении с нулем. Условие в операторе if получит значение true и метод WriteLine выведет на консоль значения переменных, полученных ими как побочный результат вычисления выражения присваивания. Эти значения соответственно равны : -54, -55, 57, 54, 7, 5. Заметьте, скобки, окружающие выражение присваивания необходимы, иначе операция сравнения выполнялась бы до присваивания, что приводило бы к ошибке.

Для пояснения деталей семантики выражений присваивания использована довольно экзотическая конструкция в операторе if.  В реальных программах такие конструкции использовать не следует. Они «от лукавого». Простота записи и понимаемость  — одни из главных критериев при создании промышленного кода. При изучении возможностей языка допустимо рассмотрение экзотических случаев.

Операция ?? – новая операция C# 2.0

Эта операция уже рассматривалась в предыдущей главе, когда речь шла о типах, допускающих значение null. Напомню, все ссылочные типы изначально допускают null в качестве возможного значения. Такое значение ссылочной переменной задает неопределенную ссылку, ссылку на не существующий объект. Значимые типы значения null не содержат, но можно определить расширенный значимый тип, включающий значение null. Синтаксически, если T – имя значимого типа, то T? – это имя расширенного типа. Операция ?? определена над операндами, допускающими значение null. Ее главное назначение – присвоить переменной значение, отличное от null, поэтому иногда ее называют операцией склеивания, поскольку она позволяет «приклеить» к null значение. Рассмотрим ее определение:

A ?? B

Если операнд A отличен от null, то он и возвращается в качества результата операции. Если же он имеет значение null, то результатом является операнд B. Эту операцию особенно удобно использовать при приведении типа T? К типу T. Рассмотрим простой пример:

int? x = null;

int y = x ?? 0;

Заметьте, если между двумя присваиваниями переменная x не приобрела значение, отличное от null, то переменная y в результате получит значение 0.

В отсутствии такой операции нам пришлось бы писать для вычисления у такую эквивалентную конструкцию:

int y = (x !=null) ? (int)x : 0

Лямбда оператор — новая операция в C# 3.0

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

<(список входных аргументов)>  =>  <выражение>

Содержательно, выражение в правой части задает описание функции, аргументы которой задаются списком левой части. Такое описание представляет собой описание анонимной функции – функции без имени, и может быть использовано, например, при задании экземпляра делегата. Зачастую функция зависит от одного аргумента, и тогда в левой части можно указывать только имя этого аргумента, опуская скобки.

Подробно этот механизм будет рассмотрен в отдельной главе нашего курса, а пока приведем первый простой пример использования этой операции. Рассмотрим следующую задачу. Пусть дан массив чисел X и задана функция F(x). Требуется найти минимальное значение этой функции, когда аргументы задаются элементами массива X. Конечно же, можно создать метод, реализующий вычисление функции F(x), но можно воспользоваться анонимной функцией, заданной лямбда выражением, что демонстрирует следующий пример:

/// <summary>

/// Лямбда оператор и лямбда выражение

/// </summary>

static void Lambda()

{

Random rnd = new Random();

const int size = 5;

int[] numbers = new int[size];

int a = rnd.Next(-10, 10), b = rnd.Next(-10, 10),

c = rnd.Next(-10, 10);

Console.WriteLine(«a={0}, b={1}, c={2}»,

a, b, c);

for (int i = 0; i < size; i++)

{

numbers[i] = rnd.Next(10);

Console.Write(«X[{0}] = {1}, «, i, numbers[i]);

}

int minValue = numbers.Min(x => a * x * x + b * x + c);

Console.WriteLine(«Min(a*x^2 +b*x + c = {0}», minValue);

 

}

Большая часть в этом пример связана с моделированием массива чисел и коэффициентов функции. Нахождение минимума функции задается одной строкой:

int minValue = numbers.Min(x => a * x * x + b * x + c);

Здесь функция Min последовательно перебирает элементы массива, формируя аргумент x функции, а лямбда выражение преобразует его в значение функции от этого аргумента. В результате возвращается минимальное значение функции. Результаты работы метода Lambda можно видеть на рис. 3_5:

Рис. 3_5 Результаты работы метода Lambda

На этом закончим рассмотрение операций языка C#, но продолжим рассмотрение некоторых вопросов, связанных с вычислением выражений.

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

Арифметический тип распадается на 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&gpgt;t;

/// <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

Рис. 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, аргументом которого является строка, а возвращаемым результатом объект соответствующего типа. Метод явно выполняет преобразование текстового представления в тот тип данных, который был целью вызова статического метода. Понятно, что строка, представляющая аргумент вызова, должна соответствовать представлению данных соответствующего типа. Для целочисленных типов строка должна содержать последовательность цифр с возможно предшествующим знаком. Для вещественных типов в записи числа могут присутствовать целая и дробная части, разделенные знаком запятой. Для булевских типов слова true или false.

Если представление данных не соответствует требованиям типа, то в ходе выполнения метода возникнет исключительная ситуация.  Приведу пример преобразования данных, выполняемых с использованием метода 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 он вызывается автоматически.

Класс Convert и его методы

Для преобразований внутри арифметического типа можно использовать кастинг — приведение типа. Для преобразований строкового типа в скалярный тип можно использовать метод Parse,  а в обратную сторону – метод ToString.

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

Среди других методов класса Convert отмечу общий статический метод ChangeType, позволяющий преобразование объекта к некоторому заданному типу. Отмечу также возможность преобразования к системному типу DateTime, который хотя и не является базисным типом языка C#, но допустим в программах, как и любой другой системный тип.

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

Методы класса Convert поддерживают общий способ выполнения преобразований между типами. Класс Convert содержит 15 статических методов вида To<Type> (ToBoolean(),…ToUInt64()), где Type может принимать значения от Boolean до UInt64 для всех встроенных типов, перечисленных в таблице 3-1. Единственным исключением является тип object, – метода ToObject нет по понятным причинам, поскольку для всех типов существует неявное преобразование к типу object. Каждый из этих 15 методов перегружен и его аргумент x может принадлежать к любому из упомянутых  типов. С учетом перегрузки с помощью методов этого класса можно осуществить любое из возможных преобразований одного типа в другой. Все методы осуществляют проверяемые преобразования и включают исключительную ситуацию всякий раз, когда преобразование осуществить невозможно или при выполнении преобразования происходит потеря точности. Приведу пример:

/// <summary>

/// Тестирование методов класса Convert

/// </summary>

public void ConvertTest()

{

string s;

byte b;

int n;

double x;

bool flag;

char sym;

DateTime dt;

sym = ’7′;

s = Convert.ToString(sym);

x = Convert.ToDouble(s);

n = Convert.ToInt32(x);

b = Convert.ToByte(n);

flag = Convert.ToBoolean(b);

x = Convert.ToDouble(flag);

s = Convert.ToString(flag);

// sym = Convert.ToChar(flag);

 

s = «300″;

n = Convert.ToInt32(s);

//b = Convert.ToByte(s);

 

s =»14.09″;

//flag = Convert.ToBoolean(s);

//x = Convert.ToDouble(s);

 

s = «14.09.2008″;

dt = Convert.ToDateTime(s);

}

Этот пример демонстрирует различные преобразования между типами. Все эти преобразования опасные, выполняются явно с использованием методов класса Convert. Вначале данные символьного типа преобразуются в строку. Затем эти данные преобразуются в вещественный тип, затем проводятся преобразования внутри арифметического типа с понижением типа от double до byte. Завершающим пример преобразованием является преобразование данных строкового типа к типу DateTime.

Опасные преобразования одного типа к другому могут успешно выполняться над некоторыми данными и приводить к ошибке с другими данными. В нашем примере закомментированы операторы, приводящие к ошибкам в период выполнения. Первая ошибка возникает при попытке преобразовать данные булевского типа к символьному типу, поскольку отсутствует преобразование, которое значение true преобразовывало бы в некоторый символ (например Т). Заметьте, в предыдущих операторах эти же данные успешно были приведены к строковому и арифметическому типу. Следующая ошибка возникает при попытке преобразовать строку со значением 300 к типу byte. Соответствующий метод распознает, что значение, записанное в строке, слишком велико для типа, представляющего цель преобразования. Еще одна ошибка возникает при попытке привести к булевскому типу строку, отличную от записи булевских констант true и false. Последняя ошибка в данном примере довольно часто встречается на практике. Она связана с тем, что ошибочно использована точка вместо запятой для отделения дробной части числа.

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

Класс Console и его методы

В заключение этой главы рассмотрим работу с методами класса Console. Хотя этот класс не связан непосредственно с выражениями – основной темой данной главы, но он имеет прямое отношение к преобразованиям типов данных и вводу-выводу данных. Без использования методов этого класса в консольных проектах не обойтись.

Класс Console используется в консольных проектах, позволяя вводить исходные данные с консоли и выводить результаты на консоль. По умолчанию при вводе с консоли данные вводятся с  клавиатуры и отображаются на дисплее, при выводе  на консоль – данные отображаются на экране дисплея. У класса Console десятки свойств и методов. Ограничимся рассмотрением основных методов, используемых при вводе и выводе.

Ввод данных с консоли. Методы Read и ReadLine

Методы Read и ReadLine позволяют читать текст с консоли, отображаемый на экране дисплея компьютера.  Методы не имеют входных аргументов. Оператор Read читает по одному символу из входной строки и возвращает в качестве результата код прочитанного символа, имеющий тип int. Посимвольный ввод применяется довольно редко. Вот некоторый пример возможного применения чтения текста с использованием оператора Read.

public void ReadTest()

{

Console.WriteLine(«Введите текст.» +

«Признаком конца текста являются два подряд идущих !! «);

char ch = Convert.ToChar(Console.Read());

char next =’ ‘;

string result = «»;

bool finish = false;

do

{

result += ch.ToString();

next = Convert.ToChar(Console.Read());

if (ch != ‘!’)

ch = next;

else

{

if (next == ‘!’) finish = true;

else ch = next;

}

}while (!finish);

Console.ReadLine();

Console.WriteLine(result);

}

В этом примере текст, введенный пользователем, читается посимвольно до тех пор, пока не встретится специальный признак конца чтения. В данном случае таким признаком является два подряд идущих восклицательных знака.

Вызов метода Readline, завершающий процедуру, позволяет «дочитать» оставшиеся символы отображаемой строки текста и перевести курсор ввода на новую строку. Такие символы всегда будут, поскольку всякая строка завершается символом конца строки, не прочитанным методом Read. На рис. 3_8 показаны результаты работы при вызове процедуры ReadTest.

Рис. 3_8 Результаты работы метода ReadTest

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

Вывод данных на консоль. Методы Write и WriteLine

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

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

Строка называется форматированной, если она содержит форматы. Формат, включаемый в строку, задается последовательностью символов, заключенной в фигурные скобки. Каждый формат задает место подстановки. В процессе форматизации в строку вместо формата подставляется некоторая другая строка. Форматы могут быть разными, и подробнее о них поговорим при описании работы со строками. В простейшем случае задания формата в фигурных скобках стоит целое число k. Это число определяет порядковый номер аргумента из дополнительного списка, при этом нумерация аргументов списка начинается с нуля. Аргумент с номером k из дополнительного списка преобразуется в строку и подставляется вместо соответствующего формата. Преобразование аргумента в строку происходит автоматически, используя метод ToString, который имеют все типы данных.

Рассмотрим применение методов Write, WrteLine, ReadLine на примере ввода и вывода с консоли квадратной матрицы:

/// <summary>

/// Ввод — вывод с консоли квадратной матрицы

/// </summary>

public void InOutMatrix()

{

int n;

Console.WriteLine(«Ввод квадратной матрицы A размерности n»);

Console.WriteLine(«Введите целое — n»);

n = Convert.ToInt32(Console.ReadLine());

double[,] A = new double[n,n];

for(int i = 0; i<n; i++)

for (int j = 0; j < n; j++)

{

Console.WriteLine(«Введите A[{0}],[{1}]«, i, j);

A[i, j] = Convert.ToDouble(Console.ReadLine());

}

Console.WriteLine(«Вывод матрицы A»);

for (int i = 0; i < n; i++)

{

for (int j = 0; j < n; j++)

Console.Write(«A[{0}],[{1}] = {2}  «, i, j, A[i, j]);

Console.WriteLine();

}

}

На рис. 3_9 показаны результаты вызова этого метода.

Рис. 3_9 Результаты работы метода InOutMatrix

Задачи

  1. Построить циклическое консольное меню-приложение «Арифметические операции».  Команды меню задают арифметические операции, допустимые в выражениях языка C#. При выборе пункта меню пользователь получает приглашение к вводу одного или двух значений в зависимости от выбранного пункта меню, затем над значениями выполняется соответствующая операция, и ее результат выводится на консоль.
  2. Построить Windows-приложение «Арифметические операции». В одно или два текстовых окна пользователь вводит значения. По нажатию командной кнопки, задающей тип арифметической операции, над введенными значениями выполняется соответствующая операция, и ее результат выводится  в текстовое окно, предназначенное для вывода значений.
  3. Построить циклическое консольное меню-приложение «Логические операции».  Команды меню задают логические и условные логические операции, допустимые в выражениях языка C#. При выборе пункта меню пользователь получает приглашение к вводу одного или двух значений в зависимости от выбранного пункта меню, затем над значениями выполняется соответствующая операция, и ее результат выводится на консоль. В зависимости от типа операции значениями могут быть как логические, так и целочисленные константы.
  4. Построить Windows-приложение «Логические операции». В одно или два текстовых окна пользователь вводит значения, которые могут быть логическими или целочисленными константами. По нажатию командной кнопки, задающей тип логической или условной логической операции, над введенными значениями выполняется соответствующая операция, и ее результат выводится  в текстовое окно, предназначенное для вывода значений.
  5. Построить циклическое консольное меню-приложение «Операции отношения и сдвига».  Команды меню задают операции отношения или сдвига, допустимые в выражениях языка C#. При выборе пункта меню пользователь получает приглашение к вводу значений, затем над значениями выполняется соответствующая операция, и ее результат выводится на консоль.
  6. Построить Windows-приложение «Операции отношения и сдвига». В текстовые окна пользователь вводит значения операндов операции. По нажатию командной кнопки, задающей операцию отношения или сдвига, над введенными значениями выполняется соответствующая операция, и ее результат выводится  в текстовое окно, предназначенное для вывода значений.
  7. Построить циклическое консольное меню-приложение «Класс Math».  Команды меню задают функции, определенные в классе Math. При выборе пункта меню пользователь получает приглашение к вводу значений, затем к значениям применяется соответствующая функция, и ее результат выводится на консоль.
  8. Построить Windows-приложение «Класс Math». В одно или два текстовых окна пользователь вводит значения. По нажатию командной кнопки, задающей функцию класса Math, к введенным значениям применяется соответствующая функция, и ее результат выводится  в текстовое окно, предназначенное для вывода значений.

Вычисление выражений. Оценка времени вычислений

Вычисление выражения построить так, чтобы минимизировать время его вычисления. Оцените время вычисления выражения в условных единицах (уе), исходя из следующих предположений: присваивание – 1 уе, операции сдвига –2 уе, сложение, вычитание – 3 уе, умножение – 5 уе, деление – 7 уе, вызов стандартной функции – 13 уе.
Проверьте корректность вычисления значения выражения. Поочередно изменяйте значения числовых констант, участвующих в выражении, например замените 527 на 526, 85. Если изменения требуется вносить в нескольких местах программного текста, то подумайте о более разумном способе записи этого выражения.

  1. Построить консольное приложение «Expression1». Приложение вычисляет значение x и выводит его на консоль, где
  2. Построить Windows-приложение «Expression1». Приложение вычисляет значение x и выводит его в текстовое окно, где
  3. Построить консольное приложение «Expression2». Приложение вычисляет значение x и выводит его на консоль, где
  4. Построить Windows-приложение «Expression2». Приложение вычисляет значение x и выводит его в текстовое окно, где
  5. Построить консольное приложение «Expression3». Приложение вычисляет значение x и выводит его на консоль, где
  6. Построить Windows-приложение «Expression3». Приложение вычисляет значение x и выводит его в текстовое окно, где
  7. Построить консольное приложение «Expression4». Приложение вычисляет значение x и выводит его на консоль, гдеВ вычисляемом выражении m, n, p, a, b, c, d, e – это имена переменных, значения которых задает пользователь.
  8. Построить Windows-приложение «Expression4». Приложение вычисляет значение x и выводит его в текстовое окно, гдеВычисление выражения построить так, чтобы минимизировать время его вычисления. В вычисляемом выражении m, n, p, a, b, c, d, e – это имена переменных, значения которых задаются в соответствующих текстовых окнах.
  9. (**) Построить консольное приложение «Expression5». Приложение вычисляет значение x и время T в миллисекундах и тиках, требуемое для n-кратного (n =100000) его вычисления, гдеДля вычисления времени использовать возможности класса DateTime. Вычисление выражения построить разными способами. Проанализировать, как это влияет на эффективность вычислений по времени.
  10. (**) Построить Windows-приложение «Expression5». Приложение вычисляет значение x и время T в миллисекундах и тиках, требуемое для n-кратного (n =100000) его вычисления, где
    Для вычисления времени использовать возможности класса DateTime. Вычисление выражения построить разными способами. Проанализировать, как это влияет на эффективность вычислений по времени.

Геометрические фигуры

  1. Построить Windows-приложение «Круг». Дано: r – радиус круга, alpha – центральный угол в градусах. Вычислить: диаметр, длину окружности, площадь круга, Площадь сектора, площадь сегмента и длину хорды, определяемую центральным углом.
  2. Построить  Windows-приложение «Квадрат». Дано: сторона квадрата – a. Точки B и C расположены на сторонах квадрата, примыкающих к вершине квадрата A. Расстояние AB = b, AC = c. Вычислить: площадь четырехугольника OBAC, где О – центр квадрата. Вычислить OB,   OC  и углы четырехугольника.
  3. Построить  Windows-приложение «Треугольник». Дано: стороны треугольника a,b,c. Вычислить остальные элементы треугольника.
  4. Построить Windows-приложение «Круг и Точка». Дано: r – радиус круга, Xs, Ys – координаты центра круга, Xt, Yt – координаты точки. Определить, принадлежит ли  точка кругу.
  5. Построить  Windows-приложение «Квадрат и Точка ». Дано: сторона квадрата – a, Xs, Ys – координаты центра квадрата, Xt, Yt – координаты точки. Определить, принадлежит ли  точка квадрату.

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

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

  1. Постройте Windows приложение, в котором тип источника – string, тип цели – один из подтипов арифметического типа, выбираемый из списка. Преобразование выполните с использованием метода Parse и методом класса Convert.
  2. Постройте Windows приложение, в котором тип источника – string, тип цели – логический.  Преобразование выполните с использованием метода Parse и методом класса Convert.
  3. Постройте Windows приложение, в котором тип источника – int, тип цели – один из подтипов арифметического типа, выбираемый из списка. Преобразование выполните с использованием приведения типа, методом Parse и методом класса Convert.
  4. Постройте Windows приложение, в котором тип источника – double, тип цели – один из подтипов арифметического типа, выбираемый из списка. Преобразование выполните с использованием приведения типа, методом Parse и методом класса Convert.
  5. Постройте Windows приложение, в котором тип источника – int, тип цели – char. Преобразование выполните с использованием приведения типа, методом Parse и методом класса Convert.
  6. Постройте Windows приложение, в котором тип источника – int, тип цели – все типы, для которых существует безопасное преобразование, выполняемое автоматически.
  7. Постройте Windows приложение, в котором тип источника – double, тип цели – все типы, для которых существует безопасное преобразование, выполняемое автоматически.
  8. Постройте Windows приложение, в котором тип источника – char, тип цели – все типы, Для которых существует безопасное преобразование, выполняемое автоматически.

Проекты

  1. Построить Windows-приложение «Стандартный калькулятор», аналогичный Windows калькулятору ­– приложению Calculator в режиме Standard.
  2.  Построить Windows-приложение «Научный калькулятор», аналогичный Windows калькулятору ­– приложению Calculator в режиме Scientific.

 Скачать лекцию 1.3