Задачи этой главы для тех, кто не имеет достаточного опыта в программировании. Они должны помочь преодолеть психологический рубеж создания первых программ. Они полезны и тем, кто уже умеет программировать, но не писал программы на C# и не работал в среде Visual Studio.
Методические указания
Что должны знать и что должны уметь студенты, осваивающие начала программирования? Вот возможный перечень основных тем:
- переменная и ее обобщение – объект;
- тип переменных и его обобщение – класс;
- встроенные типы (классы);
- процесс вычислений, как последовательное выполнение операторов языка;
- ввод и вывод данных;
- ветвление процесса вычислений – альтернатива и разбор случаев;
- циклические вычисления;
- модульность построения программ;
- процедуры и функции, как простейшие виды программных модулей;
- строковый тип и работа с текстами;
- массивы как структуры данных;
- рекуррентные вычисления;
- память и время – два основных ресурса, используемых программами;
- корректность программ;
- основы отладки программ и инструментарий среды разработки Visual Studio .Net ;
- Windows-приложения, визуальное программирование, управляемое событиями;
- формы и основные элементы управления – командные кнопки, текстовые окна, списки.
Какой лучший способ проверки того, что все эти понятия действительно освоены? Недостаточно знания ответов на все эти вопросы. Необходимо умение писать качественные программы, включающие процедуры и функции. С объектной точки освоение начал программирования означает умение реализовать достаточно содержательный класс. Если задачи из раздела «Проекты» первых глав задачника не вызывают трудностей, то можно смело считать, что начала программирования освоены.
Что можно не требовать от студентов, осваивающих начала программирования? Можно не требовать умения создавать программную систему, представляющую множество классов, связанных теми или иными отношениями. Более того, вначале можно не требовать создания собственного класса, – можно исходить из более простых моделей программирования, вполне реализуемых в рамках программирования на C# в среде Visual Studio .Net.
Модель 60-х годов – модель языка Pascal и структурного программирования
Консольное приложение, строящееся по умолчанию, позволяет полностью и без проблем реализовать эту модель. В основе этой модели лежит понятие выполняемой программы, состоящей из описания переменных программы и исполняемого блока операторов. Управление процессом вычислений такой программы определяется ее текстом и означает последовательное выполнение операторов исполняемого блока согласно порядку их следования в тексте программы. Описания переменных – это декларативная часть выполняемой программы, служащая для определения тех объектов, над которыми выполняются вычисления.
В консольном приложении роль выполняемой программы играет процедура Main. На начальных этапах у программиста нет необходимости создавать собственные классы. Все, что от него требуется, – это добавить в процедуру Main объявления переменных встроенных типов и записать исполняемый блок – последовательность выполняемых операторов. В зависимости от предпочтений преподавателя можно требовать, чтобы все объявления переменных были собраны в начале программы и предшествовали исполняемому блоку, как это требовалось в Pascal-программах 60-х годов.
Если не вводить оператор Goto, что методически совершенно обосновано, то полученная модель является моделью структурного программирования, поскольку таковыми являются управляющие структуры языка. Вычисления полностью управляются текстом программы и выполняются оператор за оператором. Конечно, операторы выбора и цикла могут быть сколь угодно сложными и иметь сложную внутреннюю структуру, но, тем не менее, их выполнение можно рассматривать как выполнение одного оператора с точно определенной семантикой.
Вот как может выглядеть решение одной из задач, полностью отвечающее этой модели:
using System;
namespace Ch1_Example1
{
/// <summary>
/// Класс, содержащий выполняемую программу
/// </summary>
class Class1
{
/// <summary>
/// Выполняемая программа!
/// нахождение корней квадратного уравнения:
/// ax^2 +bx +c =0
/// </summary>
[STAThread]
static void Main(string[] args)
{
//объявление переменных
double a,b,c; //коэффициенты уравнения
double x1, x2; // корни уравнения
double d; //дискриминант уравнения
string mes; //сообщение о результатах вычислений
string init; //строка ввода
// ввод исходных данных
Console.WriteLine(“Введите три вещественных числа – a, b, c “);
init = Console.ReadLine();
a = Convert.ToDouble(init);
init = Console.ReadLine();
b = Convert.ToDouble(init);
init = Console.ReadLine();
c = Convert.ToDouble(init);
//вычисления и вывод результатов
d = b*b -4*a*c;
if(d>0)
{
mes=”Уравнение имеет два вещественных корня!”;
Console.WriteLine(mes);
x1 = (-b + Math.Sqrt(d))/(2*a);
x2 = (-b – Math.Sqrt(d))/(2*a);
Console.WriteLine(“Корни уравнения: x1={0}, x2={1}”,
x1,x2);
}
else if(d==0)
{
mes=”Уравнение имеет кратный корень!”;
Console.WriteLine(mes);
x1 = -b/(2*a); x2 = -b/(2*a);
Console.WriteLine(“Кратный корень уравнения: x={0}”,x1);
}
else
{
mes=”Уравнение не имеет вещественных корней!”;
Console.WriteLine(mes);
}
}
}
}
Понимание того, как работает процедура Main, достаточно для понимания выполнения всего консольного приложения. На первых порах можно не обращать внимания на объектный антураж, связанный с этой процедурой.
Следует обращать внимание на качество создаваемых программ, – их надежность, понимание текста, наличие комментариев и тегов summary, описывающих смысл программы.
Модель модульного программирования в процедурах и функциях
Как можно раньше следует вводить понятие модульного программирования и понятия процедур и функций, как модулей простейшего вида. Консольное приложение, строящееся по умолчанию, легко позволяет расширить понятие программы, состоящей из одной процедуры Main, на программу, включающую процедуры и функции. Для этого достаточно добавить в созданный по умолчанию класс Class1 нужные процедуры и функции, которые и будут вызываться из основной процедуры Main. Единственное, не вполне очевидное требование состоит в том, что добавляемые процедуры и функции должны сопровождаться описателем static, объяснение специфической роли которого можно оставить на более поздний этап. Достаточно лишь пояснить, что, если процедура имеет модификатор static, то она может вызывать только процедуры и функции, имеющие этот же модификатор. Поскольку процедура Main объявлена как static процедура, то и вызываемые процедуры и функции следует снабжать этим модификатором.
Вот как может выглядеть решение некоторой задачи, в которой целесообразно создать специальную функцию, многократно вызываемую в основной программе Main:
using System;
namespace Ch1_Example2
{
/// <summary>
/// Класс, содержащий выполняемую программу и функцию
/// </summary>
class Class1
{
/// <summary>
/// Выполняемая программа!
/// нахождение интервала, на котором полином третьей степени
/// ax^3 +bx^2 +cx + d
/// меняет знак
/// </summary>
[STAThread]
static void Main(string[] args)
{
//объявление переменных
double a,b,c, d; //коэффициенты полинома
double x1, x2; // исходный интервал
double h; //шаг прохождения интервала
string mes; //сообщение о результатах вычислений
string init; //строка ввода
// ввод исходных данных
Console.WriteLine(“Введите коэффициенты полинома: ” + “четыре вещественных числа – a, b, c, d “);
init = Console.ReadLine();
a = Convert.ToDouble(init);
init = Console.ReadLine();
b = Convert.ToDouble(init);
init = Console.ReadLine();
c = Convert.ToDouble(init);
init = Console.ReadLine();
d = Convert.ToDouble(init);
Console.WriteLine(“Введите три вещественных числа – x1,x2,h” + ” – границы интервала и шаг”);
init = Console.ReadLine();
x1 = Convert.ToDouble(init);
init = Console.ReadLine();
x2 = Convert.ToDouble(init);
init = Console.ReadLine();
h = Convert.ToDouble(init);
//вычисления
double x= x1;
double p =Polinom3(a,b,c,d,x);
int signum = Math.Sign(p);
int signum1 = signum;
while(x<x2)
{
x+=h; p =Polinom3(a,b,c,d,x);
signum1=Math.Sign(p);
if( signum1 != signum)break; //обнаружена смена знака
}
//вывод результатов
if(signum == signum1)
{
mes= “На интервале [{0}, {1}] не обнаружена смена знака полинома!”;
Console.WriteLine(mes,x1, x2);
}
else
{
mes= “Обнаружена смена знака полинома! “+
“В точке x1= {0} P(x1) = {1}; В точке x2= {2} P(x2) = {3}”;
Console.WriteLine(mes,x-h,Polinom3(a,b,c,d,x-h),x,Polinom3(a,b,c,d,x));
}
}
static double Polinom3(double a, double b, double c, double d, double x)
{
return(((a*x +b)*x+c)*x+d);
}
}
}
И в этой модели студенту достаточно лишь понимать, как объявляются и вызываются процедуры и функции, оставляя пока без объяснений детали объектной технологии работы с классами. Заметьте, функции, вызываемые в процедуре Main, добавляются в класс Class1 на том же уровне, что и главная процедура Main. Вся информация, необходимая функции для ее работы, передается через ее параметры, а результат вычисления значения функции возвращается в операторе return.
Модель 80-х годов визуального программирования, управляемого событиями
Консольные приложения, несмотря на всю их полезность при изучении языка программирования, являются анахронизмом с позиций современного программирования. Консольное приложение позволяет ограничиваться созданием минимального пользовательского интерфейса: приглашение к вводу, простейшее меню, повторяющийся цикл вычислений. Современное программирование ориентировано на интерактивный способ работы с программой, когда конечный пользователь сидит непосредственно за экраном компьютера и управляет процессом выполнения программы. В таких условиях роль пользовательского интерфейса становится крайне важной. Хороший интерфейс привлекает пользователей, плохой может оттолкнуть, даже если программа прекрасно справляется со своими основными функциями.
Термин «визуальное программирование» означает создание программ, ориентированных на интерактивный способ взаимодействия пользователя и программы, создание интерфейса пользователя, позволяющего задавать требуемые программе данные, управлять процессом вычислений, следить за результатами вычислений.
Термин «программирование, управляемое событиями» означает следующее: в ходе выполнения программы могут возникать ситуации, идентифицируемые как события. В этом случае выполнение программы приостанавливается, операционная система получает сообщение от программы о возникновении в ней «события» и вызывает обработчик события, предусмотренный программой, а в случае отсутствия такового, универсальный обработчик, предусмотренный самой операционной системой. В результате начинает выполняться программа обработчика события. Такой стиль программирования, в котором ход процесса вычислений определяется событиями и их обработчиками, называется программированием, управляемом событиями.
Визуальное программирование, как правило, является программированием, управляемым событиями. Большинство событий инициируется пользователем, благодаря возможностям интерфейса, специально спроектированным для того, чтобы пользователь в зависимости от уже полученных результатов мог определять дальнейший ход вычислений, выбирая подходящие пункты меню, нажимая специальные командные кнопки, и производя другие действия, приводящие к событиям, для которых предусмотрены соответствующие обработчики.
Windows-приложение в полной мере отвечает этой модели программирования, позволяя достаточно просто и естественно создавать визуальную, управляемую событиями программу. По умолчанию в этом приложении создается форма, которую можно населять элементами управления, определяющими интерфейс пользователя. На первых порах можно ограничиться минимальным набором элементов управления – командными кнопками, текстовыми окнами и списками, что позволяет вводить и выводить как скалярные данные, так и массивы, а также запускать при нажатии командных кнопок нужные обработчики событий. В главной процедуре Main, с которой начинается выполнение программы, создается объект, задающий спроектированную форму, и эта форма открывается на экране. Дальнейший ход вычислений определяется конечным пользователем, – какие данные он будет вводить в текстовые окна, какие командные кнопки будет нажимать. Конечному пользователю программы обычно намного удобнее работать с Windows-приложением, чем с консольным приложением с его скудным интерфейсом. Платой за это является необходимость спроектировать и реализовать удобный пользовательский интерфейс.
Рассмотрим предыдущий пример, реализованный как Windows-приложение. Начнем с проектирования интерфейса. Вот как может выглядеть форма, позволяющая проводить исследование поведения полинома третьей степени на некотором заданном интервале:
Рис. 1_1 Интерфейс программы, анализирующей поведение полинома
При построении интерфейса использовались такие элементы управления, как метки (текстовые окна для вывода текста) окна редактирования, позволяющие выводить и вводить в них текст и редактировать его, командные кнопки. Для удобства пользователя использовались также окна, позволяющие группировать информацию, отделяя в данном случае исходную информацию от получаемых результатов.
Возникает естественный вопрос, где размещать программный код, позволяющий найти решение задачи. В консольном приложении ответ был очевиден – в процедуре Main, заготовка которой строится по умолчанию, но не содержит никакого кода в своем теле. В Windows-приложении построенная по умолчанию процедура Main не пуста, – ее код выполняет вполне определенную работу – создает и открывает форму, с которой должен работать пользователь. Вполне допустимым является подход, когда процедура Main не корректируется, а вся дальнейшая работа приложения определяется обработчиками событий открывающейся формы.
В нашем примере можно обойтись одним обработчиком, реагирующим на событие Click командной кнопки. В него и поместим весь код, который в консольном приложении размещался в процедуре Main:
private void button1_Click(object sender, System.EventArgs e)
{
//объявление переменных
double a,b,c, d; //коэффициенты полинома
double x1, x2; // исходный интервал
double h; //шаг прохождения интервала
string mes; //сообщение о результатах вычислений
// ввод исходных данных
a = Convert.ToDouble(this.textBoxA.Text);
b = Convert.ToDouble(this.textBoxB.Text);
c = Convert.ToDouble(this.textBoxC.Text);
d = Convert.ToDouble(this.textBoxD.Text);
x1 = Convert.ToDouble(this.textBoxX1.Text);
x2 = Convert.ToDouble(this.textBoxX2.Text);
h = Convert.ToDouble(this.textBoxH.Text);
//вычисления
double x= x1;
double p =Polinom3(a,b,c,d,x);
int signum = Math.Sign(p);
int signum1 = signum;
while(x<x2)
{
x+=h; p =Polinom3(a,b,c,d,x);
signum1=Math.Sign(p);
if( signum1 != signum)break; //обнаружена смена знака
}
//вывод результатов
if(signum == signum1)
{
mes= “Не обнаружена смена знака полинома!”;
this.textBoxMes.Text = mes;
this.textBoxX.Text = x1.ToString();
this.textBoxPX.Text = Polinom3(a,b,c,d,x1).ToString();
this.textBoxY.Text = x2.ToString();
this.textBoxPY.Text = Polinom3(a,b,c,d,x2).ToString();
}
else
{
mes= “Обнаружена смена знака полинома! “;
this.textBoxMes.Text = mes;
this.textBoxX.Text = (x-h).ToString();
this.textBoxPX.Text = Polinom3(a,b,c,d,x-h).ToString();
this.textBoxY.Text = x.ToString();
this.textBoxPY.Text = Polinom3(a,b,c,d,x).ToString();
}
}
Также как и в консольном приложении в построенный по умолчанию класс Form1 можно добавить функцию Polinom3, вызываемую в обработчике события:
double Polinom3(double a, double b,double c,double d,double x)
{
return(((a*x +b)*x+c)*x+d);
}
Отмечу следующее:
- На начальном этапе можно не останавливаться на деталях, связанных с параметрами обработчика события, необходимых операционной системе. Поскольку заготовка обработчика события строится автоматически, то программисту не приходится самому задавать значения этих параметров.
- Для функций, вызываемых из обработчиков событий, не обязательно задавать модификатор static, поскольку сами обработчики не имеют такой модификатор.
- Вся информация, необходимая функции для ее работы, передается через параметры функции.
Также все задачи, где используется “Пусть a, b и c– переменные типов T1, T2 и T3 соответственно.” неявно описанные типы. Необходимо подставить конкретные типы.
Например для целого типа можно найти и использовать следующую диаграмму:
т.е. я могу написать такую программу:
но если так:
также будет работать:
Почему?