Лекция 1.4 Операторы языка C#

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

Оператор присваивания

В лекции 1.3 подробно рассматривались операция и выражение присваивания

X = expr

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

X1 += X2 *= … = Xk = expr

Синтаксически присваивание состоит из левой и правой частей, разделенных знаком операции присваивания. Правая часть – это выражение, в том числе выражение присваивания, как в последнем примере. Левая часть – это переменная, более точно левая часть представляет собой lvalue – выражение левой части, которому можно присвоить значение. Переменная является наиболее распространенным частным случаем lvalue.

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

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

X = expr;

Допустимы и многочисленные вариации:

X1 += X2 *= … = Xk = expr;

К операторам присваивания можно отнести и такие операторы, как:

X++; X—; ++X; —X;

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

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

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

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

Операции упаковать и распаковать — boxing и unboxing

Возникает естественный вопрос, можно ли ссылочным переменным, связанным с объектами, хранимыми в куче, присваивать значимые переменные, хранимые в стеке.  Можно ли выполнять обратную операцию? В C# такие возможности преобразования типов предусмотрены. Операция  «упаковать» (boxing) позволяет переменную значимого типа «упаковать в одежды класса», создавая объект в динамической памяти. Такое преобразование выполняется автоматически всякий раз, когда цель принадлежит классу object, а источником может быть переменная любого из значимых типов. Операция  «распаковать» (unboxing) позволяет переменную типа object «распаковать и извлечь хранимое значение». Такое преобразование выполняется автоматически. Извлеченное значение не сохраняет информацию о своем типе. Поэтому, прежде чем присвоить это значение цели, его необходимо привести к нужному типу.  Ответственность за это приведение лежит на программисте.

Рассмотрим подробнее, какие преобразования могут выполняться в процессе присваивания:

  • Цель и источник значимого типа. Здесь наличествует семантика значимого присваивания. В этом случае источник и цель имеют собственную память для хранения значений. Если типы цели и источника совпадают, то никаких проблем нет. Значения источника копируются и заменяют значения соответствующих полей цели. Источник и цель после этого продолжают жить независимо. У них своя память, хранящая после присваивания одинаковые значения. Если типы разные, то необходимо преобразование типов. Оно может быть безопасным и тогда выполняется автоматически. В противном случае должно явно задаваться программистом. Явные и неявные преобразования внутри арифметического типа, кастинг, метод Parse и методы класса Convert подробно рассматривались в главе 1.2.
  • Цель и источник ссылочного типа. Здесь имеет место семантика ссылочного присваивания – присваивание ссылок. В этом случае значениями источника и цели являются ссылки на объекты, хранящиеся в динамической памяти («куче»). Если типы источника и цели совпадают, то никаких проблем нет. Цель разрывает связь с тем объектом, на который она ссылалась до присваивания, и становится ссылкой на объект, связанный с источником. Результат ссылочного присваивания двоякий. Объект, на который ссылалась цель, теряет одну из своих ссылок и может стать «висячим» — бесполезным объектом, на который никто не ссылается, так что его дальнейшую судьбу определит сборщик мусора.
    После присваивания с объектом в памяти, на который ссылался источник, теперь связываются, по меньшей мере, две ссылки, рассматриваемые как различные имена одного объекта. Ссылочное присваивание приводит к созданию псевдонимов – к появлению разных имен у одного объекта. Особо следует учитывать ситуацию, когда цель и/или источник имеет значение null – нулевой ссылки, не указывающей ни на какой объект. Если такое значение имеет источник, то в результате присваивания цель получает это значение и более не ссылается ни на какой объект. Если же цель имела значение null, а источник нет, то в результате присваивания ранее «висячая» цель становится ссылкой на объект, связанный с источником.
    Если типы источника и цели разные, то присваивание без всяких преобразований возможно лишь в том случае, если источник является потомком родительского класса, заданного целью. Цель – родитель может быть связана с объектом своего потомка, поскольку в этом случае все поля и методы родителя имеются и у потомка и будут определены.
    Если же цель не принадлежит родительскому классу источника, то тогда ссылочное присваивание возможно лишь при условии явного задания приведения типов, но тогда вся ответственность за успех этого преобразования лежит на программисте, который должен быть уверен, что объект источника, связанный ссылкой действительно принадлежит классу целевого объекта.
  • Цель ссылочного типа, источник значимого типа. В этом случае «на лету» значимый тип преобразуется в ссылочный. Как обеспечивается двойственность существования значимого и ссылочного типа – переменной и объекта? Ответ прост: за счет эффективно реализованной  операции «упаковать» (boxing), выполняемой автоматически.
    Такое присваивание возможно лишь в том случае, когда цель принадлежит классу object. Поскольку класс object является родителем для всех классов, в том числе и для значимых классов, то при таком присваивании никаких ошибок возникать не будет, оно всегда возможно.
  • Цель значимого типа, источник ссылочного типа. В этом случае «на лету» ссылочный тип преобразуется в значимый. Операция «распаковать» (unboxing) выполняет обратную операцию, – она «сдирает» объектную упаковку и извлекает хранимое значение. Заметьте, операция «распаковать» не является обратной к операции «упаковать» в строгом смысле этого слова. Оператор object obj = x корректен, но выполняемый следом оператор x = obj приведет к ошибке. Недостаточно, чтобы хранимое значение в упакованном объекте точно совпадало по типу с переменной, которой присваивается объект. Необходимо явно заданное преобразование к нужному типу.

Блок, или составной оператор

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

{

оператор_1

оператор_N

}

В языках программирования нет общепринятой нормы для использования символа точки с запятой при записи последовательности операторов. Есть три различных подхода и их вариации. Категорические противники точек с запятой считают, что каждый оператор должен записываться на отдельной строке (для длинных операторов определяются правила переноса). В этом случае точки с запятой (или другие аналогичные разделители) не нужны. Горячие поклонники точек с запятой (к ним относятся языки С++ и C#) считают, что точкой с запятой должен оканчиваться каждый оператор. В результате в операторе if перед else появляется точка с запятой. Третьи полагают, что точка с запятой не принадлежит оператору, а играет роль разделителя операторов. В выше приведенной записи блока, следуя синтаксису C#, каждый из операторов заканчивается символом точка с запятой. Но, заметьте, блок не заканчивается этим символом!

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

Пустой оператор

Пустой оператор – это пусто, завершаемое точкой с запятой. Иногда полезно рассматривать отсутствие операторов как существующий пустой оператор. Вот пример:

if (a > b) ;

else

{

int temp = a; a = b; b = temp;

}

Это корректно работающий пример. А вот типичная для новичков ошибка:

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

{

}

Здесь телом цикла является пустой оператор.

Операторы выбора

Как в С++ и других языках программирования, в языке C# для выбора одной из нескольких возможностей используются две конструкции – if и switch. Первую из них обычно называют альтернативным выбором, вторую – разбором случаев.

Оператор if

Начнем с синтаксиса оператора if:

if(выражение_1) оператор_1

else if(выражение_2) оператор_2

else if(выражение_K) оператор_K

else оператор_N

Какие особенности синтаксиса следует отметить? Логические выражения if заключаются в круглые скобки и имеют значения true или false. Каждый из операторов может быть блоком, в частности if-оператором. Поэтому возможна и такая конструкция:

if(выражение1) if(выражение2) if(выражение3) …

Ветви else if, позволяющие организовать выбор из многих возможностей, могут отсутствовать. Может быть опущена и заключительная else-ветвь. В этом случае краткая форма оператора if задает альтернативный выбор – делать или не делать – выполнять или не выполнять then-оператор.

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

Оператор switch

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

switch(выражение)

{

case константное_выражение_1: [операторы_1 оператор_перехода_1]

case константное_выражение_K: [операторы_K оператор_перехода_K] [default: операторы_N оператор_перехода_N]

}

Ветвь default может отсутствовать. Заметьте, по синтаксису допустимо, чтобы после двоеточия следовала пустая последовательность операторов, а не последовательность, заканчивающаяся оператором перехода. Константные выражения в case должны иметь тот же тип, что и switch-выражение.

Семантика оператора switch чуть запутана. Вначале вычисляется значение switch-выражения. Затем оно поочередно в порядке следования case сравнивается на совпадение с константными выражениями. Как только достигнуто совпадение, выполняется соответствующая последовательность операторов case-ветви. Поскольку последний оператор этой последовательности является оператором перехода (чаще всего это оператор break), то обычно он завершает выполнение оператора switch. Использование операторов перехода – это плохая идея. Таким оператором может быть оператор goto, передающий управление другой case-ветви, которая в свою очередь может передать управление еще куда-нибудь, получая блюдо «спагетти» вместо хорошо структурированной последовательности операторов. Семантика осложняется еще и тем, что case-ветвь может быть пустой последовательностью операторов. Тогда в случае совпадения константного выражения этой ветви со значением switch-выражения будет выполняться первая непустая последовательность очередной case-ветви.

Если значение switch-выражения не совпадает ни с одним константным выражением, то выполняется последовательность операторов ветви default, если же таковой ветви нет, то оператор switch эквивалентен пустому оператору.

Полагаю, что оператор switch – это самый неудачный оператор языка C# как с точки зрения синтаксиса, так и семантики. Неудачный синтаксис порождает запутанную семантику, являющуюся источником плохого стиля программирования. Понять, почему авторов постигла неудача, можно, оправдать – нет. Дело в том, что оператор унаследован от С++, где его семантика и синтаксис  еще хуже. В языке C# синтаксически каждая case-ветвь должна заканчиваться оператором перехода (забудем на минуту о пустой последовательности), иначе возникнет ошибка периода компиляции. В языке С++ это правило не является синтаксически обязательным, хотя на практике применяется та же конструкция с конечным оператором break. При его отсутствии управление «проваливается» в следующую case-ветвь. Конечно, профессионал может с успехом использовать этот трюк, но в целом ни к чему хорошему это не приводит. Борясь с этим, в C# потребовали обязательного включения оператора перехода, завершающего ветвь. Гораздо лучше было бы, если бы последним оператором мог быть только оператор break, как следствие, его можно было бы не писать, и семантика стала бы прозрачной – при совпадении значений двух выражений выполняются операторы соответствующей case-ветви, при завершении которой завершается и оператор switch.

Еще одна неудача в синтаксической конструкции switch связана с существенным ограничением, накладываемым на case-выражения, которые могут быть только константным выражением. Уж если изменять оператор, то гораздо лучше было бы использовать синтаксис и семантику Visual Basic, где в case-выражениях допускается список, каждое из выражений которого может задавать диапазон значений.

Разбор случаев – это часто встречающаяся ситуация в самых разных задачах. Применяя оператор switch, помните о недостатках его синтаксиса, используйте его в правильном стиле. Заканчивайте каждую case-ветвь оператором break, но не применяйте goto.

Содержательный пример применения оператора switch подробно рассмотрен в лекции 1.2. Рассмотрим еще один показательный пример, в котором вычисляется арифметическое выражение с двумя аргументами.

/// <summary>

/// Разбор случаев с использованием списков выражений

/// </summary>

/// <param name=»operation»>операция над аргументами</param>

/// <param name=»arg1″>первый аргумент бинарной операции</param>

/// <param name=»arg2″>второй аргумент бинарной операции</param>

/// <param name=»result»>результат бинарной операции</param>

public void ExprResult(string operation, double arg1, double arg2,

ref double result)

{

switch (operation)

{

case «+»:

case «Plus»:

case «Плюс»:

result = arg1 + arg2;

break;

case «-»:

case «Minus»:

case «Минус»:

result = arg1 — arg2;

break;

case «*»:

case «Mult»:

case «Умножить»:

result = arg1 * arg2;

break;

case «/»:

case «Divide»:

case «Div»:

case «разделить»:

case «Делить»:

result = arg1 / arg2;

break;

default:

result = 0;

break;

}

}//ExprResult

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

Операторы перехода

Операторов перехода, позволяющих прервать естественный порядок выполнения операторов блока, в языке C# несколько.

Оператор goto

Оператор goto имеет простой синтаксис и семантику:

goto [метка|case константное_выражение|default];

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

«О вреде оператора goto» и о том, как можно обойтись без него, писал еще Эдсгар Дейкстра при обосновании принципов структурного программирования.

Я уже многие годы не применяю этот оператор и считаю, что хороший стиль программирования не предполагает использования этого оператора в C# ни в каком из  вариантов – ни в операторе switch, ни для организации безусловных переходов.

Операторы break и continue

В структурном программировании признаются полезными «переходы вперед» (но не назад), позволяющие при выполнении некоторого условия выйти из цикла, из оператора выбора, из блока. Операторы break и continue специально предназначены для этих целей.

Оператор break
может стоять в теле цикла или завершать case-ветвь в операторе switch. Пример его использования в операторе switch уже демонстрировался. При выполнении оператора break в теле цикла завершается выполнение самого внутреннего цикла. В теле цикла, чаще всего, оператор break помещается в одну из ветвей оператора if, проверяющего условие преждевременного завершения цикла. Классическим примером является «поиск по образцу», когда в массиве ищется элемент, соответствующий образцу. Понятно, что когда такой элемент найден, поиск можно прекратить. Вот пример метода, реализующего данную стратегию поиска:

/// <summary>

/// Поиск образца в массиве

/// </summary>

/// <param name=»ar»>массив для поиска</param>

/// <param name=»pat»>образец поиска</param>

/// <param name=»patIndex»>индекс найденного элемента</param>

/// <returns>

/// true, если найден элемент, совпадающий с образцом

/// false, в противном случае

/// </returns>

public bool SearchPattern(int[] ar, int pat, out int patIndex)

{

int n = ar.Length;

patIndex = -1;

bool found = false;

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

if (ar[i] == pat)

{

found = true;

patIndex = i;

break;

}

return found;

}

Оператор continue используется только в теле цикла. В отличие от оператора break, завершающего внутренний цикл, continue осуществляет переход к следующей итерации этого цикла.

Оператор return

Еще одним оператором, относящимся к группе операторов перехода, является оператор return, позволяющий завершить выполнение процедуры или функции. Его синтаксис:

return [выражение];

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

Операторы цикла

Без циклов жить нельзя в программах, нет.

Оператор for

Наследованный от С++ весьма удобный оператор цикла for обобщает известную конструкцию цикла типа арифметической прогрессии. Его синтаксис:

for(инициализаторы; условие; список_выражений) оператор

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

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

В качестве примера рассмотрим еще одну классическую задачу: является ли строка текста палиндромом. Напомню, палиндромом называется симметричная строка текста, читающаяся одинаково слева направо и справа налево. Для ее решения цикл for подходит наилучшим образом: здесь используются два счетчика – один возрастающий, другой убывающий. Вот текст соответствующей процедуры:

/// <summary>

/// Определение палиндромов.

/// Демонстрация цикла for

/// </summary>

/// <param name=»str»>текст</param>

/// <returns>true — если текст является палиндромом</returns>

public bool IsPalindrom(string str)

{

for (int i = 0, j = str.Length — 1; i < j; i++, j—)

if (str[i] != str[j]) return (false);

return (true);

}//IsPalindrom

В цикле for разрешается опускать некоторые части заголовка  цикла.  Конструкция этого оператора, в которой все части заголовка опущены, задает бесконечный цикл:

for(;;) {…}

Эта конструкция позволяет организовать цикл с проверкой выхода в теле цикла. Ярые любители оператора for, к которым чаще всего относятся любители стиля языка С++, часто пользуются такой конструкцией. Я отношу это к нарушениям хорошего стиля программирования. В таких ситуациях лучше использовать цикл типа while.

Циклы While

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

while(выражение) оператор

Эта модификация соответствует стратегии: «сначала проверь, а потом делай». В результате проверки может оказаться, что и делать ничего не нужно. Тело такого цикла может ни разу не выполняться. Конечно же, возможно и зацикливание. В нормальной ситуации каждое выполнение тела цикла – это очередной шаг к завершению цикла.

Цикл, проверяющий условие завершения в конце, соответствует стратегии: «сначала делай, а потом проверь». Тело такого цикла выполняется, по меньшей мере, один раз. Вот синтаксис этой модификации:

do

оператор

while(выражение);

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

/// <summary>

/// Два цикла: с проверкой в конце и в начале.

/// Внешний цикл — образец многократно решаемой задачи.

/// Завершение цикла определяется в диалоге с пользователем.

/// </summary>

public voidLoop()

{

string answer, text;

do

{

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

text = Console.ReadLine();

int i =0, j = text.Length-1;

while ((i < j) && (text[i] == text[j]))

{i++; j—;}

if (text[i] == text[j])

Console.WriteLine(text +» — это палиндром!»);

else

Console.WriteLine(text +» — это не палиндром!»);

Console.WriteLine(«Продолжим? (yes/no)»);

answer = Console.ReadLine();

}

while(answer ==»yes»);

}//Loop

Цикл foreach

Новым видом цикла, не унаследованным от С++, является цикл foreach, удобный при работе с массивами, коллекциями и другими подобными контейнерами данных. Его синтаксис:

foreach(тип идентификатор in контейнер) оператор

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

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

В приведенном ниже примере показана работа с трехмерным массивом. Массив создается с использованием циклов типа for, а при нахождении суммы его элементов, минимального и максимального значения используется цикл foreach:

/// <summary>

/// Демонстрация цикла foreach.

/// Вычисление суммы, максимального и минимального

/// элементов трехмерного массива,

/// заполненного случайными числами.

/// </summary>

public void SumMinMax()

{

int [,,] arr3d = new int[10,10,10];

Random rnd = new Random();

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

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

for (int k = 0; k < 10; k++)

arr3d[i, j, k]= rnd.Next(100);

 

long sum = 0; int min = max = arr3d[0,0,0];

foreach(int item in arr3d)

{

sum +=item;

if (item > max) max = item;

else if (item < min) min = item;

}

Console.WriteLine(«sum = {0}, min = {1}, max = {2}»,

sum, min, max);

}//SumMinMax

Специальные операторы

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

Оператор yield

При рассмотрении оператора цикла foreach говорилось, что он применим к классам, содержащим контейнеры, и цикл foreach перебирает элементы контейнера в некотором заданном порядке. Для того чтобы класс содержал контейнер, он должен быть перечислимым, будучи наследником интерфейса IEnumerable. Есть другая возможность, — класс может иметь один или несколько методов, называемых итераторами, создающими контейнеры и возвращающие результат интерфейсного класса IEnumerable.

Оператор yield используется в итераторах и позволяет заполнять контейнер элементами. Его синтаксис:

yield return <выражение>;

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

/// <summary>

/// Итератор, создающий коллекцию цветов

/// </summary>

/// <returns>коллекцию </returns>

public System.Collections.IEnumerable Rainbow()

{

yield return «red»;

yield return «orange»;

yield return «yellow»;

yield return «green»;

yield return «blue»;

yield return «violet»;

}

Клиенты этого класса могут работать с этой коллекцией, например так:

string colors = «»;

foreach(string s in tst.Rainbow())

colors += s + «-»;

Здесь tst – объект класса Testing, а переменная s в цикле foreach получит значения всех цветов, помещенных в контейнер оператором yield. Следует заметить, что реально никакие контейнеры не создаются, а цикл foreach на каждом шаге вызывает итератор и создает новый элемент. Именно поэтому цикл foreach работает только на чтение элементов и не работает на запись.

Операторы try, catch, finally

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

Операторы checked и unchecked

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

Оператор fixed

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

Оператор lock

Оператором lock, блоком lock, критической секцией кода, закрытым блоком называют блок с предшествующим ключевым словом lock

lock {…}

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

 Проект Statements

Как обычно, для этой главы построено решение с именем Ch4, содержащее Windows- проект с именем Statements. В проекте создан класс Testting, методы которого позволяют тестировать работу операторов языка C#. Эти методы используются в примерах, приведенных в этой главе. Архитектурно проект представляет Windows-приложение с главной кнопочной формой. Каждый из интерфейсных классов, включенных в проект, обеспечивает пользовательский интерфейс для работы с тем или иным методом класса Testing. На рис. 1_4.1 показаны формы проекта в процессе работы с ними:

    Рис. 1_4.1 Формы проекта Statements  в процессе работы

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

Задачи

Альтернатива и разбор случаев

  1. Постройте консольное и Windows-приложение, которое по заданным коэффициентам a, b, c находит корни квадратного уравнения.
  2. Постройте консольное и Windows-приложение, которое по заданному значению аргумента x вычисляет значение функции y=F(x), где функция F(x) задана соотношением 1:
  3. Постройте консольное и Windows-приложение, которое по заданному значению аргумента x вычисляет значение функции y=F(x), где функция F(x) задана графиком 2:
  4. Постройте консольное и Windows-приложение, которое по заданному значению аргумента x вычисляет значение функции y=F(x), где функция F(x) задана графиком 3:
  5. Постройте консольное и Windows-приложение, которое по заданному значению аргумента x вычисляет значение функции y=F(x), где периодическая функция F(x) задана графиком 4:
  6. Постройте консольное и Windows-приложение, которое по заданным координатам x и y определяет, принадлежит ли точка (x, y) одной из 6 дорог (a, b, c, d, e, f), показанных на графике. Если точка принадлежит дороге, то укажите какой именно дороге, если принадлежит двум дорогам, то и этот факт следует отразить в результирующем сообщении.

 

  1. Дана точка A с координатами (x, y) и два прямоугольника Q1 и Q2 со сторонами, параллельными осям координат. Каждый из прямоугольников задается парой точек Q1(p1, p2), Q2(p3, p4), определяющих левый нижний и правый верхний углы прямоугольника.  Постройте консольное и Windows-приложение, которое определяет, принадлежит ли точка A(x, y) хотя бы одному из прямоугольников Q1 и Q2. Если точка принадлежит прямоугольнику, то следует сообщить, какому именно прямоугольнику, если принадлежит двум прямоугольникам, то и этот факт должен быть отражен в результирующем сообщении. Если точка принадлежит границе прямоугольника, то и это должно быть отображено в сообщении.
  2. Дана точка A с координатами (x, y) и мишень – 10 концентрических кругов с центром в начале координат и радиусами R1, R2 … R10. Постройте консольное и Windows-приложение, которое определяет количество выбитых очков. Предполагается, как обычно, что за попадание точки в круг самого малого радиуса начисляется 10 очков и так далее до одного очка. За попадание в «молоко» очки не начисляются.
  3. Заданы четыре точки на плоскости P1, P2, P3, P4 своими декартовыми координатами – x и y. Точки P1 и P2 задают левый нижний угол и правый верхний угол прямоугольника R1 со сторонами параллельными осям координат. Аналогично, точки P3 и P4 задают прямоугольник R2. Определить, пересекаются ли прямоугольники R1 и R2.
  4. Заданы четыре точки на плоскости P1, P2, P3, P4 своими декартовыми координатами – x и y. Точки P1 и P2 задают левый нижний угол и правый верхний угол прямоугольника R1 со сторонами параллельными осям координат. Аналогично, точки P3 и P4 задают прямоугольник R2. Определить, пересекаются ли прямоугольники R1 и R2. Если прямоугольники пересекаются, определить координаты точек P5 и P6, задающих прямоугольник пересечения.
  5. Заданы четыре точки на плоскости P1, P2, P3, P4 своими декартовыми координатами – x и y. Точки P1 и P2 задают левый нижний угол и правый верхний угол прямоугольника R1 со сторонами параллельными осям координат. Аналогично, точки P3 и P4 задают прямоугольник R2. Определить, пересекаются ли прямоугольники R1 и R2. Если прямоугольники пересекаются, определить площадь прямоугольника пересечения.
  6. Заданы три точки на плоскости P1, P2, P3 своими декартовыми координатами – x и y. Точки P1 и P2 задают левый нижний угол и правый верхний угол прямоугольника R1 со сторонами параллельными осям координат. Точка P3 задает центр круга радиуса r. Определить, пересекаются ли прямоугольник R1 и круг, вложен ли круг в прямоугольник, прямоугольник в круг.
  7. Задан круг радиуса r с центром в точке P с координатами x и y. Задана прямая y = b, параллельная оси X. Определить, пересекаются ли круг и прямая.
  8. Задан круг радиуса r с центром в точке P с координатами x и y. Задана прямая y = b, параллельная оси X. Определить пересекаются ли круг и прямая. Если есть пересечение, то определить координаты точек пересечения.
  9. Заданы три точки на плоскости P1, P2, P3 своими декартовыми координатами – x и y. Точки P1 и P2 задают левый нижний угол и правый верхний угол прямоугольника R1 со сторонами параллельными осям координат. Точка P3 задает центр круга радиуса r. Определить, пересекаются ли прямоугольник R1 и круг. Если есть пересечение, то определить координаты точек пересечения.
  10. Заданы три точки на плоскости P1, P2, P3 своими декартовыми координатами – x и y. Точки P1 и P2 задают левый нижний угол и правый верхний угол прямоугольника R1 со сторонами параллельными осям координат. Точка P3 задает центр круга радиуса r. Определить площадь пересечения круга и прямоугольника.
  11. Заданы три точки на плоскости P1, P2, P3 своими декартовыми координатами – x и y. Точки P1 и P2 задают левый нижний угол и правый верхний угол прямоугольника R1 со сторонами параллельными осям координат. Точка P3 задает центр круга радиуса r. Определить число точек с целочисленными координатами внутри области пересечения прямоугольника R1 и круга.
  12. Заданы четыре точки на плоскости P1, P2, P3, P4  своими декартовыми координатами – x и y. Точки P1 и P2 задают один отрезок прямой, точки P3 и P4 задают другой отрезок прямой. Определить пересекаются ли отрезки. Если да, то определить координаты точки пересечения.
  13.  (*) Дана точка A с координатами (x, y) и треугольник, заданный своими вершинами – точками Q1(x1, y1), Q2(x2, y2)  и Q3(x3, y3). Постройте консольное и Windows-приложение, которое определяет, принадлежит ли точка A треугольнику (находится внутри его или на его границах).
  14. Дан текст T. Постройте консольное и Windows-приложение, которое проводит частотный анализ, определяя частоту вхождения букв А, Б, … Я (больших и малых) в текст T.
  15. Дан массив элементов с элементами, принимающими одно из четырех значений: белый, черный, красный, желтый. Постройте консольное и Windows-приложение, которое определяет частоту вхождения элементов каждого цвета в массив.
  16. Студент, приходящий на экзамен, характеризуется тремя булевскими переменными P1, P2 и P3. Переменная P1 имеет значение true, если студент знает определения, и false в противном случае. Переменная P2 имеет значение true, если студент умеет доказывать теоремы, и false в противном случае. Переменная P3 имеет значение true, если студент умеет решать задачи, и false в противном случае.  Постройте консольное и Windows-приложение «Строгий экзаменатор», в котором экзаменатор руководствуется следующим алгоритмом: он спрашивает определение и ставит оценку «неуд», в случае его незнания. Студенту, знающему определение, предлагается доказать теорему, в случае неуспеха ставится оценка «уд». Студенту, знающему определения и умеющему доказывать теоремы, предлагается решить задачу, в случае неуспеха ставится оценка «хор», в случае успеха – «отл».
  17. Студент, приходящий на экзамен, характеризуется тремя булевскими переменными P1, P2 и P3. Переменная P1 имеет значение true, если студент знает определения, и false в противном случае. Переменная P2 имеет значение true, если студент умеет доказывать теоремы, и false в противном случае. Переменная P3 имеет значение true, если студент умеет решать задачи, и false в противном случае.  Постройте консольное и Windows-приложение «Добрый экзаменатор», в котором экзаменатор руководствуется следующим алгоритмом: он предлагает решить задачу и в случае успеха ставит оценку – «отл». Студенту, не умеющему решать задачи, предлагается доказать теорему, в случае успеха ставится оценка «хор». Студенту, не умеющему решать задачи и не умеющему доказывать теоремы, предлагается сформулировать определение и в случае его незнания ставится оценка «неуд», в случае успеха ставится оценка «уд».

 

Вычисление сумм, произведений и рекуррентные соотношения.

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

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

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

 

и применить для ее решения следующий шаблон:

S=0;

for(int k=1; k<=n; k++)

{

//Вычислить текущий член суммы ak

S+=ak;

}

Часто приходится пользоваться слегка расширенным шаблоном:

Init;

for(int k=1; k<=n; k++)

{

//Вычислить текущий член суммы ak

S+=ak;

}

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

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

  • Чистка цикла. Все вычисления, не зависящие от k, должны быть вынесены из цикла (в раздел Init).
  •  Рекуррентная формула. Часто можно уменьшить время вычислений ak, используя предыдущее значение ak. Иногда приходится вводить дополнительные переменные, хранящие уже вычисленные значения нескольких членов суммы. Рекуррентная формула выражает новое значение ak через предыдущее значение и дополнительные переменные, если они требуются. Начальные значения ak и дополнительных переменных должны быть корректно установлены перед выполнением цикла в разделе Init. Заметьте, если начальное значение ak вычисляется в разделе Init до цикла, то схема слегка модифицируется, — вначале выполняется прибавление ak к S, а затем новое значение ak вычисляется по рекуррентной формуле.

Рассмотрим пример. Пусть необходимо вычислить сумму:

Тогда в соответствии с шаблоном

 

Можно построить рекуррентную формулу для ak, поскольку каждое следующее значение равно предыдущему значению, умноженному на x и деленному на k. Вычисление суммы задает следующий фрагмент программы:

int S =0;

int ak=1;

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

{

S+=ak;

//Вычислить текущий член суммы ak

ak *=x/k;

}

Большинство задач этого раздела соответствуют этому шаблону. Рекуррентную формулу чаще всего можно получить, записав выражение для ak и ak-1, вычислив затем их отношение. В некоторых задачах (они отмечены звездочкой) получение рекуррентной формулы может требовать больших усилий.

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

  1. Дано натуральное число n. Вычислить сумму первых n членов расходящегося гармонического ряда:
  2. Дано натуральное число nmax  и вещественное число b. Найти, если оно существует, такое наименьшее n, меньшее nmax, что:Если сумма nmax членов гармонического ряда меньше b, то необходимо выдать соответствующее сообщение.
  3. Дано натуральное число n. Вычислить сумму первых n членов ряда:При суммировании исключается каждый третий член.
  4. Дано натуральное число n. Вычислить сумму первых 2n членов ряда:Вычислить эту сумму четырьмя разными способами: последовательно слева направо, последовательно справа налево, слева направо, вычисляя вначале положительные члены ряда, затем отрицательные, справа налево, вычисляя вначале положительные члены ряда, затем отрицательные. Сравните результаты вычислений. Чем объясняется различие в последних цифрах при больших n? Как влияет на результат использование типов float или double для переменных, задающих суммы и текущий член при суммировании?
  5. Дано натуральное число n. Вычислить сумму первых 2n членов ряда:
  6. Дано натуральное число n. Вычислить произведение первых n членов ряда:
  7. Даны натуральные числа n и k (n>=k). Вычислить биномиальный коэффициент Cnk :
  8. Даны натуральные числа n и m (n>=m). Вычислить сумму биномиальных коэффициентов:Проверьте истинность формулы бинома Ньютона:

 

  1. Дан массив B размерности n*m и массив C размерности m*n. Вычислить сумму диагональных элементов матрицы A = B*C:
  2. Дан массив B размерности n*m и массив C размерности m*n. Вычислить произведение диагональных элементов матрицы A = B*C:
  3. Даны натуральные числа n и m, вещественное x. Вычислить:
  4. Даны натуральные числа n и m, вещественное x. Вычислить:

Рекуррентные вычисления

  1. Вычислить Fn — число Фибоначчи с номером n, где
    F1 = 1;               F2 = 1;      Fk =  Fk-1 + Fk-2  для k>2.
  2. Дано натуральное число n и вещественные числа b и d. Вычислить сумму членов арифметической прогрессии:Предварительно следует записать рекуррентное соотношение для текущего члена суммы, чтобы его вычисление требовало ровно одного сложения
  3. Дано натуральное число n и вещественные числа b и d. Вычислить сумму членов геометрической прогрессии:

    Предварительно следует записать рекуррентное соотношение для текущего члена суммы, чтобы его вычисление требовало ровно одного умножения.
  4. Дано натуральное число n. Вычислить:где
  5. Даны натуральные числа n и m (50<m<n). Вычислить:где
  6. Даны натуральные числа n и m (50<m<n). Вычислить:где
  7. Дано натуральное число n и вещественное число x. Вычислить:

    Предварительно следует записать рекуррентное соотношение для текущего члена суммы, чтобы его вычисление выполнялось за время, равное 12 уе. (см. задачу 1.30). Сравните вычисленное значение S со значением ex. Вычислите разность | S – ex| при различных значениях n и x.
  8. Дано натуральное число n и вещественное число x. Вычислить:

    Предварительно следует записать рекуррентное соотношение для текущего члена суммы, чтобы его вычисление выполнялось за время, равное 27 уе. (см. задачу 1.30). Сравните вычисленное значение S со значением sin(x). Вычислите разность | S – sin(x)| при различных значениях n и x.
  9. Дано натуральное число n и вещественное число x такое, что |x| <1. Вычислить:
    Предварительно следует записать рекуррентное соотношение для текущего члена суммы, минимизируя время его вычисления. Сравните вычисленное значение S со значением arcsin(x). Вычислите разность | S – arcsin(x)| при различных значениях n и x.
  10. Дано натуральное число n и вещественное число x. Вычислить:

    Предварительно следует записать рекуррентное соотношение для текущего члена суммы, чтобы его вычисление выполнялось за время, равное 27 уе. (см. задачу 1.30). Сравните вычисленное значение S со значением cos(x). Вычислите разность | S – cos(x)| при различных значениях n и x.
  11. Дано натуральное число n и вещественное число x такое, что |x| <1. Вычислить:
    Предварительно следует записать рекуррентное соотношение для текущего члена суммы, минимизируя время его вычисления. Сравните вычисленное значение S со значением arccos(x). Вычислите разность | S – arccos(x)| при различных значениях n и x.
  12. Дано натуральное число n и вещественное число x >0. Вычислить:

    Предварительно следует записать рекуррентное соотношение для текущего члена суммы. Минимизируйте время его вычисления в условных единицах. Сравните вычисленное значение S со значением ln(x). Вычислите разность | S – ln(x)| при различных значениях n и x.
  13. (**) Даны натуральные числа n и m и вещественное число x такое, что |x| < π/2. Вычислить:
    где Biопределяются следующим соотношением:Предварительно следует записать рекуррентные соотношения, как для получения чисел Bi, так и для вычисления S. Сравните вычисленное значение S со значением tg(x). Вычислите разность | S – tg(x)| при различных значениях n и x.
  14. (*) Даны натуральные числа n и m и вещественное число x такое, что |x| < π/2. Вычислить:
    Сравните вычисленное значение S со значением tg(x). Вычислите разность | S – tg(x)| при различных значениях n и x.
  15. Дано натуральное число n и вещественное число x . Вычислить:
    Предварительно следует записать рекуррентное соотношение для текущего члена суммы, минимизируя время его вычисления. Сравните вычисленное значение S со значением arctg(x). Вычислите разность | S – arctg(x)| при различных значениях n и x.
  16. (**) Даны натуральные числа n и m и вещественное число x такое, что |x| < π. Вычислить:
    где Biопределяются следующим соотношением:Предварительно следует записать рекуррентные соотношения, как для получения чисел Bi, так и для вычисления S. Сравните вычисленное значение S со значением ctg(x). Вычислите разность | S – ctg(x)| при различных значениях n и x.
  17. Дано натуральное число n и вещественное число x . Вычислить:
    Предварительно следует записать рекуррентное соотношение для текущего члена суммы, минимизируя время его вычисления. Сравните вычисленное значение S со значением arcctg(x). Вычислите разность | S – arcctg(x)| при различных значениях n и x.
  18. (**) Даны натуральные числа n и m и вещественное число x такое, что |x| < π/2. Вычислить:
    где Eiопределяются следующим соотношением:Предварительно следует записать рекуррентные соотношения, как для получения чисел Ei, так и для вычисления S. Сравните вычисленное значение S со значением sc(x). Вычислите разность | S – sc(x)| при различных значениях n и x.
  19. Дано натуральное число n и вещественное число x. Вычислить:

    Предварительно следует записать рекуррентное соотношение для текущего члена суммы, минимизируя время его вычисления. Сравните вычисленное значение S со значением sh(x). Вычислите разность | S – sh(x)| при различных значениях n и x.
  20. Дано натуральное число n и вещественное число x. Вычислить:

    Предварительно следует записать рекуррентное соотношение для текущего члена суммы, минимизируя время его вычисления. Сравните вычисленное значение S со значением ch(x). Вычислите разность | S – ch(x)| при различных значениях n и x.
  21. Дано натуральное число n и вещественное число x такое, что |x| <1. Вычислить:
    Предварительно следует записать рекуррентное соотношение для текущего члена суммы, минимизируя время его вычисления. Сравните вычисленное значение S со значением Arcsh(x). Вычислите разность | S – Arcsh(x)| при различных значениях n и x.

Бесконечность и компьютеры. Вычисления с точностью ε

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

Мир компьютеров – это конечный мир, хотя в нем и присутствует стремление к бесконечности. Множества, с которыми приходится оперировать в мире компьютера, всегда конечны. Тип целых чисел в языках программирования – int – всегда задает конечное множество целых из некоторого фиксированного диапазона. В библиотеке FCL это наглядно подтверждается самими именами целочисленных типов System.Int16, System.Int32, System.Int64. Типы вещественных чисел – double, float – задают конечные множества. Это достигается не только тем, что диапазон задания вещественных чисел ограничен, но и ограничением числа значащих цифр, задающих вещественное число. Поэтому для вещественных чисел компьютера всегда можно указать такие два числа, между которыми нет никаких других чисел. Иррациональности компьютер не знает, – число π всегда задается конечным числом цифр.

Там, где в математике идет речь о пределах, бесконечных суммах, сходимости к бесконечности, в компьютерных вычислениях аналогичные задачи сводятся к вычислениям с заданной точностью – с точностью ε. Рассмотрим например задачу о вычислении предела числовой последовательности:

 

По определению число A является пределом числовой последовательности, если для любого сколь угодно малого числа ε существует такой номер N, зависящий от ε, что для всех n, больших N, числа an находятся в ε-окрестности числа A. Это определение дает основу для вычисления значения предела A. Понятно, что получить точное значение A во многих случаях принципиально невозможно, – его можно вычислить лишь с некоторой точностью и тоже не сколь угодно малой, поскольку, как уже говорилось, есть понятие «машинного нуля» — минимального числа, все значения меньше которого воспринимаются как нуль. В задаче 1.97 требуется вычислить значение числа π как предел числовой последовательности. Оставаясь в рамках стандартных множеств чисел (double, float) принципиально невозможно получить точное значение этого числа, поскольку в этих множествах нет иррациональных чисел с бесконечным числом цифр. Но можно получить значение этого числа с некоторой точностью. Когда два соседних члена последовательности – an и an+1 – начинают отличаться на величину, по модулю меньшую чем δ, то можно полагать, что оба члена последовательности попали в ε-окрестность числа A и an+1 можно принять за приближенной значение числа A. Это рассуждение верно только при условии, что последовательность действительно имеет предел. В противном случае этот прием может привести к ошибочным выводам. Например, последовательность, элементы которой равны 1, если индекс элемента делится на 3, и равны 2, если индекс не делится на 3. Очевидно, что у этой последовательности предела нет, хотя существуют два полностью совпадающих соседних членов последовательности.

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

Рассмотрим задачу вычисления функции с использованием ее разложения в бесконечный сходящийся ряд:

 

Вот возможный шаблон ее решения:

public double f(double x)

{

double S=0,ak=1, eps=1e-8;

while(Math.Abs(ak) >eps)

{

//Вычислить ak

S+=ak;

}

return(S);

}

При применении этого шаблона следует:

  • Получить при возможности рекуррентную формулу, используя для вычисления нового значения ak ранее вычисленные значения.
  • Использовать по возможности свойства функции f(x) для ускорения сходимости ak к нулю, например привести x  к минимально возможному диапазону для периодических функций.
  • Помнить, что данный шаблон применим только тогда, когда ряд является сходящимся.
  • Понимать, что выполнение условия (|ak| < eps ) еще не означает, что значение функции вычислено с точностью eps. Строго говоря, необходимо иметь оценку остаточного члена ряда. На практике этим обстоятельством зачастую можно пренебрегать, уменьшая при необходимости eps и достигая тем самым нужной точности вычисления f(x).

Во всех задачах этого раздела задается точность вычислений ε – малое вещественное число. Обычно, если требуется получить результат с точностью до 5-6 значащих цифр, то ε задается константой 10-8- 10-9 (1e-8 – 1e-9).

  1. Вычислить с заданной точностью значение числа π, используя следующее разложение в ряд:
    Минимизируйте время его вычисления в условных единицах. Сравните вычисленное значение π со стандартным значением PI, возвращаемым классом  Math. Для разных значений ε вычислите n – число членов суммы, требуемых для достижения заданной точности.
  2. Вычислить с заданной точностью значение числа π, используя следующее разложение в ряд:
    Минимизируйте время его вычисления в условных единицах. Сравните вычисленное значение π со стандартным значением PI, возвращаемым классом  Math. Для разных значений ε вычислите n – число членов суммы, требуемых для достижения заданной точности.
  3. Вычислить с заданной точностью значение числа π, используя следующее разложение в ряд:
    Предварительно следует записать рекуррентное соотношение для текущего члена суммы. Минимизируйте время его вычисления в условных единицах. Сравните вычисленное значение π со стандартным значением PI, возвращаемым классом  Math. Для разных значений ε вычислите n – число членов суммы, требуемых для достижения заданной точности.
  4. Вычислить с заданной точностью значение числа e, используя следующее разложение в ряд:
    Предварительно следует записать рекуррентное соотношение для текущего члена суммы. Минимизируйте время его вычисления в условных единицах. Сравните вычисленное значение e со стандартным значением E, возвращаемым классом  Math. Для разных значений ε вычислите n – число членов суммы, требуемых для достижения заданной точности.
  5. Пусть Fn и Fn+1 – два соседних числа Фибоначчи (см. задачу 1.76). Найти с заданной точностью предел отношения (Fn/Fn+1) при n→∞. Сравнить этот предел с «золотым сечением» — числом x. Напомню, золотое сечение строится следующим образом. Возьмем отрезок единичной длины и разделим его на две неравные части – большую x и меньшую y=1-x. Сечение называется «золотым», если отношение целого к большей части равно отношению большей части к меньшей: x/(1-x) = 1/x.
  6. Даны два положительных числа b и d. Пусть:Вычислить с заданной точностью пределы bn, dn, dn-bn.
  7. Пусть x и y=1-x задают золотое сечение отрезка единичной длины (см. задачу 1.104). Пусть задано число α такое, что α >0 и α<1. Вычислить с заданной точностью предел последовательности bn, где
  8. Дано вещественное число x. Вычислить с заданной точностью ex:

    Указание: для ускорения вычислений используйте разложение в ряд только для дробной части числа x. Используйте умножение и константу e для вычисления en, где n – это целая часть числа x.
  9. Дано вещественное число x. Вычислить с заданной точностью sin(x):

    Указание: для ускорения вычислений используйте разложение в ряд только для приведенного значения числа x. Помните, что sin(x) – это периодическая функция, так что всегда можно привести x к интервалу [-π, π].
  10. Дано вещественное число x такое, что |x| <1. Вычислить с заданной точностью arcsin(x):
  11. Дано вещественное число x. Вычислить с заданной точностью cos(x):

    Указание: для ускорения вычислений используйте разложение в ряд только для приведенного значения числа x. Помните, что cos(x) – это периодическая функция, так что всегда можно привести x к интервалу [-π, π].
  12. Дано вещественное число x такое, что |x| <1. Вычислить с заданной точностью arccos(x):
  13. Дано вещественное число  x > 0. Вычислить с заданной точностью ln(x):
  14. Дано вещественное число 0 < x < 2. Вычислить с заданной точностью ln(x):
  15. Дано вещественное число x. Вычислить с заданной точностью tg(x):

    где Biопределяются следующим соотношением:Указание: используйте разложение в ряд только для приведенного значения числа x. Помните, что tg(x) – это периодическая функция, так что всегда можно привести x к интервалу [-π/2, π/2].
  16. Дано вещественное число x . Вычислить с заданной точностью arctg(x):
  17. (**) Дано вещественное число x. Вычислить с заданной точностью ctg(x):
    где Biопределяются следующим соотношением:Указание: используйте разложение в ряд только для приведенного значения числа x. Помните, что сtg(x) – это периодическая функция, так что всегда можно привести x к интервалу [-π/2, π/2].
  18. Дано вещественное число x . Вычислить с заданной точностью arcсtg(x):
  19. (**) Дано вещественное число x. Вычислить с заданной точностью sc(x):

    где Eiопределяются следующим соотношением:Указание: используйте разложение в ряд только для приведенного значения числа x. Помните, что sc(x) – это периодическая функция, так что всегда можно привести x к интервалу [-π, π].
  20. Дано вещественное число x. Вычислить с заданной точностью sh(x):

    Указание: гиперболический синус sh(x) не является периодической функцией, поэтому никакого приведения x выполнять не следует.
  21. Дано вещественное число x. Вычислить с заданной точностью ch(x):

    Указание: гиперболический косинус ch(x) не является периодической функцией, поэтому никакого приведения x выполнять не следует.
  22. Дано вещественное число x. Вычислить с заданной точностью Arcsh(x):

Проекты

  1. (*) Постройте класс MyMath, имеющий те же методы, что и класс Math библиотеки FCL.
  2. (**) Постройте Калькулятор, позволяющий использовать для вычислений как методы класса Math, так и MyMath, и сравнивать результаты по точности и времени вычислений.

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

 

 

 

 

где E