Итак, мы изучили условия, циклы, операторы try ... catch ... finally, using. Этого почти достаточно для написания сложной логики. Однако на практике так логику никто не пишет. Вместо этого всю программу разделяют на небольшие кусочки, так что каждый кусочек выполняет какое-то одно осмысленное действие. Разбиение на кусочки идёт вплоть до того, что минимальный размер кусочка может достигать 1-2 строки. Во-первых, такие кусочки позволяют не дублировать код: дублирующийся кусок кода выносят в отдельную функцию, и просто вызывают её из разных мест программы. Также функции улучшают читаемость, потому что каждый блок кода имеет своё название. Они упрощают тестирование (можно протестировать каждый кусочек по отдельности) и, наконец, упрощают командную работу (ты пишешь один кусочек, а я - другой). Сложность темы состоит в том, что нужно работать сразу с большим количеством кусочков, которые взаимодействуют друг с другом сложным образом. Нужна перестройка мышления.
Функция - это блок кода, имеющий собственное название. Обычно каждая функция отвечает за какое-то одно осмысленное действие.
Чтобы функция могла взаимодействовать с другими кусками кода, у неё есть входные аргументы и результат. Тот, кто вызывает функцию, подаёт ей на вход какие-то данные (входные аргументы) и получает в ответ результат.
Объявление функции в C# и Си-подобных языках выглядит так:
ТипДанныхРезультата НазваниеФункции(ТипАргумента1 названиеАргумента1, ...)
{
...команды...
}
Как выдать что-либо в ответ из функции
Команда return результат; завершает выполнение функции и выдаёт результат в ответ. Если функция не возвращает результат в ответ, то используется return; без ничего.
Например:
Эта функция называется Sum(). Она получает на вход два числа - first и second. Внутри неё создаётся новая переменная с результатом (строка 3), который затем выдаётся в ответ (строка 4). Если мы напишем после return result; ещё какие-то команды, то они не будут выполняться, потому что return делает выход из функции.
Поскольку язык программирования - это конструктор, то мы можем вместо переменной result в 4-ой строке поставить любое другое выражение, лишь бы оно имело подходящий тип данных (в нашем случае double). Например:
или
Как подать что-то на вход функции
Тот, кто вызывает функцию, пишет её название со скобками и перечисляет входные аргументы через запятую:
Входные аргументы распознаются по порядку - первый пишется на первом месте, второй на втором и так далее. То есть, при вызове функции first будет равен 100.0, потому что он на первом месте, а second будет равен 7.0, потому что он на втором. Мы можем явно указать названия входных аргументов:
но так почти никто не делает. Чтобы понять, что на каком месте стоит, всегда можно поставить каретку на входной параметр и нажать Ctrl + P в Rider (от слова parameters):
Если у функции нет входных параметров, пишутся пустые круглые скобки - они помогают отличить вызов функции от переменной. Если функция ничего не возвращает в ответ, то указывается тип данных void, а return становится необязательным:
В этом примере мы также вызываем функцию PrintSum() несколько раз. В первый раз x=2.0, y=3.0, а во второй - x=4.0, y=5.0. Каждый раз при входе в функцию входные аргументы (переменные x и y) заново создаются и заново рассчитывается результат.
Отладка: шаг с заходом и шаг с выходом.
Я буду рассказывать на примере Rider, но всё основное есть и в Visual Studio, и в других средах разработки.
В последнем примере мы вызываем функцию PrintSum(), а она вызывает Sum(). Чтобы наглядно увидеть, как это работает, используем отладку. Поставим точку остановки (красный кружок на полях) в месте первого вызова PrintSum() и запустим программу на жучка:
Внизу Rider откроется панель отладки. Теперь наконец мы сможем понять, зачем в Rider 3 разных кнопки "сделать шаг":
- Шаг с обходом - делает следующий шаг на том же уровне, не заходя в вызываемые функции. Если мы нажмём его, то перейдём со строки 15 на строку 16, не заходя в вызов PrintSum().
- Шаг с заходом - заходит внутрь вызываемых функций (если есть). Мы окажемся внутри PrintSum() на строке 8. Нажмите. Посмотрите, чему равны x и y.
- Шаг с выходом - выполняет весь код до конца текущей функции и выходит из неё. Если мы были на строке 8, то мы выйдем из функции PrintSum() и окажемся на строке 15. Нажмите.
Зайдите внутрь второго вызова PrintSum(). Посмотрите, чему теперь равны x и y. Сделайте несколько шагов с обходом. Когда строки внутри функции закончатся, мы выйдем из неё даже не смотря на то, что нажимаем "Шаг с обходом".
Отладка: стек вызовов
Запустите программу заново. Она остановится на первом вызове PrintSum(). Пока не входите в него. Посмотрите на так называемый стек вызовов - он показывает, внутри какой функции мы сейчас находимся и кто её вызвал:
Сейчас мы внутри функции <Main>$(). Весь код файла Program.cs, который не лежит внутри никакой функции, на самом деле лежит внутри этой служебной функции <Main>$(). При запуске программы эта функция запускается первой.
Теперь сделаем шаг с заходом внутрь PrintSum(). Стек будет выглядеть так:
Он показывает, что Main() вызвала PrintSum() (читать снизу вверх). Сделаем шаг с заходом внутри PrintSum(). В 9-ой строчке вызывается Sum() и мы хотим зайти внутрь неё. Однако вместо неё мы сначала зайдём в служебную функцию, которая обрабатывает $""-строку:
Вот для таких случаев, чтобы не копаться внутри служебной функции и сразу дойти до её конца и существует кнопка "Шаг с выходом". Нажмите её.
Мы окажемся снова на 9-ой строке, где вызов Sum(). Попробуем зайти внутрь ещё раз. Мы снова окажемся в служебной функции, и только с третьего раза нам удастся зайти внутрь Sum():
Итак, системная функция Main() запустила нашу PrintSum(), а PrintSum() запустила Sum(). Такая цепочка вызовов обычно представляется не в виде списка List<T>, а в виде стека. Разберём, что это такое. Проще описать, как оно работает. Сначала начинается функция Main() и она кладётся в стек. Затем она вызывает функцию PrintSum(), и она тоже кладётся в стек. Затем PrintSum() вызывает Sum(), и она тоже кладётся в стек. Однако достаются из стека они в обратном порядке: сначала заканчивается та функция, которая была вызвана последней, то есть, Sum(). Потом заканчивается PrintSum(), а потом заканчивается Main(). Вызвавшая функция не может закончится раньше вызванной, например, PrintSum() не может закончится, пока не закончится Sum(), которая вызвана ей. Итак, стек - это такая структура данных, в которой элементы кладутся в одном порядке, а достаются в противоположном. Мы можем создать стек (просто стек чего-нибудь, не обязательно вызовов) на C# и делать с ним что-то.
Отладка: области видимости переменных
Запустим программу заново. Программа остановится на первом вызове PrintSum(). На панели отладки справа от стека располагается список видимых сейчас переменных:
Это result, объявленный на 12-й строчке.
Зайдём внутрь первого вызова PrintSum().
Внутри функции видны её входные аргументы (x, y) и не видны наружные переменные (result). Если мы захотим использовать result внутри PrintSum(), то мы всегда можем добавить ещё один входной аргумент для него и передать его туда на вход.
Зайдём внутрь Sum(). Внутри этой функции видны first и second, а x и y нет. Числа, которые в PrintSum() были обозначены x и y, были переданы на вход Sum() под названиями first и second:
Виден также result, но это другой result, не тот, который был в Main() (12-ая строчка). Внутри Sum() на 3-й строчке объявлен result, и пока мы не дошли до его вычисления, он равен 0. То есть, внутри функций могут быть объявлены переменные с такими же названиями, как снаружи, и они не мешают друг другу.
Также можно щёлкать по уровням стека вызовов, чтобы переключаться между ними. Переменные на каждом уровне видны только когда мы на нём находимся.
Далее
Чтение функций - https://dzen.ru/a/aZbRCa9NhAzfStBO?share_to=link
Оглавление - https://dzen.ru/a/aXisxwt_Mnz2qTjs?share_to=link