Свои функции в задаче
Две вещи, которые делают с функциями чаще всего: выносят в них кусок уже написанной программы и пишут сразу, когда в условии есть правило, которое считается по-разному в разных случаях. Сегодня по одному заданию на то и на другое.
Когда кусок пора выносить
Признаков три, и ни один из них не про длину:
- Повтор. Один и тот же расчёт встречается второй раз. Не третий — второй: третий раз обычно появляется уже после того, как первый пришлось чинить.
- У куска есть имя. Если над несколькими строками хочется написать комментарий-заголовок («считаем коробки», «проверяем вес»), то это и есть имя будущей функции, а комментарий после выноса не нужен.
- Кусок мешает читать главное. В
mainвидно последовательность шагов; подробности каждого шага отвлекают.
И обратное: выносить ради одной строки не нужно. Функция func sum(a int, b int) int { return a + b } читается хуже, чем a + b.
Как выносить — по шагам. Возьмём повтор с первой страницы:
qtyA := 14
boxesA := qtyA / 6
if qtyA%6 != 0 {
boxesA++
}- Обвести кусок. Где он начинается и где кончается: три строки, от деления до закрывающей скобки
if. - Что входит. Какие значения кусок берёт снаружи:
qtyAи число 6. Это будущие параметры:qtyиsize. - Что выходит. Какое одно значение кусок производит:
boxesA. Это тип результата —int. - Имя. Существительное, потому что возвращается значение:
boxes. - Перенести. Заголовок
func boxes(qty int, size int) int, тело — тот же кусок, где имена заменены на параметры, иreturnв конце. - Заменить. Каждый исходный кусок — вызовом:
boxes(14, 6). - Проверить. Прогнать программу на тех же вводах и убедиться, что вывод не изменился ни на символ.
Шаг 7 — не формальность. Перенос кода ломается на мелочах: не то имя подставили, забыли +1, перепутали параметры местами. Поэтому прогон «до» и «после» на одних и тех же данных — обязательная часть работы. Завтрашние дни доведут эту мысль до конца: в дне 14 такие прогоны превратятся в тесты, которые запускаются сами.
Попробуйте сейчас: вынести повтор в функцию.
Цель:
reportпечатает ту же сводку, что и раньше, но расчёт коробок написан в программе один раз.1. Прочитайте условие и запустите программу как есть — запомните вывод:
▶ Выполнитеcd ~/gocourse/day11/report cat TASK.txt go run . < example.txt2. Объявите свою функцию по шагам 1–6 из раздела выше и замените все три куска вызовами. Имя придумайте сами.
3. Прогоните на тех же данных и ещё на одном вводе — вывод должен совпасть с тем, что вы запомнили:
▶ Выполнитеgo run . < example.txt printf '5\n5\n10\n1\n' | go run . gofmt -l .Готово, когда: вывод на
example.txtне изменился, вmainостались только чтение чисел и печать — ни деления, ни остатка, ни++, ни цикла, — аgofmt -l .ничего не печатает: пунктыreport,splitиcode_fmt.
Правило с порогом внутри функции
Ситуация. Цена за штуку зависит от размера заказа: от какого-то количества она становится ниже, и новая цена действует на весь заказ. Такое правило в main разворачивается в несколько строк, и каждый раз, когда цена понадобится ещё где-то, эти строки приходится повторять.
Внутри функции оно занимает ровно столько места, сколько занимает в условии задачи:
package main
import "fmt"
func cost(qty int, price int, bulk int, bulkPrice int) int {
if qty >= bulk {
return qty * bulkPrice
}
return qty * price
}
func main() {
fmt.Println("10 шт.:", cost(10, 4500, 20, 3900), "коп.")
fmt.Println("20 шт.:", cost(20, 4500, 20, 3900), "коп.")
}Что увидите.
stagiaire@lab:~/gocourse/day11/scratch$ go run .
10 шт.: 45000 коп.
20 шт.: 78000 коп.Как это читать. Здесь ранний выход с третьей страницы работает в функции с результатом: если условие истинно, return отдаёт ответ и на этом всё; строка после if выполнится только тогда, когда условие ложно. else не нужен — он ничего бы не добавил.
Четыре параметра для такой функции — многовато; читать вызов cost(10, 4500, 20, 3900) тяжело, и порядок аргументов легко перепутать (пятая страница). Когда цена и порог у задачи одни и те же, их пишут внутри функции, а параметром оставляют только то, что меняется от вызова к вызову:
package main
import "fmt"
func cost(qty int) int {
if qty >= 20 {
return qty * 3900
}
return qty * 4500
}
func main() {
fmt.Println("10 шт.:", cost(10), "коп.")
fmt.Println("20 шт.:", cost(20), "коп.")
}stagiaire@lab:~/gocourse/day11/scratch$ go run .
10 шт.: 45000 коп.
20 шт.: 78000 коп.Такой выбор — не «всегда правильный». Пока цена одна, короткая форма удобнее; когда цен станет несколько, их снова придётся передавать снаружи. В сегодняшнем задании cost цена и порог — ваши, они написаны в условии, и внутри функции им самое место.
Границу проверяйте отдельно. «От 20 штук» — это qty >= 20, а не qty > 20. Ошибка на единицу из дня 08 никуда не делась: заказ ровно в 20 штук должен посчитаться по новой цене. Проверить это можно только вводом, где количество равно порогу, — в скрытых случаях проверки такой ввод есть.
Попробуйте сейчас: задание cost.
Цель:
cost/main.goсчитает стоимость каждого заказа своей функцией и печатает итог.1. Прочитайте условие — цена, порог и вторая цена в нём ваши:
▶ Выполнитеcd ~/gocourse/day11/cost cat TASK.txt2. Напишите программу в
main.go. Начните с функции: заголовокfunc cost(qty int) intи две строки сreturn. Потомmain: чтение строк до конца ввода — как в дне 08, — вызовcostна каждой строке, счётчик заказов и сумма.3. Проверьте себя на примере из условия, на пустом вводе и на числе, равном порогу:
▶ Выполнитеgo run . < example.txt go run . < /dev/null gofmt -l .Готово, когда: на
example.txtвывод совпадает с примером вTASK.txt, на пустом вводе печатается одна строкаЗаказов не было, аgofmt -l .молчит — пунктыcostиcode_fmt.
Проверка «до и после»
Вынос кода не должен менять поведение программы — значит, поведение надо с чем-то сравнивать. Держать прежний вывод в голове ненадёжно: он длинный, а изменения мелкие. Надёжнее сохранить его в файл до правки и сравнить после.
stagiaire@lab:~/gocourse/day11/report$ go run . < example.txt > ~/gocourse/day11/scratch/before.txt
stagiaire@lab:~/gocourse/day11/report$ cat ~/gocourse/day11/scratch/before.txt
Склад A: 3 кор.
Склад B: 5 кор.
Склад C: 2 кор.
Всего коробок: 10Дальше выносите расчёт в функцию, а когда программа снова заработает, сравните вывод с сохранённым — командой diff из дня 08:
stagiaire@lab:~/gocourse/day11/report$ go run . < example.txt > after.txt
stagiaire@lab:~/gocourse/day11/report$ diff ~/gocourse/day11/scratch/before.txt after.txt
stagiaire@lab:~/gocourse/day11/report$ rm after.txtdiff молчит — значит, вывод совпал буква в букву. Это и есть ответ на вопрос «ничего ли я не сломал».
Одного примера мало: он проверяет один путь через программу. Возьмите хотя бы ещё два — с нулём и с числом, которое делится нацело, — и прогоните оба. В дне 14 такие наборы примеров превратятся в тест, который запускается одной командой и сам сравнивает вывод с ожидаемым; пока это делаете вы руками.
Перед тем как сдавать
Три команды, которые стоит выполнять в каждой папке с программой, прежде чем считать её готовой. Они знакомы с блоков 1 и 2, новое — только то, что теперь у вас в файле несколько функций:
| Команда | Что проверяет |
|---|---|
gofmt -l . |
отступы и пробелы; молчит — значит, всё на месте |
go vet . |
подозрительные места, которые компилятор пропускает |
go run . < example.txt |
что программа делает то, что написано в условии |
gofmt расставит отступы в теле функций сам, а вот пустую строку между функциями он не добавит — её ставите вы. Принято: одна пустая строка между объявлениями, внутри тела пустыми строками разделяют смысловые куски.
Что может пойти не так
| Что видите | Что это значит | Что делать |
|---|---|---|
| после выноса вывод изменился | при переносе потерялась строка или перепутаны имена | сравнить с прежним выводом на том же вводе; вернуть оригинал: cp ~/.course/orig/day11/report/main.go ~/gocourse/day11/report/main.go и вынести заново |
пункт split красный: «расчёт из main уехал не весь» |
один из трёх кусков остался на месте или коробки досчитываются в main другим способом |
заменить вызовом все три; в main оставить только чтение и печать |
пункт split красный, а функция есть |
деление осталось в main — например, в строке «всего» |
всё, что считает коробки, должно быть внутри функции |
| заказ ровно в порог считается по старой цене | строгое > вместо >= |
сверить с условием: «столько-то и больше» — это >= |
Итого не сходится с суммой строк |
сумма считается не из того же значения, что печатается | считать один раз: положить результат cost(qty) в переменную и использовать её и там и там |
на пустом вводе печатается Итого: 0 коп. |
проверка на «ни одного заказа» стоит не там | считать заказы и проверять счётчик после цикла, как в дне 08 |
Дальше: как сохранить сделанное так, чтобы можно было вернуться — course next