Хелпикс

Главная

Контакты

Случайная статья





Программирование на языке C# с использованием подпрограмм



7. Программирование на языке C# с использованием подпрограмм

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

Основные понятия

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

Использование подпрограмм позволяет уменьшить объем памяти, занимаемый кодом программы в целом. Однако время выполнения программы при этом несколько возрастает, так как появляются временные издержки на вызов подпрограмм и возврат из подпрограмм.

В C# подпрограммы представлены функциями. В языке C# структура программы состоит из одного или более классов, поэтому функции не существуют вне классов. Функции классов обычно называют методами классов.

Функции (методы)

Структура функции (метода) включает заголовок и тело функции.

<модификаторы_метода> тип_возвращаемого_значения имя_метода (<спецификация_параметров>)

 { операторы_тела_метода }

Угловые скобки < > указывают на необязательность модификаторов метода и спецификаторов параметров. Фигурные скобки ограничивают тело метода.

Модификаторы метода необязательны, но их достаточно много: new, public, protected, internal, private, static, virtual, sealed, override, abstract, extern.

Если метод возвращает значение, то для указания точки выхода должен использоваться оператор возврата return вместе с возвращаемым значением.

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

При определении метода в его заголовке, как правило, размещается спецификация параметров  – разделённая запятыми последовательность спецификаторов параметров. Параметры, определенные в заголовке функции, называют формальными, а параметры, которые указываются при вызове функции − фактическими. Каждый параметр состоит из имени типа параметра и имени, по которому к нему можно обратиться в теле метода. Каждый спецификатор имеет вид:

<модификатор> тип_параметра имя_параметра

Модификатор параметра может отсутствовать или имеет одну из следующих форм: ref, out, params.

Ограничений на тип параметра не накладывается. Параметр может быть предопределённого типа (базовые типы, строки, object); перечислением; структурой; классом; массивом; интерфейсом; делегатом.

Имя параметра — это идентификатор, который выбирает программист – автор метода. Область видимости и время существования параметра ограничиваются заголовком и телом метода. Таким образом, параметры не видны и недоступны для кода, который не размещён в теле метода.

Стандарт C# включает четыре вида параметров:

− параметры, передаваемые по значениям;

− параметры, передаваемые по ссылкам (ref);

− выходные параметры (out);

− массив-параметр (params).

Параметры первых трёх видов в C# называют фиксированными параметрами. Спецификация фиксированного параметра включает необязательный модификатор ref или out, обозначение типа и идентификатор (имя параметра).

Список параметров представляет собой, возможно пустую, последовательность разделённых запятыми спецификаторов параметров, из которых только последний может быть массивом-параметром, имеющим модификатор params.

Для параметра, передаваемого по значению, при обращении к методу создаётся внутри метода временный объект, которому присваивается значение аргумента. Имя параметра в теле метода соотнесено с этим временным объектом и никак не связано с тем конкретным аргументом, который использован в обращении вместо параметра. Операции, выполняемые в теле метода с участием такого параметра, действуют только на временный объект, которому присвоено значение аргумента.

Независимость аргумента от изменений параметра, передаваемого по значению, иллюстрирует следующая программа:

using System;

 class Program

 { static void F(int p)

{ p++;

 Console.WriteLine("p = {0}", p);

}

static void Main( )

{   int a = 1;

   Console.WriteLine ("pre: a = {0}", a);

   F(a);

  Console.WriteLine("post: a = {0}", a);

}

 }

Результат выполнения программы:

pre: a = 1

 p = 2

 post: a = 1

Изменение параметра p в теле метода F( ) не повлияло на значение аргумента a. Таким образом, как иллюстрирует приведённый пример, параметры, передаваемые по значениям, служат для передачи данных в метод, но не позволяют вернуть какую-либо информацию из метода.

Задача: написать функцию (метод), возвращающую значение минимального из четырёх аргументов. Прямой путь решения этой задачи – использовать в теле метода вложенные условные операторы или условные выражения. Поступим по-другому – определим вспомогательную функцию, которая возвращает минимальное значение одного из двух параметров:

using System;

 class Program

 { // Вспомогательная функция:

 static double min2(double z1, double z2)

 { return z1 < z2 ? z1 : z2; }

 // функция возвращает минимальное из значений параметров: static double min4(double x1, double x2, double x3, double x4)

{   return min2(min2(min2(x1, x2), x3), x4);

}

static void Main()

{ Console.WriteLine (min4 (24,8,4,0.3));

} }

Результат выполнения программы:

0.3

В данном примере оба статических метода min2( ) и min4( ) выступают в роли функций

Чтобы метод мог с помощью параметров изменять внешние по отношению к методу объекты, параметры должны иметь модификатор ref, то есть передаваться по ссылке.

Модификатор ref используется не только перед формальным параметром, но и перед замещающим его фактическим аргументом. Особо следует отметить, что фактическим аргументом может быть только переменная (не константа и не выражение) того же типа, что и формальный параметр.

Для иллюстрации этой возможности модифицируем программу, приведенную выше – снабдим параметр новой функции FR( ) модификатором ref:

 using System;

class Program

 { static void FR (ref int p)

{   p++;

   Console.WriteLine("p = {0}", p);

}

static void Main()

{   int a = 1;

   Console.WriteLine ("pre: a = {0}", a);

   FR(ref a);

   Console.WriteLine ("post: a = {0}", a);

} }

Результат выполнения программы:

 pre: a = 1

 p = 2

 post: a = 2

После обращения к методу FR( ) значение внешней по отношению к методу F( ) (см. пример выше) переменной a изменилось.

Выходные параметры снабжаются модификатором out и позволяют присвоить значения объектам вызывающего метода даже в тех случаях, когда эти объекты значений ещё не имели. Возникает вопрос – зачем нужен модификатор out, если те же действия обеспечивает применение модификатора ref? Связано появление модификатора out с правилом обязательной инициализации переменных. Модификатор out "сообщает" компилятору – «не обращать внимание на отсутствие значения у переменной, которая использована в качестве аргумента», и компилятор не выдаёт сообщения об ошибке. Однако эта переменная обязательно должна получить конкретное значение в теле метода. В противном случае компилятор зафиксирует ошибку. Ошибкой будет и попытка использования в теле метода значения выходного параметра без присваивания ему значения. Метод с выходными параметрами (имеющими модификаторы ref или out) обычно выступает в роли процедуры. Хотя не запрещено такому методу возвращать значение с помощью оператора return.

 В качестве иллюстрации применения выходных параметров приведём метод, возвращающий целую (int integer) и дробную (double fra) части вещественного числа (double x).

 В функции Main( ) определим три переменных: double real – исходное число, double dPart – дробная часть, int iPart – целая часть. Переменная real инициализирована, переменные iPart, dPart не имеют значений до обращения к

методу.

 using System;

 class Program

 {// Метод возвращает значения целой и дробной частей

 // вещественного параметра

static void fun(double x, out int integer, out double fra)

{ integer = (int)x; fra = x - integer;

}

static void Main()

{ double real = 53.93;

 double dPart; int iPart;

 fun(real, out iPart, out dPart);

 Console.WriteLine("iPart={0}, dPart={1}",iPart, dPart);

} }

Результат выполнения программы: iPart=53, dPart=0,93

Необходимо отметить, что при больших значениях аргумента, заменяющего параметр double x, величина (int) x может превысить предельное значение типа int. Это приведёт к неверным результатам за счёт переполнения.

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

 

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

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

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

2. Если же функция должна возвращать некоторое значение, то нормальный выход из нее осуществляется оператором

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

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

Например:

double fsum(double x1, double x2, int a)

{

return a *(x1+x2);

}

Ниже приведен пример функции, не возвращающей никакого значения:

void sprint(string s)

{

if (s = =““) return;

ShowMessage(s);

}

Функции с переменным числом параметров

У параметра с модификатором params назначение особое – он представляет в методе список аргументов неопределённой (заранее не фиксированной) длины. Его тип – одномерный массив с элементами типа, указанного в спецификации параметра.

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

В качестве примера использования params − программа с методом для вычисления значений полинома Pn(x)=a0*x n +a1*x n-1 +...+an-1*x+an:

 // массив-параметр params

 using System;

 class Program

 {//Вычисляет значение полинома c целыми коэффициентами:

static double polynom (double x, params int [] coef)

{ double result = 0.0;

   for (int i = 0; i < coef.Length; i++)

       result = result * x + coef[i];

   return result;

}

static void Main()

{ Console.WriteLine (polynom(3.0, 3, 1, 2));

}

 }

 Результат выполнения программы: 32

Метод polynom() возвращает значение типа double, то есть играет роль функции. Первый параметр double x представляет значение аргумента полинома. Второй параметр params int [ ] coef даёт возможность передать в метод список коэффициентов полинома a0, a1, ...an-1, an. Все коэффициенты должны иметь целочисленные значения. Их реальное количество, то есть степень полинома, при обращении определяется числом использованных аргументов, а в теле метода – значением поля coef.Length.

Параметр, снабжённый модификатором params, обеспечивает передачу аргументов по значению, т.е. значения аргументов после выполнения метода не изменяются.

Рекурсия

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

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

Структура рекурсивной процедуры может принимать три разных формы:

1. Форма с выполнением действий до рекурсивного вызова (с выполнением действий s на рекурсивном спуске):

int rec (int n)

{

s; // действия s выполняются на рекурсивном спуске

if (условие)

              rec(n);

}

2. Форма с выполнением действий после рекурсивного вызова (с выполнением действий s на рекурсивном возврате):

int rec (int n)

{

if (условие)

             rec(n);

s; // действия s выполняются на рекурсивном возврате

}

3. Форма с выполнением действий как до, так и после рекурсивного вызова (с выполнением действий как на рекурсивном спуске, так и на рекурсивном возврате):

int rec (int n)

{

  s1;

  if (условие)

                    rec(n);

     s2;

}

 

int rec (int n)

{

if (условие)

{

   s1;

       rec(n);

   s2;

}

}

Все формы рекурсивных процедур находят применение на практике. Глубокое понимание рекурсивного механизма и умение управлять им является необходимым качеством квалифицированного программиста.

Классический пример рекурсивного метода – функция для вычисления факториала неотрицательного целого числа. На языке C# её можно записать таким образом:

static long fact (int k)

{ if (k < 0) return 0;

if (k == 0 || k == 1) return 1;

    return k * fact(k - 1);

}




  

© helpiks.su При использовании или копировании материалов прямая ссылка на сайт обязательна.