Урок 3. Функции, определенные пользователем
В ходе написания програмного кода часто встречается необходимость повторного использования некоторых фрагментов кода, что приводит к необходимости копирования повторяющихся фрагментов. Поэтому во многих языках программирования реализована возможность создания таких програмных компонентов как процедуры, функции, подпрограммы. 4Test не является исключением. В нем можно реализовать функции для дальнейшего использования в различных участках скрипта.
Что такое функция. Это некоторый фрагмент кода, сгруппированый в некотором блоке с определенным именем и применяемый как часть выражения. У функции есть входные параметры - значения, которые передаются в функцию; и возвращаемое значение - результат, который выдает данная функция.
Общий вид
Структура функции в общем случае имеет вид:
[<scope>] [<return_type>] <function_name> ( [<parameter_list>] )
<statements>
Здесь,
- <scope> - область видимости функции. Возможны 2 значения:
- private - функция доступна только в том файле, в котором она определена
- public - функция доступна во всех файлах, использующих файл, в котором определена данная функция. Этот модификатор определен по умолчанию
- <return_type> - тип возвращаемого значения. Если тип не указан, то по умолчанию используется VOID. Если же указан некоторый тип ( отличный от VOID ), то в теле функции обязательно должна присутствовать конструкция return <expr>, где expr - выражение, значение которого является величиной типа возвращаемого значения или приводится к данному типу путем стандартных преобразований типов. При этом нужно проследить, чтобы не было вариантов выполнения функции, при которых функция завершит выполнение при этом не будет выполнен какой-либо return.
- <function_name> - имя функции. Представляет собой последовательность числовых, буквенных символов и символа подчеркивания, при этом первый символ не должен быть числовым
- <parameter_list> - список аргументов, передаваемых функции. Представляет собой перечень объявлений в формате [<способ_передачи>] <тип_параметра> <имя_параметра> [optional], перечисленных через запятую.
- <statements> - тело функции, набор конструкций, которые выполняются внутри функции
В качестве примера опишем функцию Join, которая объединяет элементы списка строк ( передаваемого первым параметром ) в одну строку, в которой каждый элемент списка разделен строкой-разделителем, передаваемой вторым параметром. Результат выполнения функции - объединенная строка. Для этого создадим файл Functions.inc в дальнейшем будем туда добавлять наши функции, которые будут использоваться в примерах. Реализация имеет вид:
|
Code
|
[+] STRING Join( LIST OF STRING lsInput , STRING sSep )
[ ] STRING sValue
[ ] STRING sResult = ""
[ ]
[+] for each sValue in lsInput
[ ] sResult += sValue + sSep
[ ]
[ ] return sResult
|
Теперь попробуем воспользоваться данной функцией. Создадим файл Lesson03.t и реализуем в нем вызов данной функции:
|
Code
|
[ ] // Lesson 3: User-Defined functions usage sample
[ ] use "Functions.inc"
[ ]
[+] main()
[+] LIST OF STRING lsValue = {...}
[ ] "Test1"
[ ] "line"
[ ] "Test2"
[ ] "text"
[ ] STRING sValue
[ ]
[ ]
[ ] sValue = Join( lsValue , " " )
[ ] Print( sValue )
[ ]
|
В результате будет выведена строка Test1 line Test2 text, что свитетельствует о корректности вызова функции. Функции являются частью выражения. При этом нужно использовать ее в выражениях, оперирующих с тем же типом, что и возвращаемое значение данной функции или чтобы хотя бы тип возвращаемого значения приводился к типу данных, используемому в выражении.
Режимы передачи параметров
Теперь перейдем к рассмотрению отдельных моментов в написании функции. Как уже было указано выше, в списке аргументов присутствует такая составляющая, как b><способ_передачи>, который отвечает за то, как параметр передается в функцию. Различается 2 основных способа передачи параметров:
- по значению - функция оперирует именно со значением данного параметра, но не с объектом, который был передан в качестве параметра;
- по ссылке - параметр фактически ссылается на переменную, которая была передана.
Иными словами, если мы передаем некоторую переменную в качестве параметра функции, то при передаче по значению исходная переменная не меняется ( функция создает ее копию ), а при передаче по значению есть возможность менять значение данной переменной внутри внешней функции. Для чего нужно такое разделение. Иногда нужно, чтобы функция возвращала не одно значение, а несколько. С помощью return можно вернуть только одно значение. Можно, конечно, сделать так, чтобы возвращался список или record, но это не всегда является удачным решением. Вот поэтому иногда удобно передать в качестве некоторого параметра адрес переменной, в которую будет помещен результат некоторых вычислений. То есть помимо возвращаемого значения функции будет еще и измененная переменная.
В функциях 4Test-а есть 3 способа передачи параметров, описываемых соответствующими модификаторами:
- in - параметр передается по значению ( модификатор по умолчанию )
- out - параметр передается по ссылке, при этом изначально расчитано на то, что в данную переменную значение будет только записываться. Это как раз и обеспечивает возможность возврата более чем одной величины.
- inout - параметр передается по ссылке и используется как для чтения так и для записи.
Таким образом, исходя из вышесказанного, функция Join из предыдущего примера фактически объявлена в виде
STRING Join( in LIST OF STRING lsInput , in STRING sSep )
То есть фактически мы передавали параметры по значению, но поскольку модификатор in ставится по умолчанию, то его явно указывать нет необходимости. Если бы внутри данной функции как-то менялись переменные-параметры, то эти изменения не затронули бы фактически переданную переменную.
В качестве примера использования оператора inout напишем функцию Inc, которая принимает параметром целое число и увеличивает его на 1. При этом ничего не возвращается. Код функции имеет вид:
|
Code
|
[+] INTEGER Inc( inout INTEGER iVal )
[ ] return (iVal++)
|
Затем в коде можно использовать ее так:
|
Code
|
[ ] INTEGER i = 3
[ ] Inc( i )
[ ] Print( i )
|
После выполнения этого кода переменная i содержит значение 4.
При использовании модификаторов out или inout нужно помнить, что в этом случае передается ссылка на некоторую переменную, поэтому при попытке передать константу в качестве параметра в данном случае возникнет исключение.
Опциональные параметры
Часто могут возникать ситуации, когда некоторый параметр удобно использовать по умолчанию и не передавать явно. Это наиболее характерно в случае необходимости расширения существующей функциональности. Например, у нас была некоторая функция с определенным набором параметров, и возникла необходимость добавить еще один параметр, который определяет некоторую дополнительную возможность данной функции. При этом после изменения данной функции нам нужно внести изменения во всех местах, где данная функция используется, так как количество передаваемых параметров изменилось. Вот в таких случаях крайне удобно, когда параметр является необязательным. Это позволяет расширить набор параметров функции, при этом нет необходимости корректировать все вхождения данной функции. То есть опциональные параметры позволяют реализовать такую вещь, как обратная совместимость. Другим характерным случаем, когда удобно использовать опциональный параметр, является случай, когда у функции есть некоторый наиболее часто используемый набор параметров, при этом иногда используется другой набор параметров. В этом случае наиболее употребимая запись данной функции просто становится наиболее компактной, что ускоряет процесс написания кода.
Итак, как это реализуется. В объявлении функции некоторый параметр может содержать ключевое слово optional, что указывает на необязательность данного параметра. Но указать опциональность некоторого параметра мало, нужно еще обеспечить обработку случая, когда параметр явно не был передан.
Если параметр явно не был передан, то он имеет значение NULL, если передается по значению ( модификатор in ) или <unset>, если параметр передается по ссылке ( модификатор out, inout). Это нужно обязательно учитывать, так как выражения, в которых одна из компонент содержит подобные значения, могут вызвать исключение. Поэтому, как правило нужно отдельно отследить ситуацию, когда параметр не передан и присвоить некоторое значение по умолчанию. В качестве примера напишем функцию, которая выводит в файл результатов сообщение об ошибке, текст которого передается первым параметром, и если второй параметр типа BOOLEAN содержит величину TRUE, то генерируется исключение. По умолчанию второй параметр равен FALSE. В Functions.inc запишем такую функцию:
|
Code
|
[+] VOID Error( STRING sErrorMessage , BOOLEAN bCritical optional )
[ ] LogError( sErrorMessage )
[ ]
[ ] // Setting value by default
[+] if( IsNull( bCritical ) )
[ ] bCritical = TRUE
[ ]
[+] if( bCritical )
[ ] raise -1 , sErrorMessage
|
В последнем примере выделены основные моменты. Во-первых, указано, где именно устанавливается опциональность параметра, а во-вторых, показано каким образом выставляется значение по умолчанию, если параметр явно не был передан. В данном случае используется стандартная функция IsNull, которая принимает параметром некоторую переменную, которую надо проверить на равенство NULL, и возвращает TRUE, если переданная переменная содержит в себе NULL. Если опциональный параметр передается с модификаторами in или inout, то помимо IsNull нужно использовать IsSet, которая проверяет переменную на равенство <unset>. Последний пример написан в том виде, в котором он представлен выше, с целью выделить важные для рассматриваемого случая составляющие. На самом деле данная функция может быть немного упрощена:
|
Code
|
[+] VOID Error( STRING sErrorMessage , BOOLEAN bCritical optional )
[ ] LogError( sErrorMessage )
[+] if( IsNull( bCritical ) || bCritical )
[ ] raise -1 , sErrorMessage
|
Эта конструкция в данном случае аналогична предыдущей, разве что не устанавливает значение bCritical, если параметр не передан явно, а сразу генерирует исключение.
При использовании необязательных параметров крайне желательно необязательные параметры объявлять после обязательных и располагать их в порядке уменьшения интенсивности использования, чтобы свести к минимуму ситуации, когда некоторый параметр нужно передать по умолчанию, а следующий за ним параметр надо указать явно. В таких случаях необязательный параметр не будет пропущен.
Передача неопределенного количества параметров
В некоторых случаях заранее неизвестно количество аргументов, которое может передаваться в некоторую функцию. Причем неизвестно не только количество элементов, но и в общем случае неизвестны типы. Ярким примером такой функции является функция Print, которая используется для вывода текста в файл результатов. В примерах она использовалась с одним параметром, но в общем случае число параметров для этой функции неограничено. Как реализовать подобную возможность. Для этого в функцию нужно передать такую структуру как varargs OF <type>. Это специальная структура, используемая в функциях, которая по своему поведению аналогична списку ( фактически это и есть список ) с той разницей, что элементы данной структуры являются параметрами, которые переданы функции. При этом и в этом случае в функцию могут передаваться фиксированные параметры. Единственное условие, которое нужно при этом соблюдать - фиксированные параметры должны следовать левее переменных параметров.
В качестве примера опишем функцию, которая отображает список переданных параметров в виде: <description>:<value>, где <description> - строка, передаваемая первым параметром, а <value> - значение аргумента из списка переданных параметров, количество которых произвольно. Поскольку эта функциональность является локальной (то есть за пределами данного урока она использоваться не будет), то ее имеет смысл объявить в файле Lesson03.t сразу под блоком подключения файлов. Таким образом функция описывается в виде:
|
Code
|
[+] private VOID _printArgs( STRING sDescription , varargs OF ANYTYPE laParams )
[ ] ANYTYPE aValue
[ ]
[+] for each aValue in laParams
[ ] Print( sDescription + ": {aValue}")
|
И затем внутри main допишем строки:
|
Code
|
[ ] _printArgs( "Test" , 1 , "Some text" , STRING , 3.4 )
[+] _printArgs( "Values" , {1,2,3} , {...} )
[ ] "val1"
[ ] "val2"
[ ] "val3"
|
Результатом выполнения последнего фрагмента кода будет текст в файле результата вида:
[ ] Test: 1
[ ] Test: Some text
[ ] Test: STRING
[ ] Test: 3.400000
[ ] Values: {1, 2, 3}
[ ] Values: {val1, val2, val3}
|
То есть, как видно из примера, параметры напечатались в том виде, в котором они были переданы. При этом количество параметров и их тип произвольны.
Рекурсия
Могут возникнуть случаи, когда некоторая функция вызывается внутри самой себя. Такие вызовы называются рекурсивными. Такое характерно для обхода иерархических структур ( например дерево каталогов ). Также данный прием применяется для вычисления выражений, заданных рекуррентно ( то есть зависящих от предыдущих значений ).
При использовании такого приема как рекурсия необходимо обязательно предусмотреть условие выхода, чтобы функция не зацикливалась. В качестве примера использования рекурсии, напишем функцию, вычисляющую факториал от числа. Где здесь может быть применена рекурсия? Воспользуемся таким свойством, как:
n! = ( n - 1 )! * n ; n = 1...∞
И при этом учтем, что 0! = 1 , а также предусмотрим случай, когда параметром функции будет передано отрицательное число. В этом случае генерируем исключение. Добавим в файл Functions.inc строки вида:
|
Code
|
[+] INTEGER Factorial( INTEGER iVal )
[ ] // Check for iVal value correctness
[+] if( iVal < 0 )
[ ] raise -1 , "Parameter to Factorial function should be greater or equal to 0"
[ ]
[ ] // Check if iVal equals 0
[+] if( iVal <= 1 )
[ ] return 1
[+] else
[ ] return iVal * Factorial( iVal - 1 )
|
Выделенный фрагмент представляет собой рекурсивный вызов функции. Как видно из примера, такой вызов оставлен напоследок, когда все необходимые условия проверены. Подобного принципа желательно придерживаться и в дальнейшем при написании подобных функций.
Динамический вызов функций
При определенных организациях основного цикла выполнения может возникнуть необходимость обращаться к функциям в общем виде, зная только их имя и набор параметров. Даже есть такой способ организации основного цикла выполнения, в котором фактические значения извлекаются из внешних файлов, а в скриптах находится только обработка в общем виде. Соответственно есть необходимость в некотором динамическом способе вызова функции.
По аналогии с переменными, можно воспользоваться оператором @. Например, функцию Factorial из предыдущего пункта можно вызвать так:
|
Code
|
[ ] INTEGER iValue = @( "Factorial" )( 4 )
|
При этом появляется одно ограничение - параметры должны задаваться явно, при этом типы обязательно должны соответствовать. Если реализовывать все функции по общему шаблону, то данное ограничение трудностей не вызывает, но в общем случае нужно воспользоваться чем-то другим.
Этим "чем-то другим" является оператор ArgListCall. У него есть 2 операнда:
- Имя вызываемой функции
- Список аргументов, который передается как LIST OF ANYTYPE
Возвращает данный оператор возвращаемое значение вызываемой функции. Используя этот оператор можно реализовать цикл, в котором вызываются различные функции с различным набором параметров, при этом имена функций и параметры хранятся отдельно в списках. Попробуем организовать такой цикл для функций Factorial,Join. В файле Lesson03.t допишем строки:
|
Code
|
[+] LIST OF STRING lsNames = {...}
[ ] "Join"
[ ] "Factorial"
[+] LIST OF LIST OF ANYTYPE llaArgs = {...}
[+] {...}
[+] {...}
[ ] "Line"
[ ] "split"
[ ] "into"
[ ] "list"
[ ] " - "
[+] {...}
[ ] 6
[ ]
[+] for iValue = 1 to 2
[+] do
[ ] Print( ArgListCall( lsNames[iValue] , llaArgs[iValue] ) )
[+] except
[ ] ExceptPrint()
|
Это только пример, расчитанный на 2 вызова функций. Результат выполнения этого фрагмента кода:
[ ] Line - split - into - list -
[ ] 720
|
Тот же самый результат выдаст последовательный вызов используемых функций с соответствующими параметрами, что говорит о том, что мы осуществили динамический вызов функций корректно.
Данная возможность позволяет увеличить гибкость кода, расширяемость функциональности. Это в свою очередь способствует созданию более сложных механизмов организации построения и выполнения скриптов.
Листинги
Functions.inc
|
Code
|
[ ]
[+] STRING Join( LIST OF STRING lsInput , STRING sSep )
[ ] STRING sValue
[ ] STRING sResult = ""
[ ]
[+] for each sValue in lsInput
[ ] sResult += sValue + sSep
[ ]
[ ] return sResult
[ ]
[+] VOID Error( STRING sErrorMessage , BOOLEAN bCritical optional )
[ ] LogError( sErrorMessage )
[+] if( IsNull( bCritical ) || bCritical )
[ ] raise -1 , sErrorMessage
[ ]
[+] INTEGER Factorial( INTEGER iVal )
[ ] // Check for iVal value correctness
[+] if( iVal < 0 )
[ ] raise -1 , "Parameter to Factorial function should be greater or equal to 0"
[ ]
[ ] // Check if iVal equals 0
[+] if( iVal <= 1 )
[ ] return 1
[+] else
[ ] return iVal * Factorial( iVal - 1 )
[ ]
[+] INTEGER Inc( inout INTEGER iVal )
[ ] return (iVal++)
[ ]
|
Lesson03.t
|
Code
|
[ ] // Lesson 3: User-Defined functions usage sample
[ ] use "Functions.inc"
[ ]
[+] private VOID _printArgs( STRING sDescription , varargs OF ANYTYPE laParams )
[ ] ANYTYPE aValue
[ ]
[+] for each aValue in laParams
[ ] Print( sDescription + ": {aValue}")
[ ]
[+] main()
[+] LIST OF STRING lsValue = {...}
[ ] "Test1"
[ ] "line"
[ ] "Test2"
[ ] "text"
[ ] STRING sValue
[ ]
[ ]
[ ] sValue = Join( lsValue , " " )
[ ] Print( sValue )
[ ]
[ ] INTEGER i = 3
[ ] Inc( i )
[ ] Print( i )
[ ]
[ ] _printArgs( "Test" , 1 , "Some text" , STRING , 3.4 )
[+] _printArgs( "Values" , {1,2,3} , {...} )
[ ] "val1"
[ ] "val2"
[ ] "val3"
[ ]
[ ] Print( Factorial( 9 ) )
[ ] Print( Factorial( 5 ) )
[ ]
[ ] INTEGER iValue = @( "Factorial" )( 4 )
[ ]
[+] LIST OF STRING lsNames = {...}
[ ] "Join"
[ ] "Factorial"
[+] LIST OF LIST OF ANYTYPE llaArgs = {...}
[+] {...}
[+] {...}
[ ] "Line"
[ ] "split"
[ ] "into"
[ ] "list"
[ ] " - "
[+] {...}
[ ] 6
[ ]
[+] for iValue = 1 to 2
[+] do
[ ] Print( ArgListCall( lsNames[iValue] , llaArgs[iValue] ) )
[+] except
[ ] ExceptPrint()
[ ]
[ ]
[ ]
[ ]
|
