что такое процедура программирование
Что такое процедура программирование
Описание и вызов. В Паскале подпрограммы называются процедурами и функциями и описываются в разделе с тем же названием.
Процедура имеет такую же структуру, как и программа, но с двумя отличиями:
• заголовок процедуры имеет другой синтаксис и включает служебное слово procedure ;
• описание процедуры заканчивается точкой с запятой (а не точкой). Все имена, описанные в программе до процедуры, действуют во всей программе и в любой ее подпрограмме (если они там не описаны заново). Они называются глобальными, в отличие от локальных имен, описанных в процедуре и действующих лишь в ней.
procedure ( ) Описание формальных параметров может иметь вид
Оператор вызова процедуры имеет вид
Указанные выражения называют фактическими параметрами. Их список должен точно соответствовать списку описаний формальных параметров процедуры. Во время вызова процедуры каждому параметру-значению присваивается значение соответствующего фактического параметра и поэтому их обычно используют для передачи входных данных.
Параметры-переменные следует использовать для представления результатов процедуры.
Пример: составим программу, которая с помощью строки символов разделит экран на части, где напечатает таблицу квадратных корней для чисел 1, 2. 10 и таблицу натуральных логарифмов для чисел 1, 2. 5.
Печать строки символов оформим как процедуру. Так как никакую информацию передавать из процедуры в программу не надо, то аргументы процедуры (вид и количество символов) будут описаны как параметры-значения.
Заметим, что процедура в программе выполняется пять раз.
for j:=l to a do write (c);
line(35,’-‘); writeln(‘ таблица квадратных корней ‘);
for x:=l to 10 do writeln(x:8,sqrt(x):8,4);
line (35,’-‘); writein (‘таблица натуральных логарифмов’);
for x:=l to 5 do writein(x:8,In(x):8:4);
• заголовок функции начинается со служебного слова function и заканчивается указанием типа значения функции:
function (список описаний формальных параметров): ;
•раздел операторов функции должен содержать хотя бы один оператор присваивания имени функции;
Функции (и процедуры) могут использовать свое имя в собственном описании, т.е. могут быть рекурсивными.
Пример: составим программу, которая для заданных четырех натуральных чисел а, b, с, d напечатает наибольшие общие делители первой и второй пар чисел и сравнит их по величине.
Применяя эти формулы к числам 21 и 15, последовательно находим nod (21,15) = nod (6,15) = nod (15,6) = nod (3,6) = nod (6,3) = nod (0,3) = nod (3,0) = 3.
function nod(x, у :integer):integer;
writeln (‘введите 4 натуральных числа’);
if m>n then writeln(‘ первый > второго ‘)
else if m первый второго ‘)
else writeln(‘ нод пар равны ‘) end.
Внешние библиотеки. Как известно, подпрограммы (процедуры и функции) используются в программах с целью их структурирования, а также при многократных повторениях некоторых частей программы. Процедуры и функции описываются в программных единицах в разделе описания подпрограмм. Они являются внутренними для этих программных единиц.
Пример. Создадим внешнюю библиотеку из двух процедур и одной функции. Первая процедура программы 20 очищает экран, выдает приветствие, затем после нажатия клавиши снова очищает экран. Вторая процедура возводит число а в степень b. Третья подпрограмма-функция вычисляет значение экспоненты с некоторым грубым приближением на основе ряда Тейлора.
repeat ( цикл позволяет )
gotoxy(35,50);write(‘ пробел ‘); ( сменить экран >
procedure STEPEN(a,b:real;var y:real);
STEPEN(2,4,a); writeln(‘2 в степени 4 =’,a); b:=MEXP(l);
write(‘ машинная exp(1)=’,EXP(1):6:4,’ моя exp(1)=’,b:6:4);
Программист может сам создать модуль. Ниже приведен пример с соответствующими комментариями.
Пример. Создать модуль, дополняющий математические возможности Паскаля арифметическими действиями над комплексными числами.
Будем представлять комплексные числа парами действительных: (а, b). Как известно, действия над ними выполняются по правилам
Обратим внимание, что в интерфейсной части модуля от процедур присутствуют лишь заголовки, а в части «реализация» от заголовков процедур остаются лишь их имена.
procedure Sum(a,b,c,d: real; var x,y: real);
procedure Raz(a,b,c,d: real; var x,y: real);
procedure Proiz (a,b,c,d: real; var x,y: real);
procedure Chstn(a,b,c,d: real; var x,y: real);
begin z:= c*c+d*d; x:=(a*c+b*d)/z; y:=(b*c-a*d)/z end;
Что такое процедура программирование
Пример 1. Предположим, что в нескольких местах программы требуется выводить на экран сообщение об ошибке: Ошибка программы. Это можно сделать, например, так:
вывод «Ошибка программы» write (‘Ошибка программы’);
Конечно, можно вставить этот оператор вывода везде, где нужно вывести сообщение об ошибке. Но тут есть две сложности. Во-первых, при этом строка-сообщение будет храниться в памяти много раз. Во-вторых, если мы задумаем поменять текст сообщения, нужно будет искать эти операторы вывода по всей программе. Для таких случаев в языках программирования предусмотрены процедуры — вспомогательные алгоритмы, которые выполняют некоторые действия.
1.2. Определение ВА
Можно ли взять определения из предыдущего пункта из теории за 5-9 класс?
1.3. Оформление процедур
в школьном алгоритмическом языке и в Паскале
вывод ‘Ошибка программы’
writeln(‘ Ошибка программы ‘)
В школьном алгоритмическом языке процедура оформляется точно так же, как и основной алгоритм, но размещается после основной программы.
Как мы видели, использование процедур сокращает код, если какие-то операции выполняются несколько раз в разных местах программы. Кроме того, иногда большую программу разбивают на несколько процедур для удобства, оформляя в виде процедур отдельные этапы сложного алгоритма. Такой подход делает всю программу более понятной.
1.4. Процедура с параметрами
Процедура Error при каждом вызове делает одно и то же. Более интересны процедуры, которым можно передавать параметры (аргументы) — дополнительные данные, которые изменяют выполняемые действия.
Предположим, что в программе требуется многократно выводить на экран запись целого числа (0..255) в 8-битном двоичном коде. Старшая цифра в такой записи — это частное от деления числа на 128. Далее возьмём остаток от этого деления и разделим на 64 — получается вторая цифра и т. д. Алгоритм, решающий эту задачу для переменной п, можно записать так:
Писать такой цикл каждый раз, когда нужно вывести двоичное число, очень утомительно. Кроме того, легко сделать ошибку или опечатку, которую будет сложно найти. Поэтому лучше оформить этот вспомогательный алгоритм в виде процедуры. Но этой процедуре нужно передать значение параметра — число для перевода в двоичную систему. Программа получается такой:
алг printBin (цел n 0)
Основная программа содержит всего одну команду — вызов процедуры printBin для значения 99. В заголовке процедуры в скобках записывают тип и внутреннее имя параметра (т. е. имя, по которому к нему можно обращаться в процедуре). Значение параметра, переданное из вызывающей программы в процедуру (в этом примере — число 99), обычно называют аргументом.
В школьном алгоритмическом языке запрещено изменять параметры процедуры внутри процедуры, поэтому мы назвали параметр п0, а в начале процедуры скопировали его значение в переменную п.
Параметров может быть несколько, в этом случае они перечисляются в заголовке процедуры через запятую (в школьном алгоритмическом языке) или точку с запятой (в Паскале). Например, процедуру, которая выводит экран среднее арифметическое двух целых чисел, можно записать так:
procedure printSred (a: integer; b: integer);
Если несколько параметров одного типа стоят в списке один за другим, их можно определить списком:
procedure printSred(a, b: integer);
Напишем процедуру, которая меняет местами значения двух переменных. Проще всего для этого использовать третью переменную (пока напишем программу только на Паскале):
procedure Swap(a, b: integer);
После запуска этой программы обнаружится, что значения переменных х и у остались прежними: на экран будет выведено: 2 3. Дело в том, что эта процедура работает с копиями переданных ей параметров. Это значит, что процедура Swap создаёт в памяти временные локальные переменные с именами а и b и копирует в них переданные значения переменных х и у основной программы. Поэтому и все перестановки в нашей программе были сделаны именно с копиями, а значения переменных х и у не изменились. Такая передача параметров называется передачей по значению.
Чтобы решить проблему, нужно явно сказать, чтобы процедура работала с теми же ячейками памяти, что и основная программа. Для этого в Паскале в заголовке процедуры перед именем изменяемого параметра пишут ключевое слово var :
procedure Swap ([ var la, b: integer);
Теперь процедура решает поставленную задачу: на выходе мы увидим: 3 2, что и требовалось. В подобных случаях говорят, что параметры передаются по ссылке, а не по значению. Это означает, что фактически в процедуру передаётся адрес переменной и можно изменять значение этой переменной, записывая новые данные по этому адресу.
В школьном алгоритмическом языке все параметры делятся на аргументы (исходные данные, обозначаются арг) и результаты (ключевое слово рез, эти значения процедура передаёт вызывающей программе). По умолчанию (если не указано иначе) все параметры считаются аргументами. Поэтому два следующих заголовка равносильны:
В нашем случае параметры процедуры одновременно являются и аргументами, и результатами, поэтому их нужно объявлять с помощью ключевого слова аргрез. Приведём полную программу:
Лекция 1.5 Процедуры и функции – методы класса
Первыми формами модульности, появившимися в языках программирования, были процедуры и функции. Поскольку функции в математике использовались издавна, то появление их в языках программирования было совершенно естественным. Уже с первых шагов программирования процедуры и функции позволяли решать одну из важнейших задач, стоящих перед программистами, – задачу повторного использования программного кода. Один раз написанную функцию можно многократно вызывать в программе с разными значениями параметров, передаваемых функции в момент вызова. Встроенные в язык функции позволяли существенно расширить возможности языка программирования. Важным шагом в автоматизации программирования было появление библиотек процедур и функций, доступных из языка программирования.
Процедуры и функции – методы класса
Долгое время процедуры и функции играли не только функциональную, но и архитектурную роль. Весьма популярным при построении программных систем был метод функциональной декомпозиции «сверху вниз», и сегодня еще играющий важную роль. Вся программа рассматривалась, как некоторая главная функция. В процессе проектирования программы происходила декомпозиция главной функции на подфункции, решающие частные задачи. Этот процесс декомпозиции продолжался до тех пор, пока не приходили к достаточно простым функциям, реализация которых не требовала декомпозиции и могла быть описана базовыми конструкциями языка программирования.
с появлением ООП архитектурная роль функциональных модулей отошла на второй план. Для ОО-языков, к которым относится и язык C#, роль архитектурного модуля играет класс. Программная система строится из модулей, роль которых играют классы, но каждый из этих модулей имеют содержательную начинку, задавая некоторую абстракцию данных.
Процедуры и функции связываются теперь с классом, они обеспечивают требуемую функциональность класса и называются методами класса. Поскольку класс в объектно-ориентированном программировании рассматривается как некоторый тип данных, то главную роль в классе начинают играть его данные – поля класса, задающие свойства объектов класса. Методы класса «служат» данным, занимаясь их обработкой. Помните, в C# процедуры и функции существуют только как методы некоторого класса, они не существуют вне класса.
В данном контексте понятие класс распространяется и на все его частные случаи – структуры, интерфейсы, делегаты.
В языке C# нет специальных ключевых слов – method, procedure, function, но сами понятия конечно же присутствуют. Синтаксис объявления метода позволяет однозначно определить, чем является метод – процедурой или функцией.
Прежнюю роль библиотек процедур и функций теперь играют библиотеки классов. Библиотека классов FCL, доступная в языке C#, существенно расширяет возможности языка. Знание классов этой библиотеки, методов этих классов совершенно необходимо для практического программирования на C#, использование всей его мощи.
Процедуры и функции. Отличия
Функция отличается от процедуры двумя особенностями:
Процедура C# имеет свои особенности:
Хорошо известно, что одновременное существование в языке процедур и функций в каком-то смысле избыточно. Добавив еще один выходной аргумент любую функцию можно записать в виде процедуры. Справедливо и обратное. Если допускать функции с побочным эффектом, то любую процедуру можно записать в виде функции. В языке С – дедушке C# – так и сделали, оставив только функции. Однако значительно удобнее иметь обе формы реализации метода – процедуры и функции. Обычно метод предпочитают реализовать в виде функции тогда, когда он имеет один выходной аргумент, рассматриваемый как результат вычисления значения функции. Возможность вызова функций в выражениях также влияет на выбор в пользу реализации метода в виде функции. В других случаях метод реализуют в виде процедуры.
Описание методов (процедур и функций). Синтаксис
Синтаксически в описании метода различают две части – описание заголовка и описание тела метода:
Рассмотрим синтаксис заголовка метода:
Имя метода и список формальных аргументов составляют сигнатуру метода. Заметьте, в сигнатуру не входят имена формальных аргументов, здесь важны типы аргументов. В сигнатуру не входит и тип возвращаемого результата.
Квадратные скобки (метасимволы синтаксической формулы) показывают, что атрибуты и модификаторы могут быть опущены при описании метода. Подробное их рассмотрение будет дано в лекциях, посвященных описанию классов. Сейчас же упомяну только об одном из модификаторов – модификаторе доступа. У него четыре возможных значения, из которых пока рассмотрим только два – public и private. Модификатор public показывает, что метод открыт и доступен для вызова клиентами и потомками класса. Модификатор private говорит, что метод предназначен для внутреннего использования в классе и доступен для вызова только в теле методов самого класса. Заметьте, если модификатор доступа опущен, то по умолчанию предполагается, что он имеет значение private и метод является закрытым для клиентов и потомков класса.
Обязательным при описании заголовка является указание типа результата, имени метода и круглых скобок, наличие которых необходимо и в том случае, если сам список формальных аргументов отсутствует. Формально тип результата метода указывается всегда, но значение void однозначно определяет, что метод реализуется процедурой. Тип результата, отличный от void, указывает на функцию. Вот несколько простейших примеров описания методов:
Методы A и B являются закрытыми, а метод С – открыт. Методы A и С реализованы процедурами, а метод B – функцией, возвращающей целое значение.
Список формальных аргументов
Как уже отмечалось, список формальных аргументов метода может быть пустым и это довольно типичная ситуация для методов класса. Список может содержать фиксированное число аргументов, разделяемых символом запятой.
Рассмотрим теперь синтаксис объявления одного формального аргумента:
Обязательным является указание типа и имени аргумента. Заметьте, никаких ограничений на тип аргумента не накладывается. Он может быть любым скалярным типом, массивом, классом, структурой, интерфейсом, перечислением, функциональным типом.
Несмотря на фиксированное число формальных аргументов, есть возможность при вызове метода передавать ему произвольное число фактических аргументов. Для реализации этой возможности в списке формальных аргументов необходимо задать ключевое слово params. Оно может появляться в объявлении лишь для последнего аргумента списка, объявляемого как массив произвольного типа. При вызове метода этому формальному аргументу соответствует произвольное число фактических аргументов.
Содержательно, все аргументы метода разделяются на три группы: входные, выходные и обновляемые. Аргументы первой группы передают информацию методу, их значения в теле метода только читаются. Аргументы второй группы представляют собой результаты метода, они получают значения в ходе работы метода. Аргументы третьей группы выполняют обе функции. Их значения используются в ходе вычислений и обновляются в результате работы метода. Выходные аргументы всегда должны сопровождаться ключевым словом out, обновляемые – ref. Что же касается входных аргументов, то, как правило, они задаются без ключевого слова, хотя иногда их полезно объявлять с параметром ref, о чем подробнее скажу чуть позже. Заметьте, если аргумент объявлен как выходной с ключевым словом out, то в теле метода обязательно должен присутствовать оператор присваивания, задающий значение этому аргументу. В противном случае возникает ошибка еще на этапе компиляции.
Для иллюстрации давайте рассмотрим группу методов класса Testing из проекта ProcAndFun, сопровождающего эту лекцию:
/// Группа перегруженных методов Cube()
/// первый аргумент — результат
/// представляет сумму кубов
/// произвольного числа оставшихся аргументов
/// Аргументы могут быть разного типа.
void Cube(out long p2, int p1)
p2 = (long)Math.Pow(p1, 3);
void Cube(out long p2, params int[] p)
p2 = 0; for (int i = 0; i
/// Функция с побочным эффектом
Увеличивается на 1
/// значение a на входе
Четыре перегруженных метода с именем Cube и метод F будут использоваться при объяснении перегрузки и побочного эффекта. Сейчас проанализируем только их заголовки. Все методы закрыты, поскольку объявлены без модификатора доступа. Перегруженные методы с именем Cube являются процедурами, метод F – функцией. Все четыре перегруженных метода имеют разную сигнатуру. Хотя имена и число аргументов у всех методов одинаковы, но типы и ключевые слова, предшествующие аргументам, различны. Первый аргумент у всех четырех перегруженных методов является выходным и сопровождается ключевым словом out, в теле метода этому аргументу присваивается значение. Аргумент функции F является обновляемым, он снабжен ключевым словом ref, в теле функции используется его значение для получения результата функции, но и само значение аргумента изменяется в теле функции. Два метода из группы перегруженных методов используют ключевое слово params для своего последнего аргумента. Позже мы увидим, что при вызове этих методов этому аргументу будет соответствовать несколько фактических аргументов, число которых может быть произвольным.
Тело метода
Переменные, описанные в блоке, считаются локализованными в этом блоке. В записи операторов блока участвуют имена локальных переменных блока, имена полей класса и имена аргументов метода.
Знание семантики описаний и операторов достаточно для понимания семантики блока. Необходимые уточнения будут даны чуть позже.
Вызов метода. Синтаксис
Как уже отмечалось, метод может вызываться в выражениях или быть вызван как оператор тела блока. В качестве оператора может использоваться любой метод – как процедура, так и функция. Конечно, функцию разумно вызывать как оператор только, если она обладает побочным эффектом. В последнем случае она вызывается ради своего побочного эффекта, а возвращаемое значение никак не используется. Любое выражение с побочным эффектом может выступать в роли оператора, классическим примером является оператор x++;.
Если же попытаться вызвать процедуру в выражении, то это приведет к ошибке еще на этапе компиляции. Возвращаемое процедурой значение void не совместимо с выражениями. Так что в выражениях могут быть вызваны только функции.
Сам вызов метода, независимо от того, процедура это или функция, имеет один и тот же синтаксис:
Если это оператор, то вызов завершается точкой с запятой.
Формальный аргумент, задаваемый при описании метода, синтаксически является идентификатором – именем аргумента. Фактический аргумент – представляет собой «выражение», значительно более сложную синтаксическую конструкцию. Вот точный синтаксис фактического аргумента:
О соответствии списков формальных и фактических аргументов
Между списком формальных и списком фактических аргументов должно выполняться определенное соответствие по числу, порядку следования, типу и статусу аргументов. Если в первом списке n формальных аргументов, то фактических аргументов должно быть не меньше n (соответствие по числу). Каждому i-му формальному аргументу (для всех i от 1 до n-1) ставится в соответствие i-й фактический аргумент. Последнему формальному аргументу при условии, что он объявлен с ключевым словом params, ставятся в соответствие все оставшиеся фактические аргументы (соответствие по порядку). Если формальный аргумент объявлен с ключевым словом ref или out, то фактический аргумент должен сопровождаться таким же ключевым словом в точке вызова (соответствие по статусу).
Появление ключевых слов при вызове методов – это особенность языка C#, отличающая его от большинства других языков. Такой синтаксис следует приветствовать, поскольку он направлен на повышение надежности программной системы, напоминания программисту о том, что данный фактический аргумент является выходным и значение его наверняка изменится после вызова метода. Однако из-за непривычности синтаксиса при вызове методов эти слова часто забывают писать, что приводит к появлению синтаксических ошибок.
Если T – тип формального аргумента, то выражение, задающее фактический аргумент, должно быть согласовано по типу с типом T. Это означает, что вычисленный тип выражения совпадает c типом T или допускает неявное преобразование к типу T, или является потомком типа T (соответствие по типу).
Если формальный аргумент является выходным – объявлен с ключевым словом ref или out, то соответствующий фактический аргумент не может быть сложным выражением, поскольку используется в левой части оператора присваивания, так что он должен быть именем, которому можно присвоить значение.
Вызов метода. Семантика
Что происходит в момент вызова метода? Выполнение начинается с вычисления фактических аргументов, которые, как мы знаем, являются выражениями. Вычисление этих выражений может приводить в свою очередь к вызову других методов. Так что первый этап может быть довольно сложным и требовать больших временных затрат. В чисто функциональном программировании все вычисление по программе сводится к вызову одной функции, фактические аргументы которой содержат вызовы функций, фактические аргументы которых содержат вызовы функций, и так далее и так далее. И на языке C# можно организовать вычисления подобным образом.
Для простоты понимания семантики вызова можно полагать, что в точке вызова создается блок, соответствующий телу метода (реально все происходит значительно эффективнее). В этом блоке происходит замена имен формальных аргументов фактическими аргументами. Для выходных (ref и out) аргументов, для которых фактические аргументы также являются именами, эта замена или передача аргументов происходит по ссылке, означая замену формального аргумента ссылкой на реально существующий объект, заданный фактическим аргументом. Чуть более сложную семантику имеет вызов по значению, применяемый к формальным аргументам, объявленным без ключевых слов ref и out. При вычислении выражений, заданных такими фактическими аргументами, их значения присваиваются специально создаваемым временным переменным, локализованным в теле исполняемого блока. Имена этих локализованных переменных и подставляются вместо имен формальных аргументов. Понятно, что тип локализованных переменных определяется типом соответствующего формального аргумента.
Заметьте, семантика замены формальных аргументов фактическими эквивалентна семантике присваивания, подробно рассмотренной в предыдущих главах.
Каково следствие семантики вызова по значению? Если вы забыли указать ключевое слово ref или out для аргумента, фактически являющегося выходным, то к нему будет применяться вызов по значению. Даже если в теле метода происходит изменение значения этого аргумента, то оно действует только на время выполнения тела метода. Как только метод заканчивает свою работу (завершается блок) все локальные переменные (в том числе созданные для замены формальных аргументов) оканчивают свое существование, так что изменения не затронут фактических аргументов, и они сохранят свое значение, бывшее у них до вызова. Отсюда вывод: все выходные аргументы значимых типов, значения которых предполагается изменить в процессе работы, должны иметь ключевое слово ref или out.
Говоря о семантике вызова по ссылке и по значению, следует сделать важное уточнение. В объектном программировании, каковым является и программирование на C#, основную роль играют ссылочные типы – мы работаем с классами и объектами. Когда методу передается объект ссылочного типа, то все поля этого объекта могут в методе меняться самым беззастенчивым образом. И это несмотря на то, что объект формально не является выходным, не имеет ключевых слов ref или out, использует семантику вызова по значению. Сама ссылка на объект при этом, как и положено, остается неизменной, но состояние объекта, его поля могут полностью обновиться. Такая ситуация типична и представляет один из основных способов изменения состояния объектов. Именно поэтому ref или out не столь часто появляются при описании аргументов метода.
Что нужно знать о методах?
Знания формального синтаксиса и семантики недостаточно, чтобы эффективно работать с методами. Рассмотрим сейчас несколько важных вопросов, касающихся различных сторон работы с методами класса.
Почему у методов мало аргументов?
Методы класса имеют значительно меньше аргументов, чем процедуры и функции в классическом процедурном стиле программирования, когда не используется концепция классов. За счет чего происходит уменьшение числа аргументов у методов? Ведь аргументы играют важную роль – они передают информацию методу, нужную ему для работы, и возвращают информацию – результаты работы метода – программе, вызвавшей метод.
Все дело в том, что методы класса – это не просто набор процедур и функций, это методы, обслуживающие данные класса. Все поля класса доступны любому методу по определению. Нужно четко понимать, что в момент выполнения программной системы работа идет с объектами – экземплярами класса. Из полей соответствующего объекта – цели вызова – извлекается информация, нужная методу в процессе работы, а работа метода чаще всего сводится к обновлению значений полей этого объекта. Поэтому понятно, что методу не нужно через входные аргументы передавать информацию, содержащуюся в полях. Если в результате работы метода обновляется значение некоторого поля, то, опять-таки не нужен никакой выходной аргумент.
Поля класса или функции без аргументов?
Поля хранят информацию о состоянии объектов класса. Состояние объекта динамически изменяется в ходе вычислений – обновляются значения полей. Часто возникающая дилемма при проектировании класса: что лучше – создать ли поле, хранящее информацию, или создать функцию без аргументов, вычисляющую значение этого поля всякий раз, когда это значение понадобится. Решение дилеммы – это вечный для программистов выбор между памятью и временем, Если предпочесть поле, то это приводит к дополнительным расходам памяти. Они могут быть значительными, когда создается большое число объектов, ведь поле должен иметь каждый объект. Если предпочесть функцию, то это потребует временных затрат на вычисление значения, они могут быть значительными, если функция вызывается многократно, а ее вычисление требует значительно больших затрат в сравнении с выбором текущего значения поля.
Если бы синтаксис описания метода допускал отсутствие скобок у функции (метода), в случае, когда список аргументов отсутствует, то клиент класса мог бы и не знать, обращается он к полю или к методу. Такой синтаксис принят, например, в языке Eiffel. Преимущество такого подхода в том, что изменение реализации никак не сказывается на клиентах класса. В языке C# это не так. Когда мы хотим получить длину строки, то пишем s.Length, точно зная, что Length – это поле, а не метод класса string. Если бы по каким-либо причинам разработчики класса string решили изменить реализацию и заменить поле Length соответствующей функцией, то ее вызов имел бы вид s.Length().
Пример: Две версии класса Account
Продемонстрируем рассмотренные выше вопросы на примере проектирования классов Account и Account1, описывающих такую абстракцию данных, как банковский счет. Определим на этих данных две основные операции – занесение денег на счет и снятие денег. В первом варианте – классе Account – будем активно использовать поля класса. Помимо двух основных полей credit и debit, хранящих приход и расход счета, введем поле balance, задающее текущее состояние счета и два поля, связанных с последней выполняемой операцией. Поле sum будет хранить сумму денег текущей операции, а поле result – результат выполнения операции. Увеличение числа полей класса приведет, как следствие, к уменьшению числа аргументов у методов класса. Вот описание нашего класса:
/// Класс Account определяет банковский счет.
/// простейший вариант с возможностью трех операций:
/// положить деньги на счет, снять со счета, узнать баланс.
public class Account
int debit=0, credit=0, balance =0;
/// Зачисление на счет с проверкой
public void putMoney(int sum)
credit += sum; balance = credit — debit; result =1;
/// Снятие со счета с проверкой
public void getMoney(int sum)
/// Уведомление о выполнении операции
Console.WriteLine(«Операция зачисления денег прошла успешно!»);
Console.WriteLine(«Cумма=<0>, Ваш текущий баланс=<1>»,
Console.WriteLine(«Операция снятия денег прошла успешно!»);
Console.WriteLine(«Cумма=<0>, Ваш текущий баланс=<1>»,
Console.WriteLine(«Операция зачисления денег не выполнена!»);
Console.WriteLine(«Сумма должна быть больше нуля!»);
Console.WriteLine(«Cумма=<0>, Ваш текущий баланс=<1>»,
Console.WriteLine(«Операция снятия денег не выполнена!»);
Console.WriteLine(«Сумма должна быть не больше баланса!»);
Console.WriteLine(«Cумма=<0>, Ваш текущий баланс=<1>»,
Как можно видеть, только у методов getMoney и putMoney имеется один входной аргумент. Это тот аргумент, который нужен по сути дела, поскольку только клиент может решить какую сумму он хочет снять или положить на счет. Других аргументов у методов класса нет, вся информация передается через поля класса. Уменьшение числа аргументов приводит к повышению эффективности работы с методами, так как исчезают затраты на передачу фактических аргументов. Но за все надо платить. В данном случае усложняются сами операции работы со вкладом, поскольку нужно в момент выполнения операции обновлять значения многих полей класса. Закрытый метод Mes вызывается после выполнения каждой операции, сообщая о том, как прошла операция и информируя клиента о текущем состоянии его баланса.
А теперь спроектируем аналогичный класс Account1, отличающийся только тем, что у него будет меньше полей. Вместо поля balance в классе появится соответствующая функция с этим же именем, вместо полей sum и result появятся аргументы у методов, обеспечивающие необходимую передачу информации. Вот как выглядит этот класс:
/// Класс Account1 определяет банковский счет.
/// Вариант с аргументами и функциями
public class Account1
int debit=0, credit=0;
/// Зачисление на счет с проверкой
public void putMoney(int sum)
if (sum >0)credit += sum;
/// Снятие со счета с проверкой
public void getMoney(int sum)
/// Уведомление о выполнении операции
void Mes(int result, int sum)
Console.WriteLine(«Операция зачисления денег прошла успешно!»);
Console.WriteLine(«Cумма=<0>, Ваш текущий баланс=<1>»,
Console.WriteLine(«Операция снятия денег прошла успешно!»);
Console.WriteLine(«Cумма=<0>, Ваш текущий баланс=<1>»,
Console.WriteLine(«Операция зачисления денег не выполнена!»);
Console.WriteLine(«Сумма должна быть больше нуля!»);
Console.WriteLine(«Cумма=<0>, Ваш текущий баланс=<1>»,
Console.WriteLine(«Операция снятия денег не выполнена!»);
Console.WriteLine(«Сумма должна быть не больше баланса!»);
Console.WriteLine(«Cумма=<0>, Ваш текущий баланс=<1>»,
Сравнивая этот класс с классом Account, можно видеть, что число полей сократилось с 5 до двух, упростились основные методы getMoney и putMoney. Но в качестве платы у класса появился дополнительный метод balance(), у метода Mes теперь появились два аргумента. Какой класс лучше? Однозначно сказать нельзя, все зависит от контекста, приоритетов, заданных при создании конкретной системы.
Приведу процедуру класса Testing, тестирующую работу с классами Account и Account1:
public void TestAccounts()
Account myAccount = new Account();
//Аналогичная работа с классом Account1
Console.WriteLine(«Новый класс и новый счет!»);
Account1 myAccount1 = new Account1();
На рис. 1_5.1 показаны результаты работы этой процедуры.
Рис. 1_5.1. Тестирование классов Account и Account1
Функции с побочным эффектом
Функция называется функцией с побочным эффектом, если помимо результата, вычисляемого функцией и возвращаемого ей в операторе return, она имеет выходные аргументы с ключевыми словами ref и out. В языках C/C++ функции с побочным эффектом применяются сплошь и рядом. Хороший стиль ОО-программирования не рекомендует использование таких функций. Выражения, использующие функции с побочным эффектом, могут потерять свои прекрасные свойства, присущие им в математике. Если F(a) – функция с побочным эффектом, то a + F(a) может быть не равно F(a) + a, так что теряется коммутативность операции сложения.
Примером такой функции является функция F, приведенная выше. Вот тест, демонстрирующий потерю коммутативности сложения при работе с этой функцией:
/// тестирование побочного эффекта
public void TestSideEffect()
Console.WriteLine(«a=<0>, b=<1>, c=<2>»,a, b, c);
На рис. 1_5.2 показаны результаты работы этого метода.
Рис. 1_5.2. Демонстрация вызова функции с побочным эффектом
Обратите внимание на полезность указания ключевого слова ref в момент вызова. Его появление хоть как-то оправдывает некоммутативность сложения.
Напомню, что и выражения с побочным эффектом также приводят к потере коммутативности сложения и умножения. Выражение x + ++x не эквивалентно выражению ++x + x.
Методы. Перегрузка
Должно ли быть уникальным имя метода в классе? Нет, это не требуется. Более того, проектирование методов с одним и тем же именем является частью стиля программирования на С++ и стиля C#. Существование в классе методов с одним и тем же именем называется перегрузкой, а сами одноименные методы называются перегруженными.
Перегрузка методов полезна, когда требуется решать подобные задачи с разным набором аргументов. Типичный пример – это нахождение площади треугольника. Площадь можно вычислить по трем сторонам, по двум углам и стороне, по двум сторонам и углу между ними и при многих других наборах аргументов. СчиПродемонстрируем рассмотренные выше вопросы на примере проектирования классов Account и Account1, описывающих такую абстракцию данных, как банковский счет. Определим на этих данных две основные операции – занесение денег на счет и снятие денег. В первом варианте – классе Account – будем активно использовать поля класса. Помимо двух основных полей credit и debit, хранящих приход и расход счета, введем поле balance, задающее текущее состояние счета и два поля, связанных с последней выполняемой операцией. Поле sum будет хранить сумму денег текущей операции, а поле result – результат выполнения операции. Увеличение числа полей класса приведет, как следствие, к уменьшению числа аргументов у методов класса. Вот описание нашего класса:тается удобным во всех случаях иметь для метода одно имя, например Square, и всегда, когда нужно вычислить площадь, не задумываясь вызывать метод Square, передавая ему известные в данный момент аргументы.
Пример этот может быть не совсем удачен, поскольку при перегрузке сигнатуры реализаций должны отличаться, а для вычисления площади передаются три аргумента, вообще говоря, одного типа. Так что в этом случае придется использовать искусственные приемы, например, объявляя стороны треугольника типа float, а углы типа – double.
Перегрузка характерна и для знаков операций. В зависимости от типов аргументов один и тот же знак может выполнять фактически разные операции. Классическим примером является знак операции сложения +, который играет роль операции сложения не только для арифметических данных разных типов, но и выполняет конкатенацию строк.
Перегрузка требует уточнения семантики вызова метода. Когда встречается вызов не перегруженного метода, то имя метода в вызове однозначно определяет, тело какого метода должно выполняться в точке вызова. Когда же метод перегружен, то знания имени недостаточно – оно не уникально. Уникальной характеристикой перегруженных методов является их сигнатура. Перегруженные методы, имея одинаковое имя, должны отличаться либо числом аргументов, либо их типами, либо ключевыми словами (заметьте, с точки зрения сигнатуры ключевые слова ref и out не отличаются). Уникальность сигнатуры позволяет вызвать требуемый перегруженный метод.
Выше уже были приведены четыре перегруженных метода с именем Cube, отличающиеся сигнатурой. Методы отличаются типами аргументов и ключевым словом params. Когда вызывается метод Cube с двумя аргументами, то в зависимости от типа будет вызываться реализация, не содержащая аргумент с модификатором params. Когда же число аргументов больше двух, то работает реализация, позволяющая справиться с заранее не фиксированным числом аргументов. Заметьте, эта реализация может прекрасно работать и для случая двух аргументов, но полезно иметь частные случаи для фиксированного набора аргументов. При поиске подходящего перегруженного метода частные случаи получают предпочтение в сравнение с общим случаем.
Насколько полезна перегрузка методов? Здесь нет экономии кода, поскольку каждую реализацию нужно задавать явно, нет выигрыша по времени, скорее требуются определенные затраты на поиск подходящей реализации, который может приводить к конфликтам, к счастью, обнаруживаемым на этапе компиляции. В нашем примере вполне разумно было бы отказаться от перегрузки и иметь четыре метода с разными именами, осознанно вызывая метод, применимый к конкретным данным.
Есть ситуации, где перегрузка полезна, недаром она широко используется при построении библиотеки FCL. Возьмем, например класс Convert, у которого 16 методов с разными именами, зависящими от целевого типа преобразования. Каждый из этих 16 методов перегружен и в свою очередь имеет примерно 16 реализаций в зависимости от типа источника. Согласитесь, что-то неразумное было бы иметь в классе Convert 256 методов вместо 16 перегруженных методов. Впрочем, также неразумно было бы иметь один перегруженный метод, имеющий 256 реализаций. Перегрузка – это инструмент, которым следует пользоваться с осторожностью и обоснованно.
В заключение этой темы, посмотрим, как проводилось тестирование работы с перегруженными методами:
/// Тестирование перегруженных методов Cube()
public void TestLoadMethods()
long u = 0; double v = 0;
Cube(out u, 7); Cube(out v, 7.5);
Console.WriteLine(«u= <0>, v= <1>», u, v);
Cube(out v, 7.5, Math.Sin(11.5) + Math.Cos(13.5), 15.5);
Console.WriteLine(«u= <0>, v= <1>», u, v);
На рис. 1_5.3 показаны результаты этого тестирования.
Рис. 1_5.3 Тестирование перегрузки методов
Архитектура проекта
Как обычно для поддержки примеров этой главы создано Решение с именем Ch5, содержащее консольный проект ProcAndFun. Помимо автоматически созданного класса Program в проект добавлены три класса – Testing, Account, Account1. В Main процедуре класса Program создается объект testObject класса Testing, вызывающий методы этого класса. Каждый из методов представляет собой тест, позволяющий на примере пояснить излагаемый материал.
Задачи и алгоритмы
Слово, число, рисунок, нота,– величайшие изобретения человечества. Для программистов это информационные объекты, с которыми нужно уметь оперировать.
Числа
Алгоритмы и задачи, рассматриваемые в этой главе, можно использовать на начальном этапе обучения программированию при изучении простейшего вида модульности – процедур и функций.
Многие задачи этой главы являются хорошими примерами при изучении темы классов. У класса двойственная природа – с одной стороны это модуль, с другой – тип данных. Рациональные числа, комплексные числа, простые числа, являются естественными примерами классов, поскольку интуитивно понятно, какой тип данных они задают.
Цифры. Системы счисления
Для записи чисел привычным способом, знакомым еще с первых классов школы, является их запись в позиционной системе счисления. Напомним некоторые факты. В позиционной системе счисления всегда есть цифра 1. Считается, что единицу создал бог, а остальные цифры придуманы человеком. Если так, то наиболее замечательной из человеческих придумок в этой области является введение цифры 0. Цифры позиционной системы упорядочены и каждая получатся из предыдущей прибавлением единицы. Число различных цифр в позиционной системе счисления задает основание системы счисления – p. В привычной для нас десятичной системе счисления p = 10 и цифрами являются знакомые всем символы: 0, 1, 2, … 9. В двоичной системе счисления цифр всего две – 0 и 1 и p = 2. В шестнадцатеричной системе счисления p =16 и привычных символов для обозначения цифр не хватает, так что дополнительно используются большие буквы латинского алфавита: 0, 1, 2, … 9, A, B, C, D, E, F, где A задает 10, а F – цифру 15. Поскольку в любой позиционной системе счисления цифры задают числа от 0 до p-1, то для числа p уже нет специального символа. Как следствие, в любой позиционной системе счисления основание системы счисления представляется числом 10, так что справедливы следующие соотношения: 2(10) = 10(2); 16(10) = 10(16); p(10) = 10(p). Здесь и в дальнейшем при записи числа при необходимости будем указывать в круглых скобках и систему счисления. В обыденной жизни непреложным фактом является утверждение «2*2=4». Мы понимаем, что столь же верным является утверждение «2*2 = 11» (в троичной системе счисления), или, если хотите «2*2 = 10», – все зависит от системы счисления, в которой ведутся вычисления.
Еще Эйлер занимался записью чисел в различных системах счисления. В его записных книжках можно найти запись числа π в двоичной системе счисления и запись чисел в системе с основанием 24, цифры которой он обозначал буквами латиницы.
Понимание соотношения (1) достаточно для решения большинства задач, рассматриваемых в этом разделе, являющихся частными случаями следующей задачи: дано представление числа в системе с основанием p, требуется найти его представление в системе с основанием q. (Пример: найти запись числа N=2BA37,5F(16) в троичной системе счисления).
Рассмотрим возможную схему решения подобных задач. Зная основание системы счисления p и цифры в записи числа в этой системе счисления, нетрудно вычислить значение числа N в десятичной системе счисления. Для этого достаточно воспользоваться соотношением (1), в котором значения цифр и основание системы счисления – p задаются в десятичной системе и в ней же ведутся вычисления. Например:
Сложнее, но достаточно просто решается и обратная задача. Зная значение числа N в десятичной системе, нетрудно получить цифры, задающие его запись в системе с основанием q. Представим число N в виде суммы целой и дробной частей N= C+D. Рассмотрим схему получения цифр отдельно для целой части C и дробной – D. Пусть в системе с основанием q число C представимо в виде C = cncn-1…c0. Тогда нетрудно получить его последнюю цифру c0 и число M = cncn-1…c1, полученное отбрасыванием последней цифры в записи числа C. Для этого достаточно воспользоваться операциями деления нацело и получения остатка при делении нацело: c0 = C%p; M = C/p;
Применяя этот прием n раз, получим все цифры в записи числа C. Заметьте, имеет место соотношение: N= (N/p)*p + N%p, где в соответствии с языком C# операция / означает деление нацело, а % – остаток от деления нацело. Чтобы получить все цифры и сохранить их в массиве, достаточно эту схему вставить в соответствующий цикл, что схематично можно представить следующим почти программным текстом:
Для получения цифр дробной части применяется та же схема, но с некоторой модификацией. Если цифры целой части вычисляются, начиная с последней, младшей цифры числа, то цифры дробной части вычисляются, начиная с первой цифры после запятой. Для ее получения достаточно имеющуюся дробь умножить на основание системы счисления и в полученном результате взять целую часть. Для получения последующих цифр этот процесс следует применять к числу M, представляющему дробь, из которой удалена первая цифра:
d1 =[D*p]; m=
Чтобы перейти от системы счисления p к системе счисления q, всегда ли следует использовать десятичную систему в качестве промежуточной: p→ 10 → q? Нет, не всегда. В ряде случаев удобнее использовать прием, основанный на следующем утверждении:
Для обратного перехода из q в p достаточно сгруппировать цифры системы q и каждую группу из k цифр заменить одной цифрой системы p. Доказательство этих утверждений оставляю читателю.
Римская система счисления
Еще сегодня для записи целых чисел, в особенности дат, используется римская система счисления. Эта система записи чисел не является позиционной. В ее основе лежит понятие человеческих рук с их пятью и десятью пальцами. Поэтому в этой системе есть цифры 1, 5 и 10, записываемые с помощью символов I, V, X. Помимо этого есть еще четыре цифры – 50, 100, 500, 1000, задаваемые символами L, C, D, M. В этой системе нет цифры 0 и она не является позиционной. С помощью цифр римской системы обычно записывают целые числа, не превышающие 4000. Запись числа представляет собой последовательность подряд идущих цифр, а значением числа является сумма цифр в его записи. Например число III означает I + I + I = 3. В записи числа старшие цифры предшествуют младшим, например CVI = C+V+I = 100+5+1=106. Из этого правила есть одно исключение. Младшая цифра может предшествовать старшей цифре и тогда вместо сложения применяется вычитание, например CIX = C+X-I = 100+10-1=109.
Задачи
Если d | m и d | n, то d является общим делителем чисел m и n. Среди всех общих делителей можно выделить наибольший общий делитель, обозначаемый как НОД(m, n). Если НОД(m, n) = 1, то числа m и n называются взаимно простыми. Простые числа взаимно просты, так что НОД(q, p) =1, если q и p – простые числа.
Если m | A и n | A, то A является общим кратным чисел m и n. Среди всех общих кратных можно выделить наименьшее общее кратное, обозначаемое как НОК(m, n). Если НОК(m, n) = m*n, то числа m и n являются взаимно простыми. НОК(q, p) =q*p, если q и p – простые числа.
Если через Pm и Qn обозначить множества всех простых множителей чисел m и n, то
Если получено разложение чисел m и n на простые множители, то, используя приведенные соотношения, нетрудно вычислить НОД(m,n) и НОК(m,n). Существуют и более эффективные алгоритмы, не требующие разложения числа на множители.
Алгоритм Эвклида
Эффективный алгоритм вычисления НОД(m,n) предложен еще Эвклидом. Он основывается на следующих свойствах НОД(m,n), доказательство которых предоставляется читателю:
Если m > n, то по третьему свойству его можно уменьшить на величину n. Если же m n) m = m — n;