JavaRush /Курси /Go SELF /Шаблон read → parse → compute → print

Шаблон read → parse → compute → print

Go SELF
Рівень 2 , Лекція 5
Відкрита

1. Вступ

Коли ви тільки починаєте розв’язувати задачі, дуже легко перетворити main() на кашу: половина рядків читає вхід, чверть щось рахує, ще трохи друкує, а посередині раптом конкатенація рядків і спроба помножити текст на число. Спойлер: Go цього не оцінить. Шаблон readparsecomputeprint потрібен для того, щоб ваш код був передбачуваним: ви завжди розумієте, на якому етапі перебуваєте, і де шукати помилку, якщо результат не збігається з очікуваним.

Найприємніше в цьому шаблоні — те, що він підходить майже до всіх задач зі стандартним введенням і виведенням. Навіть якщо задача проста, звичка розкладати все по етапах економить нерви. А нерви, як відомо, у програмуванні — витратний матеріал. Тож добре, коли витрата помірна.

Загальна схема: що означає кожен крок

Якщо описати шаблон людською мовою, вийде буквально так: «прочитав → зрозумів → порахував → показав результат». Звучить як звичайна розмова, і це добре: програмування для новачка має бути максимально розмовним, без відчуття, що ви викликаєте давніх духів.

Ось схема, яку корисно тримати в голові, а іноді й прямо писати в коментарях у коді:

flowchart LR
    A[read: читаємо вхідні дані] --> B[parse: за потреби перетворюємо типи]
    B --> C[compute: обчислюємо результат]
    C --> D[print: виводимо результат у потрібному форматі]

І ось невелика таблиця: що зазвичай роблять на кожному етапі.

Етап Що робимо Типовий результат етапу
read
зчитуємо дані в змінні a, b, s, n отримали значення
parse
перетворюємо рядок на число або навпаки n := atoi(s) або text := itoa(n)
compute
виконуємо обчислення sum, area, change, result
print
друкуємо у потрібному форматі один рядок, одне число або кілька значень

Важливо: крок parse потрібен не завжди. Якщо вхід уже є числом і ми читаємо його в int, тоді парсити нічого не потрібно. Але якщо число надходить у вигляді тексту, без Atoi не обійтися.

2. Крок read: читаємо вхід так, щоб далі було зручно

На етапі read ми виконуємо найтиповішу роботу: оголошуємо змінні й зчитуємо в них значення. Тут часто виникає спокуса одразу щось порахувати просто всередині Scan, але краще не поспішати: спочатку дістаньмо дані, а потім уже думатимемо.

Найпростіший варіант — читаємо числа одразу в int:

package main

import "fmt"

func main() {
	var a, b int
	fmt.Scan(&a, &b)     // read
	fmt.Println(a + b)   // compute+print (поки в один рядок)
}

Цей приклад робочий, але в ньому етапи зведені докупи. Для тренування шаблону краще розділяти їх, навіть якщо здається: «Та ну, тут же 2 рядки».

Ось та сама думка, але акуратніше:

package main

import "fmt"

func main() {
	// read
	var a, b int
	fmt.Scan(&a, &b)

	// compute
	sum := a + b

	// print
	fmt.Println(sum)
}

Порожні рядки й коментарі тут — не прикраса. Це візуальні межі. Потім ви будете вдячні собі, коли почнете помилятися в довших задачах. Тобто приблизно завтра. Або, точніше, в якийсь момент.

3. Крок parse: коли вхід текстовий, а рахувати треба числа

На етапі parse ми перетворюємо один тип даних на інший. І тут важливо прийняти філософію Go: «само не перетвориться». Спочатку це дратує, але потім раптом стає зрозуміло, чому так безпечніше: ви не можете випадково скласти «12» і 3 та отримати «123» або 15. Ви зобов’язані явно обрати потрібний варіант.

Припустімо, за умовою задачі вам дають число як рядок. Це трапляється частіше, ніж здається: коди, номери, іноді «0012» тощо. Тоді read читає рядок, а parse перетворює його на int:

package main

import (
	"fmt"
	"strconv"
)

func main() {
	// read
	var s string
	fmt.Scan(&s)

	// parse
	n, _ := strconv.Atoi(s)

	// compute
	double := n * 2

	// print
	fmt.Println(double)
}

Тут ми використали _, щоб ігнорувати помилку. У навчальних задачах це інколи допустимо, бо формат введення гарантований: якщо написано «на вхід подається ціле число», значить, так і буде. Але важливо розуміти: Atoi повертає (int, error), а error — це звичайне значення, яке можна перевіряти й обробляти. У Go прийнято явно працювати з помилками як зі значеннями.

Якщо ви хочете хоча б побачити, чи сталася помилка, без розгалужень, які ми ще не використовуємо, просто виведіть err:

package main

import (
	"fmt"
	"strconv"
)

func main() {
	var s string
	fmt.Scan(&s)

	n, err := strconv.Atoi(s)
	fmt.Println(n)    // якщо є помилка, n буде 0
	fmt.Println(err)  // <nil>, якщо все добре
}

Так, це не красива обробка. Але для розуміння механіки — чудово.

4. Крок compute: рахуємо результат і не змішуємо етапи

Етап compute — це місце, де ви розв’язуєте задачу. Тут дуже корисно триматися правила: «одна змінна — одна ідея». Новачки часто пишуть величезну формулу просто всередині Println, а потім губляться: помилка в арифметиці, у пріоритеті операцій чи взагалі в тому, що ввід був не той.

Замість цього краще так: проміжні значення — у змінні з нормальними іменами.

Наприклад, нехай у нас є задача: дані ширина й висота прямокутника, треба вивести площу та периметр.

package main

import "fmt"

func main() {
	// read
	var width, height int
	fmt.Scan(&width, &height)

	// compute
	area := width * height
	perimeter := 2*width + 2*height

	// print
	fmt.Println(area, perimeter)
}

Зверніть увагу: perimeter := 2*width + 2*height читається легко, бо ми не намагаємося робити математику на швидкість. Якщо хочеться ще ясніше, можна додати дужки, навіть коли вони не потрібні. Іноді це цілком нормальна плата за читабельність.

5. Крок print: виведення — це частина розв’язання, а не «ну потім якось»

Виведення в навчальних задачах — підступна річ. Можна ідеально порахувати відповідь, але вивести «Відповідь: 42» замість «42» — і отримати неправильне рішення. Програма не зобов’язана бути ввічливою. Їй треба бути точною.

На етапі print корисно пам’ятати кілька речей.

fmt.Println друкує аргументи через пробіл і додає переведення рядка. fmt.Print друкує без переведення рядка. Якщо ви виводите кілька чисел, Println — найпростіший шлях.

package main

import "fmt"

func main() {
	x := 5
	y := 7
	fmt.Println(x, y) // 5 7
}

Якщо вам потрібно зібрати рядок вручну, наприклад «x=5», тоді або друкуйте аргументами, або використовуйте strconv.Itoa. Конкатенація рядків із числами напряму не спрацює.

package main

import (
	"fmt"
	"strconv"
)

func main() {
	x := 5
	line := "x=" + strconv.Itoa(x)
	fmt.Println(line) // x=5
}

6. Міні‑приклад: «Каса»

Зараз зберемо маленький єдиний застосунок, який ідеально лягає на шаблон: міні‑«каса». Ідея проста: на вхід подається вартість і сума оплати (у центах), а на вихід — решта, теж у центах, а потім — розкладена на долари й центи. Умови тут нам поки не потрібні, тому вважаємо, що оплата завжди не менша за вартість.

Почнемо з версії, де на вхід уже надходять числа (int). Це прямолінійно й приємно.

package main

import "fmt"

func main() {
	// read
	var priceCents, paidCents int
	fmt.Scan(&priceCents, &paidCents)

	// compute
	changeCents := paidCents - priceCents
	changeDollars := changeCents / 100
	changeRestCents := changeCents % 100

	// print
	fmt.Println(changeDollars, changeRestCents)
}

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

package main

import (
	"fmt"
	"strconv"
)

func main() {
	// read
	var priceStr, paidStr string
	fmt.Scan(&priceStr, &paidStr)

	// parse
	priceCents, _ := strconv.Atoi(priceStr)
	paidCents, _ := strconv.Atoi(paidStr)

	// compute
	changeCents := paidCents - priceCents
	dollars := changeCents / 100
	cents := changeCents % 100

	// print
	fmt.Println(dollars, cents)
}

Зверніть увагу, як передбачувано це читається: ви буквально бачите, де ввід, де парсинг, де математика, де виведення. Це і є головна цінність шаблону.

7. Як називати змінні, щоб не плутатися

Хороші імена змінних — це коли ви можете прочитати код як історію. Погані — коли код перетворюється на x1, x2, tmp, res2, і ви починаєте вгадувати: tmp — це тимчасово чи назавжди? Чому res2, а де res1? А x — це ширина чи вік кота?

Домовленість, яка добре працює в задачах із введенням і виведенням: вхідні змінні називаємо за змістом, а вихідні — за роллю.

Наприклад, порівняйте два варіанти. Формально обидва працюють, але мозок сприймає їх по-різному.

package main

import "fmt"

func main() {
	var a, b int
	fmt.Scan(&a, &b)
	c := a*b + a + b
	fmt.Println(c)
}

А тепер так:

package main

import "fmt"

func main() {
	var width, height int
	fmt.Scan(&width, &height)

	area := width * height
	perimeter := 2*width + 2*height

	fmt.Println(area, perimeter)
}

У другому випадку навіть людина, яка не писала цю програму, розуміє, що відбувається. І це важливіше, ніж здається: через кілька годин ви самі вже можете бути не тією людиною, яка писала код. Пам’ять любить відпустку.

Нижче — маленька підказка-таблиця: як часто зручно називати змінні в навчальних задачах.

Зміст Приклади імен
розміри
width, height, length
кількість
n, count, total
сума
sum, totalSum
відповідь
result, answer
вхідний рядок
s, text, priceStr
число після парсингу
n, value, priceCents

Зверніть увагу: інколи n — цілком нормальне ім’я. Але лише тоді, коли воно справді означає кількість або розмір, а не коли ви просто втомилися вигадувати назви.

8. Мікродіагностика: друк проміжних значень

Коли ви розв’язуєте задачу й отримуєте не той результат, дуже хочеться одразу впасти в містику: «комп’ютер зламався», «у Go баг», «всесвіт проти мене». На практиці найчастіше проблема в одному з етапів: не так прочитали, не так розпарсили, не так порахували або не так вивели.

Поки ми ще не використовуємо розгалуження, найкращий спосіб налагодження — тимчасово друкувати проміжні значення. Причому так, щоб потім їх легко було прибрати.

Наприклад, у «касі»:

package main

import "fmt"

func main() {
	var priceCents, paidCents int
	fmt.Scan(&priceCents, &paidCents)

	changeCents := paidCents - priceCents

	fmt.Println(priceCents, paidCents, changeCents) // тимчасове налагодження

	dollars := changeCents / 100
	cents := changeCents % 100
	fmt.Println(dollars, cents)
}

Так, це ламає формат виведення для перевіряльної системи. Але налагодження й фінальна версія — це різні стани коду. Головне — не забути прибрати налагоджувальні рядки перед здачею. Це вічна класика.

9. Типові помилки

Помилка №1: змішування етапів до стану «незрозуміло, де що».
Початківець часто пише: fmt.Scan(&a); fmt.Println("ans", a+something) — і так по всій програмі. Нібито працює, доки задача маленька. Але щойно з’являється парсинг рядка, кілька входів або пара проміжних обчислень, код перетворюється на клубок. Лікується дисципліною: спочатку read, потім, якщо потрібно, parse, далі compute, а потім print. Між ними — порожні рядки.

Помилка №2: спроба склеїти рядок і число оператором +.
У Go оператор + або додає числа, або склеює рядки. Він не робить «і те, і інше за настроєм». Тому "age=" + age не скомпілюється. Якщо потрібно вивести разом, друкуйте аргументами (fmt.Println("age=", age)) або перетворюйте число на рядок через strconv.Itoa.

Помилка №3: парсинг роблять про всяк випадок, навіть коли він не потрібен.
Іноді студент уже дізнався про Atoi і починає використовувати його завжди: читає int через Scan, а потім думає: «А чи не розпарсити ще раз?» Це зайве й тільки плутає. Якщо вхід — число, читайте його в int і одразу рахуйте. parse потрібен лише тоді, коли тип входу не збігається з типом, з яким ви хочете працювати.

Помилка №4: імена змінних на кшталт x, x1, x2, tmp, коли зміст важливий.
Короткі імена допустимі, але вони не мають перетворювати задачу на шифрування. Якщо величина — ширина, нехай буде width. Якщо це вартість у центах, нехай буде priceCents. Такі імена зменшують кількість логічних помилок: ви рідше переплутаєте величини місцями, бо вони виглядають по-різному.

Помилка №5: неправильне виведення через «дружні слова».
Дуже хочеться писати fmt.Println("Відповідь:", result) — програма виглядає розумнішою. Але перевіряльна система зазвичай очікує строго result, без префіксів. Тому на етапі print тримайте в голові просте правило: виводимо тільки те, що просить умова, і рівно в тому форматі, який просить умова.

1
Опитування
Робота з цілими числами, рівень 2, лекція 5
Недоступний
Робота з цілими числами
Робота з цілими числами
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ