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

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

Унарные операции приоритета 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.

Результаты работы метода Ariphmetica

увеличить изображение
Рис. 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(2^n), то уже при n = 64 никакого быстродействия всех современных компьютеров не хватит для решения этой задачи в сколь-либо допустимые сроки. Программист обязан уметь оценивать объем ресурсов, требуемых программе.

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

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

"Выбора тяжко бремя - память или время!"

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

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

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

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

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

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

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

x=\frac{(a+53.5*33/37^2)*(a-53.5*33/37^2)}{\sqrt[3]{(133+53.5*33/37^2)}}
y=\frac{(a+53.5*33/37^2)}{(a-53.5*33/37^2)}

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

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

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

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

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

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

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

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

Добрый день!

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