Выражения и операции
Операции высшего приоритета
Рассмотрим подробнее операции из таблицы 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 приведены результаты вывода на консоль, полученные при вызове этого метода.
Операции "увеличить" и "уменьшить" (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.
Следует также заметить, что рассматриваемые операции применимы только к переменным, свойствам и индексаторам класса, то есть к выражениям, которым отведена область памяти. В языках C++ и C# такие выражения называются l-value, поскольку они могут встречаться в левых частях оператора присваивания. Как следствие, запись в C# выражения < --x++ > приведет к ошибке. Как только к x слева или справа приписана одна из операций, выражение перестает принадлежать к классу l-value выражений и вторую операцию приписать уже невозможно.
Подводя итоги, отмечу, что операции выполняются только тогда, когда вычисляется соответствующий операнд, а не в соответствии с приоритетом, указанным в таблице 3.1. Важнее помнить, что хороший стиль программирования рекомендует использовать эти операции только в выражениях, не содержащих других операндов. Еще лучше вообще не использовать их в выражениях, а применять их только как операторы:
x++; y--;
В этом случае фактически исчезает побочный эффект, являющийся опасным средством, и операции используются как краткая запись операторов:
x = x + 1; y = y - 1;