Опубликован: 02.12.2009 | Уровень: для всех | Доступ: свободно | ВУЗ: Тверской государственный университет
Лекция 1:

Язык программирования и среда разработки. Цели курса

Лекция 1: 12345678 || Лекция 2 >

Пример

Один из принципов, которых я придерживаюсь при написании курсов по программированию, состоит в том, что в таких курсах программный код должен составлять существенную часть текста. Этот код следует читать и изучать не менее внимательно, чем обычный текст. Зачастую он говорит больше, чем рассуждения автора. Поэтому и данная лекция заканчивается примером, который иллюстрирует основные понятия, введенные в лекции. Я отказался от традиции начинать с классического приложения "Здравствуй, мир!". Для первого рассмотрения наш пример будет достаточно сложным: мы построим Решение, содержащее три проекта - проект DLL, консольный проект и Windows-проект.

Постановка задачи

Начну с описания содержательной постановки задачи. Вначале некоторая преамбула. В системе типов языка C# есть несколько типов, задающих различные подмножества арифметического типа данных - int, double и другие. Для значения x любого из этих типов хорошо бы уметь вычислять математические функции - \sin(x), \ln(x) и другие. Встраивать вычисление этих функций в каждый из классов, задающих соответствующий арифметический подтип, кажется неразумным. Поэтому в библиотеку FCL включен класс Math, методы которого позволяют вычислять по заданному аргументу нужную математическую функцию. Класс Math является примером статического класса, играющего единственную роль - роль модуля. У этого класса нет собственных данных, если не считать двух математических констант - e и \pi, а его методы являются сервисами, которые он предоставляет другим классам.

Построим аналог класса Math и поместим этот класс в DLL, что позволит повторно использовать его, присоединяя при необходимости к различным проектам. В нашем примере не будем моделировать все сервисы класса Math. Ограничимся рассмотрением вычисления функции \sin(x). Эту функцию, как и другие математические функции, можно вычислить, используя разложение в ряд Тэйлора:

\sin(x)=x-\frac{x^3}{3!}+\frac{x^5}{5!}-\ldots=\sum\limits_{i=0}^{\infty}(-1)^i\frac{x^{2i+1}}{(2i+1)!} ( 1.1)

Детали вычислений, использующих формулу 1.1, отложим на момент реализации. А пока продолжим уточнять цель нашего примера. Итак, мы хотим построить DLL, содержащей класс, являющийся аналогом класса Math из библиотеки FCL. Затем мы хотим построить консольный проект, позволяющий провести тестирование корректности вычислений функций построенного нами класса. Затем мы построим Windows-проект, интерфейс которого позволит провести некоторые интересные исследования. Все три проекта будут находиться в одном Решении.

Создание DLL - проекта типа "Class Library"

Запустим Visual Studio 2008, со стартовой страницы перейдем к созданию проекта и в качестве типа проекта укажем тип "Class Library". В открывшемся окне создания DLL, показанном на рис. 1.5, все поля заполнены значениями по умолчанию. Как правило, их следует переопределить, задавая собственную информацию.

Создание проекта DLL

увеличить изображение
Рис. 1.5. Создание проекта DLL

В поле Name задается имя строящейся DLL - MathTools в нашем случае.

В поле Location указывается путь к папке, где будет храниться Решение, содержащее проект. Для Решений этого курса создана специальная папка.

В поле Solution выбран элемент "Create New Solution", создающий новое Решение. Альтернативой является элемент списка, указывающий, что проект может быть добавлен к существующему Решению.

В окне Solution Name задано имя Решения. Здесь выбрано имя Ch1, указывающее на то, что все проекты первой лекции вложены в одно Решение.

Обратите внимание и на другие установки, сделанные в этом окне, - включен флажок (по умолчанию) "Create directory for solution", в верхнем окошке из списка возможных каркасов выбран каркас Framework .Net 3.5. Задав требуемые установки и щелкнув по кнопке "OK", получим автоматически построенную заготовку проекта DLL, открытую в среде разработки проектов Visual Studio 2008 . На рис. 1.6 показан внешний вид среды с построенным Решением и проектом.

Среда Visual Studio 2008 с начальным проектом DLL

увеличить изображение
Рис. 1.6. Среда Visual Studio 2008 с начальным проектом DLL

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

В окне проектов Solution Explorer показано Решение с именем "Ch1", содержащее проект DLL с именем "MathTools". В папке "Properties" проект содержит файл с описанием сборки - ее имя и другие характеристики. В папке "References" лежат ссылки на основные пространства имен библиотеки FCL, которые могут понадобиться в процессе работы DLL.

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

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

Мы рассмотрели подготовительную часть работы, которую Visual Studio 2008 выполнила для нас. Дальше предстоит потрудиться самим. С чего следует начать? С переименования! Важное правило стиля программирования говорит, что имена классов должны быть содержательными. Изменим имя "Class1" на имя "MyMath". Как следует правильно изменять имена объектов в проектах? Никак не вручную. В окне кода проекта выделите имя изменяемого объекта, затем в главном меню выберите пункт Refactor и подпункт Rename. В открывшемся окне укажите новое имя. Тогда будут показаны все места, требующие переименования объекта. В данном случае будет только одна очевидная замена, но в общем случае замен много, так что автоматическая замена всех вхождений крайне полезна.

Следующий шаг также продиктован правилом стиля: имя класса и имя файла, хранящего класс, должны совпадать. Переименование имени файла делается непосредственно в окне проектов Solution Explorer.

И следующий шаг продиктован крайне важным правилом стиля, имеющим собственное название: правило "И не вздумайте!", которое гласит - "И не вздумайте написать класс без заголовочного комментария". Для добавления документируемого комментария достаточно в строке, предшествующей заголовку класса, набрать три подряд идущих слеша (три косых черты). В результате перед заголовком класса появится заголовочный комментарий - тэг " summary ", в который и следует добавить краткое, но содержательное описание сути класса. Тэги " summary ", которыми следует сопровождать классы, открытые (public) методы и поля класса играют три важные роли. Они облегчают разработку и сопровождение проекта, делая его самодокументируемым. Клиенты класса при создании объектов класса получают интеллектуальную подсказку, поясняющую суть того, что можно делать с объектами. Специальный инструментарий позволяет построить документацию по проекту, включающую информацию из тегов " summary ". В нашем случае комментарий к классу MyMath может быть достаточно простым - "Аналог класса Math библиотеки FCL".

Поскольку мы хотим создать аналог класса Math, в нашем классе должны быть аналогичные методы. Начнем, как уже говорилось, с метода, позволяющего вычислить функцию \sin(x). Заголовок метода сделаем такой же, как и в классе аналоге. Согласно правилу стиля "И не вздумайте" зададим заголовочный комментарий к методу. В результате в тело класса добавим следующий код:

/// <summary>
 /// Sin(x)
 /// </summary>
 /// <param name="x">угол в радианах - аргумент функции Sin</param>
 /// <returns>Возвращает значение функции Sin для заданного угла</returns>
 public static double Sin(double x)
 {            
 }

Осталось написать реализацию вычисления функции, заданную формулой 1.1. Как и во всяком реальном программировании для этого требуется знание некоторых алгоритмов. Алгоритмы вычисления конечных и бесконечных сумм относятся к элементарным алгоритмам, изучаемым в самом начале программистских курсов. Хотя этот курс я пишу в ориентации на лиц, владеющих программированием и основами алгоритмических знаний, но я хотел бы, чтобы он был доступен и для тех, для кого C# является первым языком программирования. Поэтому прежде чем написать программный текст, скажем несколько слов о том, как вычислять конечные и бесконечные суммы, аналогичные формуле 1.1, которая задает вычисление функции \sin(x). Искушенные читатели могут пропустить этот текст.

Вычисление конечных и бесконечных сумм

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

S=\sum\limits_{k=1}^n a_k ( 1.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, построив рекуррентную формулу a_{k+1} = f(a_k). Этот прием с успехом применяется как при вычислении функции \sin(x) по формуле 1.1, так и при аналогичных вычислениях большинства других математических функций.

Покажем на примере формулы 1.1, как можно построить необходимые рекуррентные соотношения. Запишем соотношения для a_0,\, a_k,\, a_{k+1):

a_0=\frac{{(-1)}^0x}{1!}=x;\quad a_k=\frac{{(-1)}^k x^{2k+1}}{(2k+1)!};\quad a_{k+1}=\frac{{(-1)}^{k+1} x^{2k+3}}{(2k+3)!} ( 1.3)

Вычислив отношение a_{k+1}/a_k, получим требуемое рекуррентное соотношение:

a_{k+1}=a_k\frac{-x^2}{(2k+2)(2k+3)} ( 1.4)

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

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

А теперь поговорим о том, как справляться с бесконечными суммами, примером которых является формула 1.1. Для математики бесконечность естественна. Множество целых чисел бесконечно, множество рациональных чисел бесконечно, множество вещественных чисел бесконечно. Элементы первых двух множеств можно пронумеровать - они задаются счетными множествами, множество вещественных чисел несчетно. Сколь угодно малый промежуток вещественной оси мы бы не взяли, там находится бесконечно много вещественных чисел. Число \pi и другие иррациональные числа задаются бесконечным числом цифр, не имеющим периода.

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

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

\lim\limits_{n\to\infty}a_n=A

По определению число A является пределом числовой последовательности, если для любого сколь угодно малого числа \varepsilon существует такой номер N, зависящий от \varepsilon, что для всех n, больших N, числа a_n находятся в \varepsilon -окрестности числа A. Это определение дает основу для вычисления значения предела A. Понятно, что получить точное значение A во многих случаях принципиально невозможно, - его можно вычислить лишь с некоторой точностью и тоже не сколь угодно малой, поскольку существует понятие "машинного нуля" - минимального числа, все значения меньше которого воспринимаются как нуль. Когда два соседних члена последовательности - a_n и a_{n+1} - начинают отличаться на величину по модулю меньшую чем \delta, то можно полагать, что оба члена последовательности попали в \varepsilon -окрестность числа A и a_{n+1} можно принять за приближенное значение числа A. Это рассуждение верно только при условии, что последовательность действительно имеет предел. В противном случае этот прием может привести к ошибочным выводам. Например, рассмотрим последовательность, элементы которой равны 1, если индекс элемента делится на 3, и равны 2, если индекс не делится на 3. Очевидно, что у этой последовательности предела нет, хотя существуют полностью совпадающие соседние члены последовательности.

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

Вернемся к задаче вычисления функции \sin(x). Вот возможный шаблон решения:

Init;
while(Abs(ak) > EPS)
	{
			S+=ak;
			k++;
			//Вычислить новое значение ak
			…	
	}

При применении этого шаблона предполагается, что в разделе Init объявляются и должным образом инициализируются нужные переменные - S, ak, k. По завершению цикла переменная S содержит значение функции, вычисленное с заданной точностью.

Теперь мы готовы расширить определение класса, добавив код метода.

Код

Приведем полный код проекта DLL, построенный на данный момент:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MathTools
{
    /// <summary>
    /// Аналог класса Math библиотеки FCL
    /// </summary>
    public class MyMath
    {
        //Константы класса
        const double TWOPI = 2 * Math.PI;
        const double EPS = 1E-9;

        //Статические методы класса

        /// <summary>
        /// Sin(x)
        /// </summary>
        /// <param name="x">
        ///     угол в радианах - аргумент функции Sin
        /// </param>
        /// <returns>
        ///     Возвращает значение функции Sin для заданного угла
        /// </returns>
        public static double Sin(double x)
        {
            //Оптимизация - приведение к интервалу
            x = x % TWOPI;

            //Init
            double a = x;
            double res = 0;
            int k = 0;

            //Основные вычисления
            while (Math.Abs(a) > EPS)
            {
                res += a;
                a *= -x * x / ((2 * k + 2) * (2 * k + 3));
                k++;
            }
            return res;  
        }
    }
}

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

Итак, все хорошо? Не совсем. Оптимизацию можно продолжить, правда, не столь уже существенную. Сейчас для вычисления значения переменной a требуется выполнить одно деление, пять умножений, два сложения, взятие результата с обратным знаком. Попробуйте самостоятельно написать новую версию метода с улучшенными показателями, не глядя на код, который я сейчас приведу. Я добавил в класс новую версию метода, сохранив для новой версии имя метода - Sin. В классе остался и старый метод, но уже с именем SinOld. Две версии, давая один и тот же результат вычислений, позволят нам в дальнейшем провести некоторые полезные исследования.

Вот код метода с дополнительной оптимизацией:

public static double Sin(double x)
        {
            //Оптимизация - приведение к интервалу
            x = x % TWOPI;

            //Init
            double a = x;
            double res = 0;
            int k = 0;
            double x2 = x * x;

            //Основные вычисления
            while (Math.Abs(a) > EPS)
            {
                res += a;
                k+=2;
                a *= -x2 / (k * (k + 1));                
            }
            return res;
        }

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

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

А теперь вернемся к технической стороне дела. Построим Решение, содержащее проект, для чего в Главном меню среды выберем пункт Build|Build Solution. В результате успешной компиляции будет построен файл с уточнением dll. Поскольку построенная сборка не содержит выполняемого файла, то непосредственно запустить наш проект на выполнение не удастся. Построим консольный проект, к которому присоединим нашу DLL, и протестируем, насколько корректно работают созданные нами методы. Заодно разберемся с тем, как строится консольный проект и как к нему подсоединяется сборка, содержащая DLL.

Лекция 1: 12345678 || Лекция 2 >
Гулжанат Ергалиева
Гулжанат Ергалиева
Федор Антонов
Федор Антонов

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

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

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

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